From e72f6cd958810e3af9b18a71bc078dbb8c5d6cac Mon Sep 17 00:00:00 2001 From: Joe LeVeque Date: Fri, 22 Jan 2021 12:00:45 -0800 Subject: [PATCH] [ledd] Minor refactor; add unit tests (#143) - Refactor ledd: - Remove useless try/catch from around imports - Move argument parsing out of `DaemonLedd.run()` method and into `main()` function, a more appropriate location - Fix LGTM alert for unreachable code - Add unit tests and report coverage: - Test passing good and bad command-line arguments to ledd process Unit test coverage with this patch: ``` ----------- coverage: platform linux, python 3.7.3-final-0 ----------- Name Stmts Miss Cover ---------------------------------- scripts/ledd 66 34 48% Coverage HTML written to dir htmlcov Coverage XML written to file coverage.xml ``` --- sonic-ledd/pytest.ini | 2 + sonic-ledd/scripts/ledd | 53 ++++++++++------------ sonic-ledd/setup.cfg | 2 + sonic-ledd/setup.py | 5 +++ sonic-ledd/tests/__init__.py | 0 sonic-ledd/tests/mock_swsscommon.py | 28 ++++++++++++ sonic-ledd/tests/test_ledd.py | 68 +++++++++++++++++++++++++++++ 7 files changed, 128 insertions(+), 30 deletions(-) create mode 100644 sonic-ledd/pytest.ini create mode 100644 sonic-ledd/setup.cfg create mode 100644 sonic-ledd/tests/__init__.py create mode 100644 sonic-ledd/tests/mock_swsscommon.py create mode 100644 sonic-ledd/tests/test_ledd.py diff --git a/sonic-ledd/pytest.ini b/sonic-ledd/pytest.ini new file mode 100644 index 000000000000..83b74d373c06 --- /dev/null +++ b/sonic-ledd/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +addopts = --cov=scripts --cov-report html --cov-report term --cov-report xml diff --git a/sonic-ledd/scripts/ledd b/sonic-ledd/scripts/ledd index 445e47981fdc..91e6162994a5 100644 --- a/sonic-ledd/scripts/ledd +++ b/sonic-ledd/scripts/ledd @@ -5,16 +5,13 @@ Front-panel LED control daemon for SONiC """ -try: - import getopt - import sys +import getopt +import sys - from sonic_py_common import daemon_base - from sonic_py_common import multi_asic - from sonic_py_common.interface import backplane_prefix - from swsscommon import swsscommon -except ImportError as e: - raise ImportError(str(e) + " - required module not found") +from sonic_py_common import daemon_base +from sonic_py_common import multi_asic +from sonic_py_common.interface import backplane_prefix +from swsscommon import swsscommon #============================= Constants ============================= @@ -42,25 +39,6 @@ class DaemonLedd(daemon_base.DaemonBase): # Run daemon def run(self): - # Parse options if provided - if (len(sys.argv) > 1): - try: - (options, remainder) = getopt.getopt(sys.argv[1:], - 'hv', - ['help', 'version']) - except getopt.GetoptError as e: - print(e) - print(USAGE_HELP) - sys.exit(2) - - for opt, arg in options: - if opt == '--help' or opt == '-h': - print(USAGE_HELP) - sys.exit(0) - elif opt == '--version' or opt == '-v': - print('ledd version ' + VERSION) - sys.exit(0) - # Load platform-specific LedControl module try: led_control = self.load_platform_util(LED_MODULE_NAME, LED_CLASS_NAME) @@ -117,10 +95,25 @@ class DaemonLedd(daemon_base.DaemonBase): if not key.startswith(backplane_prefix()): led_control.port_link_state_change(key, fvp_dict["oper_status"]) - return 1 - def main(): + # Parse options if provided + if len(sys.argv) > 1: + try: + (options, remainder) = getopt.getopt(sys.argv[1:], 'hv', ['help', 'version']) + except getopt.GetoptError as e: + print(e) + print(USAGE_HELP) + sys.exit(1) + + for opt, arg in options: + if opt == '--help' or opt == '-h': + print(USAGE_HELP) + sys.exit(0) + elif opt == '--version' or opt == '-v': + print('ledd version {}'.format(VERSION)) + sys.exit(0) + ledd = DaemonLedd(SYSLOG_IDENTIFIER) ledd.run() diff --git a/sonic-ledd/setup.cfg b/sonic-ledd/setup.cfg new file mode 100644 index 000000000000..b7e478982ccf --- /dev/null +++ b/sonic-ledd/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/sonic-ledd/setup.py b/sonic-ledd/setup.py index 4959d8031404..2a55a094b01a 100644 --- a/sonic-ledd/setup.py +++ b/sonic-ledd/setup.py @@ -16,6 +16,11 @@ setup_requires=[ 'wheel' ], + tests_require=[ + 'mock>=2.0.0; python_version < "3.3"', + 'pytest', + 'pytest-cov' + ], classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: No Input/Output (Daemon)', diff --git a/sonic-ledd/tests/__init__.py b/sonic-ledd/tests/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/sonic-ledd/tests/mock_swsscommon.py b/sonic-ledd/tests/mock_swsscommon.py new file mode 100644 index 000000000000..7ceabf43724d --- /dev/null +++ b/sonic-ledd/tests/mock_swsscommon.py @@ -0,0 +1,28 @@ +STATE_DB = '' + + +class Table: + def __init__(self, db, table_name): + self.table_name = table_name + self.mock_dict = {} + + def _del(self, key): + del self.mock_dict[key] + pass + + def set(self, key, fvs): + self.mock_dict[key] = fvs.fv_dict + pass + + def get(self, key): + if key in self.mock_dict: + return self.mock_dict[key] + return None + + +class FieldValuePairs(dict): + def __init__(self, len): + self.fv_dict = {} + + def __setitem__(self, key, val_tuple): + self.fv_dict[val_tuple[0]] = val_tuple[1] diff --git a/sonic-ledd/tests/test_ledd.py b/sonic-ledd/tests/test_ledd.py new file mode 100644 index 000000000000..f4be68357360 --- /dev/null +++ b/sonic-ledd/tests/test_ledd.py @@ -0,0 +1,68 @@ +import os +import sys +from imp import load_source + +import pytest +# TODO: Clean this up once we no longer need to support Python 2 +if sys.version_info.major == 3: + from unittest.mock import Mock, MagicMock, patch +else: + from mock import Mock, MagicMock, patch +from sonic_py_common import daemon_base + +SYSLOG_IDENTIFIER = 'ledd_test' +NOT_AVAILABLE = 'N/A' + +daemon_base.db_connect = MagicMock() + +test_path = os.path.dirname(os.path.abspath(__file__)) +modules_path = os.path.dirname(test_path) +scripts_path = os.path.join(modules_path, "scripts") +sys.path.insert(0, modules_path) + +os.environ["LEDD_UNIT_TESTING"] = "1" +load_source('ledd', scripts_path + '/ledd') +import ledd + + +def test_help_args(capsys): + for flag in ['-h', '--help']: + with patch.object(sys, 'argv', ['ledd', flag]): + with pytest.raises(SystemExit) as pytest_wrapped_e: + ledd.main() + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + out, err = capsys.readouterr() + assert out.rstrip() == ledd.USAGE_HELP.rstrip() + + +def test_version_args(capsys): + for flag in ['-v', '--version']: + with patch.object(sys, 'argv', ['ledd', flag]): + with pytest.raises(SystemExit) as pytest_wrapped_e: + ledd.main() + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + out, err = capsys.readouterr() + assert out.rstrip() == 'ledd version {}'.format(ledd.VERSION) + + +def test_bad_args(capsys): + for flag in ['-n', '--nonexistent']: + with patch.object(sys, 'argv', ['ledd', flag]): + with pytest.raises(SystemExit) as pytest_wrapped_e: + ledd.main() + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 1 + out, err = capsys.readouterr() + assert out.rstrip().endswith(ledd.USAGE_HELP.rstrip()) + + +class TestDaemonLedd(object): + """ + Test cases to cover functionality in DaemonLedd class + """ + + def test_run(self): + daemon_ledd = ledd.DaemonLedd(SYSLOG_IDENTIFIER) + # TODO: Add more coverage