From 58db2d53e34207c6471135a829dcf40d28711570 Mon Sep 17 00:00:00 2001 From: Joe LeVeque Date: Mon, 24 Aug 2020 10:35:22 -0700 Subject: [PATCH] [sonic-py-common] Add unit test framework (#5238) **- Why I did it** To install the framework for adding unit tests to the sonic-py-common package and report coverage. ** How I did it ** - Incorporate pytest and pytest-cov into sonic-py-common package build - Updgrade version of 'mock' installed to version 3.0.5, the last version which supports Python 2. This fixes a bug where the file object returned from `mock_open()` was not iterable (see https://bugs.python.org/issue32933) - Add support for Python 3 setuptools and pytest in sonic-slave-buster environment - Add tests for `device_info.get_machine_info()` and `device_info.get_platform()` functions - Also add a .gitignore in the root of the sonic-py-common directory, move all related ignores from main .gitignore file, and add ignores for files and dirs generated by pytest-cov --- .gitignore | 6 -- sonic-slave-buster/Dockerfile.j2 | 27 ++++--- src/sonic-py-common/.gitignore | 13 ++++ src/sonic-py-common/pytest.ini | 2 + src/sonic-py-common/setup.cfg | 2 + src/sonic-py-common/setup.py | 8 ++ .../sonic_py_common/device_info.py | 2 +- src/sonic-py-common/tests/__init__.py | 0 src/sonic-py-common/tests/device_info_test.py | 75 +++++++++++++++++++ 9 files changed, 119 insertions(+), 16 deletions(-) create mode 100644 src/sonic-py-common/.gitignore create mode 100644 src/sonic-py-common/pytest.ini create mode 100644 src/sonic-py-common/setup.cfg create mode 100644 src/sonic-py-common/tests/__init__.py create mode 100644 src/sonic-py-common/tests/device_info_test.py diff --git a/.gitignore b/.gitignore index 307a6cf0ae..5916ed62b3 100644 --- a/.gitignore +++ b/.gitignore @@ -28,12 +28,6 @@ platform/*/docker-*/Dockerfile # Installer-related files and directories installer/x86_64/platforms/ -src/sonic-py-common/**/*.pyc -src/sonic-py-common/.eggs/ -src/sonic-py-common/build -src/sonic-py-common/dist -src/sonic-py-common/sonic_py_common.egg-info - # Misc. files asic_config_checksum files/Aboot/boot0 diff --git a/sonic-slave-buster/Dockerfile.j2 b/sonic-slave-buster/Dockerfile.j2 index e7ad71c53a..88f905cb0f 100644 --- a/sonic-slave-buster/Dockerfile.j2 +++ b/sonic-slave-buster/Dockerfile.j2 @@ -209,8 +209,10 @@ RUN apt-get update && apt-get install -y \ clang \ pylint \ python-pytest \ + python3-pytest \ gcovr \ python-pytest-cov \ + python3-pytest-cov \ python-parse \ # For snmpd default-libmysqlclient-dev \ @@ -339,6 +341,21 @@ RUN export VERSION=1.14.2 \ && echo 'export PATH=$PATH:$GOROOT/bin' >> /etc/bash.bashrc \ && rm go$VERSION.linux-*.tar.gz +# For building Python packages +RUN pip install setuptools==40.8.0 +RUN pip3 install setuptools==49.6.00 + +# For running Python unit tests +RUN pip install pytest-runner==4.4 +RUN pip3 install pytest-runner==5.2 +RUN pip install mockredispy==2.9.3 +RUN pip3 install mockredispy==2.9.3 + +# For Python 2 unit tests, we need 'mock'. The last version of 'mock' +# which supports Python 2 is 3.0.5. In Python 3, 'mock' is part of 'unittest' +# in the standard library +RUN pip install mock==3.0.5 + # For p4 build RUN pip install \ ctypesgen==1.0.2 \ @@ -357,25 +374,17 @@ RUN apt-get purge -y python-click # For sonic utilities testing RUN pip install click natsort tabulate netifaces==0.10.7 fastentrypoints -# For sonic snmpagent mock testing -RUN pip3 install mockredispy==2.9.3 - RUN pip3 install "PyYAML>=5.1" # For sonic-platform-common testing RUN pip3 install redis # For supervisor build -RUN pip install meld3 mock +RUN pip install meld3 # For vs image build RUN pip install pexpect==4.6.0 -# For sonic-utilities build -RUN pip install mockredispy==2.9.3 -RUN pip install pytest-runner==4.4 -RUN pip install setuptools==40.8.0 - # For sonic-swss-common testing RUN pip install Pympler==0.8 diff --git a/src/sonic-py-common/.gitignore b/src/sonic-py-common/.gitignore new file mode 100644 index 0000000000..5f621ff9e2 --- /dev/null +++ b/src/sonic-py-common/.gitignore @@ -0,0 +1,13 @@ +**/*.pyc + +# Distribution / packaging +*.egg-info/ +.eggs/ +build/ +dist/ + +# Unit test / coverage reports +.cache +.coverage +coverage.xml +htmlcov/ diff --git a/src/sonic-py-common/pytest.ini b/src/sonic-py-common/pytest.ini new file mode 100644 index 0000000000..777811f8f4 --- /dev/null +++ b/src/sonic-py-common/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --cov=sonic_py_common --cov-report html --cov-report term --cov-report xml diff --git a/src/sonic-py-common/setup.cfg b/src/sonic-py-common/setup.cfg new file mode 100644 index 0000000000..b7e478982c --- /dev/null +++ b/src/sonic-py-common/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/src/sonic-py-common/setup.py b/src/sonic-py-common/setup.py index b129447262..f56ad96b04 100644 --- a/src/sonic-py-common/setup.py +++ b/src/sonic-py-common/setup.py @@ -27,6 +27,13 @@ setup( packages=[ 'sonic_py_common', ], + setup_requires= [ + 'pytest-runner' + ], + tests_require=[ + 'pytest', + 'mock==3.0.5' # For python 2. Version >=4.0.0 drops support for py2 + ], classifiers=[ 'Intended Audience :: Developers', 'Operating System :: Linux', @@ -35,5 +42,6 @@ setup( 'Programming Language :: Python', ], keywords='SONiC sonic PYTHON python COMMON common', + test_suite = 'setup.get_test_suite' ) diff --git a/src/sonic-py-common/sonic_py_common/device_info.py b/src/sonic-py-common/sonic_py_common/device_info.py index 99f818b0f2..b5aefc397f 100644 --- a/src/sonic-py-common/sonic_py_common/device_info.py +++ b/src/sonic-py-common/sonic_py_common/device_info.py @@ -6,7 +6,7 @@ import subprocess import yaml from natsort import natsorted -# TODD: Replace with swsscommon +# TODO: Replace with swsscommon from swsssdk import ConfigDBConnector, SonicDBConfig USR_SHARE_SONIC_PATH = "/usr/share/sonic" diff --git a/src/sonic-py-common/tests/__init__.py b/src/sonic-py-common/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/sonic-py-common/tests/device_info_test.py b/src/sonic-py-common/tests/device_info_test.py new file mode 100644 index 0000000000..f3b14b0a5f --- /dev/null +++ b/src/sonic-py-common/tests/device_info_test.py @@ -0,0 +1,75 @@ +import os +import sys + +# TODO: Remove this if/else block once we no longer support Python 2 +if sys.version_info.major == 3: + from unittest import mock +else: + # Expect the 'mock' package for python 2 + # https://pypi.python.org/pypi/mock + import mock + +from sonic_py_common import device_info + + +# TODO: Remove this if/else block once we no longer support Python 2 +if sys.version_info.major == 3: + BUILTINS = "builtins" +else: + BUILTINS = "__builtin__" + +MACHINE_CONF_CONTENTS = """\ +onie_version=2016.11-5.1.0008-9600 +onie_vendor_id=33049 +onie_machine_rev=0 +onie_arch=x86_64 +onie_config_version=1 +onie_build_date="2017-04-26T11:01+0300" +onie_partition_type=gpt +onie_kernel_version=4.10.11 +onie_firmware=auto +onie_switch_asic=mlnx +onie_skip_ethmgmt_macs=yes +onie_machine=mlnx_msn2700 +onie_platform=x86_64-mlnx_msn2700-r0""" + +EXPECTED_GET_MACHINE_INFO_RESULT = { + 'onie_arch': 'x86_64', + 'onie_skip_ethmgmt_macs': 'yes', + 'onie_platform': 'x86_64-mlnx_msn2700-r0', + 'onie_machine_rev': '0', + 'onie_version': '2016.11-5.1.0008-9600', + 'onie_machine': 'mlnx_msn2700', + 'onie_config_version': '1', + 'onie_partition_type': 'gpt', + 'onie_build_date': '"2017-04-26T11:01+0300"', + 'onie_switch_asic': 'mlnx', + 'onie_vendor_id': '33049', + 'onie_firmware': 'auto', + 'onie_kernel_version': '4.10.11' +} + + +class TestDeviceInfo(object): + @classmethod + def setup_class(cls): + print("SETUP") + + def test_get_machine_info(self): + with mock.patch("os.path.isfile") as mock_isfile: + mock_isfile.return_value = True + open_mocked = mock.mock_open(read_data=MACHINE_CONF_CONTENTS) + with mock.patch("{}.open".format(BUILTINS), open_mocked): + result = device_info.get_machine_info() + assert result == EXPECTED_GET_MACHINE_INFO_RESULT + open_mocked.assert_called_once_with("/host/machine.conf") + + def test_get_platform(self): + with mock.patch("sonic_py_common.device_info.get_machine_info") as get_machine_info_mocked: + get_machine_info_mocked.return_value = EXPECTED_GET_MACHINE_INFO_RESULT + result = device_info.get_platform() + assert result == "x86_64-mlnx_msn2700-r0" + + @classmethod + def teardown_class(cls): + print("TEARDOWN")