From 8652fd95285afef1ea2ef58a5728733184258d1c Mon Sep 17 00:00:00 2001 From: Alexei Znamensky <103110+russoz@users.noreply.github.com> Date: Tue, 29 Aug 2023 22:13:52 +1200 Subject: [PATCH] Refactored unit tests for modules based on CmdRunner (#7154) * refactored unit tests for modules based on CmdRunner * improved/fixed test helper * fixed sanity * refactored yaml spec out of the python file * small adjustments --- .../plugins/modules/cmd_runner_test_utils.py | 99 ++++++++ tests/unit/plugins/modules/test_opkg.py | 233 ++---------------- tests/unit/plugins/modules/test_opkg.yaml | 142 +++++++++++ 3 files changed, 257 insertions(+), 217 deletions(-) create mode 100644 tests/unit/plugins/modules/cmd_runner_test_utils.py create mode 100644 tests/unit/plugins/modules/test_opkg.yaml diff --git a/tests/unit/plugins/modules/cmd_runner_test_utils.py b/tests/unit/plugins/modules/cmd_runner_test_utils.py new file mode 100644 index 0000000000..189cba730d --- /dev/null +++ b/tests/unit/plugins/modules/cmd_runner_test_utils.py @@ -0,0 +1,99 @@ +# Copyright (c) Ansible project +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +from collections import namedtuple +from itertools import chain, repeat + +import pytest +import yaml + + +ModuleTestCase = namedtuple("ModuleTestCase", ["id", "input", "output", "run_command_calls"]) +RunCmdCall = namedtuple("RunCmdCall", ["command", "environ", "rc", "out", "err"]) + + +class CmdRunnerTestHelper(object): + def __init__(self, command, test_cases): + self.command = command + self._test_cases = test_cases + self.testcases = self._make_test_cases() + + @property + def cmd_fixture(self): + @pytest.fixture + def patch_bin(mocker): + mocker.patch('ansible.module_utils.basic.AnsibleModule.get_bin_path', return_value=os.path.join('/testbin', self.command)) + + return patch_bin + + def _make_test_cases(self): + test_cases = yaml.safe_load(self._test_cases) + + results = [] + for tc in test_cases: + tc["run_command_calls"] = [RunCmdCall(**r) for r in tc["run_command_calls"]] if tc.get("run_command_calls") else [] + + results.append(ModuleTestCase(**tc)) + + return results + + @property + def testcases_params(self): + return [[x.input, x] for x in self.testcases] + + @property + def testcases_ids(self): + return [item.id for item in self.testcases] + + def __call__(self, testcase, mocker): + return _Context(self, testcase, mocker) + + +class _Context(object): + def __init__(self, helper, testcase, mocker): + self.helper = helper + self.testcase = testcase + self.mocker = mocker + + self.run_cmd_calls = self.testcase.run_command_calls + self.mock_run_cmd = self._make_mock_run_cmd() + + def _make_mock_run_cmd(self): + call_results = [(x.rc, x.out, x.err) for x in self.run_cmd_calls] + error_call_results = (123, + "OUT: testcase has not enough run_command calls", + "ERR: testcase has not enough run_command calls") + mock_run_command = self.mocker.patch('ansible.module_utils.basic.AnsibleModule.run_command', + side_effect=chain(call_results, repeat(error_call_results))) + return mock_run_command + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return False + + def check_results(self, results): + print("testcase =\n%s" % str(self.testcase)) + print("results =\n%s" % results) + if 'exception' in results: + print("exception = \n%s" % results["exception"]) + + for test_result in self.testcase.output: + assert results[test_result] == self.testcase.output[test_result], \ + "'{0}': '{1}' != '{2}'".format(test_result, results[test_result], self.testcase.output[test_result]) + + call_args_list = [(item[0][0], item[1]) for item in self.mock_run_cmd.call_args_list] + expected_call_args_list = [(item.command, item.environ) for item in self.run_cmd_calls] + print("call args list =\n%s" % call_args_list) + print("expected args list =\n%s" % expected_call_args_list) + + assert self.mock_run_cmd.call_count == len(self.run_cmd_calls) + if self.mock_run_cmd.call_count: + assert call_args_list == expected_call_args_list diff --git a/tests/unit/plugins/modules/test_opkg.py b/tests/unit/plugins/modules/test_opkg.py index 8e52368ff9..055a8659d7 100644 --- a/tests/unit/plugins/modules/test_opkg.py +++ b/tests/unit/plugins/modules/test_opkg.py @@ -7,235 +7,34 @@ from __future__ import (absolute_import, division, print_function) __metaclass__ = type import json - -from collections import namedtuple -from ansible_collections.community.general.plugins.modules import opkg +from ansible_collections.community.general.plugins.modules import opkg as module import pytest -TESTED_MODULE = opkg.__name__ +from .cmd_runner_test_utils import CmdRunnerTestHelper -ModuleTestCase = namedtuple("ModuleTestCase", ["id", "input", "output", "run_command_calls"]) -RunCmdCall = namedtuple("RunCmdCall", ["command", "environ", "rc", "out", "err"]) - - -@pytest.fixture -def patch_opkg(mocker): - mocker.patch('ansible.module_utils.basic.AnsibleModule.get_bin_path', return_value='/testbin/opkg') - - -TEST_CASES = [ - ModuleTestCase( - id="install_zlibdev", - input={"name": "zlib-dev", "state": "present"}, - output={ - "msg": "installed 1 package(s)" - }, - run_command_calls=[ - RunCmdCall( - command=["/testbin/opkg", "list-installed", "zlib-dev"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="", - err="", - ), - RunCmdCall( - command=["/testbin/opkg", "install", "zlib-dev"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out=( - "Installing zlib-dev (1.2.11-6) to root..." - "Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib-dev_1.2.11-6_mips_24kc.ipk" - "Installing zlib (1.2.11-6) to root..." - "Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib_1.2.11-6_mips_24kc.ipk" - "Configuring zlib." - "Configuring zlib-dev." - ), - err="", - ), - RunCmdCall( - command=["/testbin/opkg", "list-installed", "zlib-dev"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="zlib-dev - 1.2.11-6\n", - err="", - ), - ], - ), - ModuleTestCase( - id="install_zlibdev_present", - input={"name": "zlib-dev", "state": "present"}, - output={ - "msg": "package(s) already present" - }, - run_command_calls=[ - RunCmdCall( - command=["/testbin/opkg", "list-installed", "zlib-dev"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="zlib-dev - 1.2.11-6\n", - err="", - ), - ], - ), - ModuleTestCase( - id="install_zlibdev_force_reinstall", - input={"name": "zlib-dev", "state": "present", "force": "reinstall"}, - output={ - "msg": "installed 1 package(s)" - }, - run_command_calls=[ - RunCmdCall( - command=["/testbin/opkg", "list-installed", "zlib-dev"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="zlib-dev - 1.2.11-6\n", - err="", - ), - RunCmdCall( - command=["/testbin/opkg", "install", "--force-reinstall", "zlib-dev"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out=( - "Installing zlib-dev (1.2.11-6) to root...\n" - "Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib-dev_1.2.11-6_mips_24kc.ipk\n" - "Configuring zlib-dev.\n" - ), - err="", - ), - RunCmdCall( - command=["/testbin/opkg", "list-installed", "zlib-dev"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="zlib-dev - 1.2.11-6\n", - err="", - ), - ], - ), - ModuleTestCase( - id="install_zlibdev_with_version", - input={"name": "zlib-dev=1.2.11-6", "state": "present"}, - output={ - "msg": "installed 1 package(s)" - }, - run_command_calls=[ - RunCmdCall( - command=["/testbin/opkg", "list-installed", "zlib-dev"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="", - err="", - ), - RunCmdCall( - command=["/testbin/opkg", "install", "zlib-dev=1.2.11-6"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out=( - "Installing zlib-dev (1.2.11-6) to root..." - "Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib-dev_1.2.11-6_mips_24kc.ipk" - "Installing zlib (1.2.11-6) to root..." - "Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib_1.2.11-6_mips_24kc.ipk" - "Configuring zlib." - "Configuring zlib-dev." - ), - err="", - ), - RunCmdCall( - command=["/testbin/opkg", "list-installed", "zlib-dev"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="zlib-dev - 1.2.11-6 \n", # This output has the extra space at the end, to satisfy the behaviour of Yocto/OpenEmbedded's opkg - err="", - ), - ], - ), - ModuleTestCase( - id="install_vim_updatecache", - input={"name": "vim-fuller", "state": "present", "update_cache": True}, - output={ - "msg": "installed 1 package(s)" - }, - run_command_calls=[ - RunCmdCall( - command=["/testbin/opkg", "update"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="", - err="", - ), - RunCmdCall( - command=["/testbin/opkg", "list-installed", "vim-fuller"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="", - err="", - ), - RunCmdCall( - command=["/testbin/opkg", "install", "vim-fuller"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out=( - "Multiple packages (libgcc1 and libgcc1) providing same name marked HOLD or PREFER. Using latest.\n" - "Installing vim-fuller (9.0-1) to root...\n" - "Downloading https://downloads.openwrt.org/snapshots/packages/x86_64/packages/vim-fuller_9.0-1_x86_64.ipk\n" - "Installing terminfo (6.4-2) to root...\n" - "Downloading https://downloads.openwrt.org/snapshots/packages/x86_64/base/terminfo_6.4-2_x86_64.ipk\n" - "Installing libncurses6 (6.4-2) to root...\n" - "Downloading https://downloads.openwrt.org/snapshots/packages/x86_64/base/libncurses6_6.4-2_x86_64.ipk\n" - "Configuring terminfo.\n" - "Configuring libncurses6.\n" - "Configuring vim-fuller.\n" - ), - err="", - ), - RunCmdCall( - command=["/testbin/opkg", "list-installed", "vim-fuller"], - environ={'environ_update': {'LANGUAGE': 'C', 'LC_ALL': 'C'}, 'check_rc': False}, - rc=0, - out="vim-fuller - 9.0-1 \n", # This output has the extra space at the end, to satisfy the behaviour of Yocto/OpenEmbedded's opkg - err="", - ), - ], - ), -] -TEST_CASES_IDS = [item.id for item in TEST_CASES] +TESTED_MODULE = module.__name__ +with open("tests/unit/plugins/modules/test_opkg.yaml", "r") as TEST_CASES: + helper = CmdRunnerTestHelper(command="opkg", test_cases=TEST_CASES) + patch_bin = helper.cmd_fixture @pytest.mark.parametrize('patch_ansible_module, testcase', - [[x.input, x] for x in TEST_CASES], - ids=TEST_CASES_IDS, + helper.testcases_params, ids=helper.testcases_ids, indirect=['patch_ansible_module']) @pytest.mark.usefixtures('patch_ansible_module') -def test_opkg(mocker, capfd, patch_opkg, testcase): +def test_opkg(mocker, capfd, patch_bin, testcase): """ - Run unit tests for test cases listen in TEST_CASES + Run unit tests for test cases listed in TEST_CASES """ - run_cmd_calls = testcase.run_command_calls + with helper(testcase, mocker) as ctx: + # Try to run test case + with pytest.raises(SystemExit): + module.main() - # Mock function used for running commands first - call_results = [(x.rc, x.out, x.err) for x in run_cmd_calls] - mock_run_command = mocker.patch('ansible.module_utils.basic.AnsibleModule.run_command', side_effect=call_results) + out, err = capfd.readouterr() + results = json.loads(out) - # Try to run test case - with pytest.raises(SystemExit): - opkg.main() - - out, err = capfd.readouterr() - results = json.loads(out) - print("testcase =\n%s" % str(testcase)) - print("results =\n%s" % results) - - for test_result in testcase.output: - assert results[test_result] == testcase.output[test_result], \ - "'{0}': '{1}' != '{2}'".format(test_result, results[test_result], testcase.output[test_result]) - - call_args_list = [(item[0][0], item[1]) for item in mock_run_command.call_args_list] - expected_call_args_list = [(item.command, item.environ) for item in run_cmd_calls] - print("call args list =\n%s" % call_args_list) - print("expected args list =\n%s" % expected_call_args_list) - - assert mock_run_command.call_count == len(run_cmd_calls) - if mock_run_command.call_count: - assert call_args_list == expected_call_args_list + ctx.check_results(results) diff --git a/tests/unit/plugins/modules/test_opkg.yaml b/tests/unit/plugins/modules/test_opkg.yaml new file mode 100644 index 0000000000..2a90699933 --- /dev/null +++ b/tests/unit/plugins/modules/test_opkg.yaml @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +# Copyright (c) Alexei Znamensky (russoz@gmail.com) +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +--- +- id: install_zlibdev + input: + name: zlib-dev + state: present + output: + msg: installed 1 package(s) + run_command_calls: + - command: [/testbin/opkg, list-installed, zlib-dev] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: "" + err: "" + - command: [/testbin/opkg, install, zlib-dev] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: | + Installing zlib-dev (1.2.11-6) to root... + Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib-dev_1.2.11-6_mips_24kc.ipk + Installing zlib (1.2.11-6) to root... + Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib_1.2.11-6_mips_24kc.ipk + Configuring zlib. + Configuring zlib-dev. + err: "" + - command: [/testbin/opkg, list-installed, zlib-dev] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: | + zlib-dev - 1.2.11-6 + err: "" +- id: install_zlibdev_present + input: + name: zlib-dev + state: present + output: + msg: package(s) already present + run_command_calls: + - command: [/testbin/opkg, list-installed, zlib-dev] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: | + zlib-dev - 1.2.11-6 + err: "" +- id: install_zlibdev_force_reinstall + input: + name: zlib-dev + state: present + force: reinstall + output: + msg: installed 1 package(s) + run_command_calls: + - command: [/testbin/opkg, list-installed, zlib-dev] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: | + zlib-dev - 1.2.11-6 + err: "" + - command: [/testbin/opkg, install, --force-reinstall, zlib-dev] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: | + Installing zlib-dev (1.2.11-6) to root... + Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib-dev_1.2.11-6_mips_24kc.ipk + Configuring zlib-dev. + err: "" + - command: [/testbin/opkg, list-installed, zlib-dev] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: | + zlib-dev - 1.2.11-6 + err: "" +- id: install_zlibdev_with_version + input: + name: zlib-dev=1.2.11-6 + state: present + output: + msg: installed 1 package(s) + run_command_calls: + - command: [/testbin/opkg, list-installed, zlib-dev] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: "" + err: "" + - command: [/testbin/opkg, install, zlib-dev=1.2.11-6] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: | + Installing zlib-dev (1.2.11-6) to root... + Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib-dev_1.2.11-6_mips_24kc.ipk + Installing zlib (1.2.11-6) to root... + Downloading https://downloads.openwrt.org/releases/22.03.0/packages/mips_24kc/base/zlib_1.2.11-6_mips_24kc.ipk + Configuring zlib. + Configuring zlib-dev. + err: "" + - command: [/testbin/opkg, list-installed, zlib-dev] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: "zlib-dev - 1.2.11-6 \n" # This output has the extra space at the end, to satisfy the behaviour of Yocto/OpenEmbedded's opkg + err: "" +- id: install_vim_updatecache + input: + name: vim-fuller + state: present + update_cache: true + output: + msg: installed 1 package(s) + run_command_calls: + - command: [/testbin/opkg, update] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: "" + err: "" + - command: [/testbin/opkg, list-installed, vim-fuller] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: "" + err: "" + - command: [/testbin/opkg, install, vim-fuller] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: | + Multiple packages (libgcc1 and libgcc1) providing same name marked HOLD or PREFER. Using latest. + Installing vim-fuller (9.0-1) to root... + Downloading https://downloads.openwrt.org/snapshots/packages/x86_64/packages/vim-fuller_9.0-1_x86_64.ipk + Installing terminfo (6.4-2) to root... + Downloading https://downloads.openwrt.org/snapshots/packages/x86_64/base/terminfo_6.4-2_x86_64.ipk + Installing libncurses6 (6.4-2) to root... + Downloading https://downloads.openwrt.org/snapshots/packages/x86_64/base/libncurses6_6.4-2_x86_64.ipk + Configuring terminfo. + Configuring libncurses6. + Configuring vim-fuller. + err: "" + - command: [/testbin/opkg, list-installed, vim-fuller] + environ: {environ_update: {LANGUAGE: C, LC_ALL: C}, check_rc: false} + rc: 0 + out: "vim-fuller - 9.0-1 \n" # This output has the extra space at the end, to satisfy the behaviour of Yocto/OpenEmbedded's opkg + err: ""