From c022e574ac42b923ba8011287bf20d232be0330e Mon Sep 17 00:00:00 2001 From: Nicolas Marcq Date: Fri, 25 Nov 2022 11:33:21 +0100 Subject: [PATCH] add unit test for the cli --- docs/build.md | 48 ++++++++++ monkeyble/cli/const.py | 2 + monkeyble/cli/exceptions.py | 15 +++ monkeyble/cli/monkeyble_cli.py | 21 ++-- setup.py | 11 ++- tests/mocks.yml | 2 +- tests/units/test_cli.py | 132 ++++++++++++++++++++++++++ tests/units/test_config/monkeyble.yml | 10 ++ 8 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 docs/build.md create mode 100644 monkeyble/cli/exceptions.py create mode 100644 tests/units/test_cli.py create mode 100644 tests/units/test_config/monkeyble.yml diff --git a/docs/build.md b/docs/build.md new file mode 100644 index 0000000..f5be571 --- /dev/null +++ b/docs/build.md @@ -0,0 +1,48 @@ +# Build + +## Ansible collection + +Build the collection +``` +ansible-galaxy collection build +``` + +Push to Galaxy: +``` +ansible-galaxy collection publish +``` + +E.g: +``` +ansible-galaxy collection publish hpe-monkeyble-1.0.3.tar.gz +``` + +## Python package + +The build package need to be present +``` +pip3 install build +``` + +Build the CLI: +``` +python -m build +``` + +This command creates a file in `dist/` + + +Publish to test env Pypi: +``` +python3 -m twine upload --repository testpypi -u sispheor -p $PYPI_PASSWORD dist/* +``` + +Publish to prod env Pypi: +``` +python3 -m twine upload --repository pypi dist/* +``` + +Test installing with pipx +``` +pipx inject ansible --index-url https://test.pypi.org/simple/ --include-apps monkeyble +``` diff --git a/monkeyble/cli/const.py b/monkeyble/cli/const.py index e4b0224..f041e8f 100644 --- a/monkeyble/cli/const.py +++ b/monkeyble/cli/const.py @@ -1,3 +1,5 @@ MONKEYBLE = "Monkeyble" TEST_PASSED = "PASSED" TEST_FAILED = "FAILED" +MONKEYBLE_DEFAULT_CONFIG_PATH = "monkeyble.yml" +MONKEYBLE_DEFAULT_ANSIBLE_CMD = "ansible-playbook" diff --git a/monkeyble/cli/exceptions.py b/monkeyble/cli/exceptions.py new file mode 100644 index 0000000..47a270d --- /dev/null +++ b/monkeyble/cli/exceptions.py @@ -0,0 +1,15 @@ +# Copyright 2022 Hewlett Packard Enterprise Development LP +import sys + +from ansible.utils.display import Display +from ansible import constants as C +from monkeyble.cli.utils import Utils + +global_display = Display() + + +class MonkeybleCLIException(Exception): + def __init__(self, message, exit_code=1): + super().__init__(message) + Utils.print_danger(message) + sys.exit(exit_code) diff --git a/monkeyble/cli/monkeyble_cli.py b/monkeyble/cli/monkeyble_cli.py index 7688ed9..cf10339 100644 --- a/monkeyble/cli/monkeyble_cli.py +++ b/monkeyble/cli/monkeyble_cli.py @@ -7,12 +7,16 @@ import yaml from tabulate import tabulate -from monkeyble.cli.const import * +from monkeyble.cli.const import MONKEYBLE_DEFAULT_CONFIG_PATH, TEST_PASSED, TEST_FAILED, MONKEYBLE, \ + MONKEYBLE_DEFAULT_ANSIBLE_CMD +from monkeyble.cli.exceptions import MonkeybleCLIException from monkeyble.cli.models import MonkeybleResult, ScenarioResult from monkeyble.cli.utils import Utils logger = logging.getLogger(MONKEYBLE) +logging.basicConfig(level=logging.DEBUG, format='%(asctime)s %(message)s') + # actions available ACTION_LIST = ["test"] @@ -38,7 +42,7 @@ def run_ansible(ansible_cmd, playbook, inventory, extra_vars, scenario): stderr=subprocess.STDOUT) for line in iter(pipes.stdout.readline, b''): print(f"{line.rstrip().decode('utf-8')}") - std_out, std_err = pipes.communicate() + pipes.wait() if pipes.returncode == 0: return TEST_PASSED else: @@ -46,12 +50,11 @@ def run_ansible(ansible_cmd, playbook, inventory, extra_vars, scenario): def run_monkeyble_test(monkeyble_config): - ansible_cmd = "ansible-playbook" + ansible_cmd = MONKEYBLE_DEFAULT_ANSIBLE_CMD if "ansible_cmd" in monkeyble_config: ansible_cmd = monkeyble_config["ansible_cmd"] if "monkeyble_tests" not in monkeyble_config: - Utils.print_danger("No 'monkeyble_tests' variable defined") - sys.exit(1) + raise MonkeybleCLIException(message="No 'monkeyble_tests' variable defined") list_result = list() for test_config in monkeyble_config["monkeyble_tests"]: if "ansible_cmd" in test_config: @@ -60,13 +63,12 @@ def run_monkeyble_test(monkeyble_config): playbook = test_config.get("playbook", None) new_result = MonkeybleResult(playbook) if playbook is None: - Utils.print_danger("Missing 'playbook' key in a test") - sys.exit(1) + raise MonkeybleCLIException(message="Missing 'playbook' key in a test") inventory = test_config.get("inventory", None) extra_vars = test_config.get("extra_vars", None) scenarios = test_config.get("scenarios", None) if scenarios is None: - Utils.print_danger("No scenario selected") + raise MonkeybleCLIException(message=f"No scenarios for playbook {playbook}") # print the current path Utils.print_info(f"Monkeyble - current path: {pathlib.Path().resolve()}") list_scenario_result = list() @@ -124,7 +126,7 @@ def load_monkeyble_config(arg_config_path): - cli args 'config' """ # set a default config - config_path = "monkeyble.yml" + config_path = MONKEYBLE_DEFAULT_CONFIG_PATH # load from env if exist env_config = os.getenv("MONKEYBLE_CONFIG", default=None) if env_config is not None: @@ -132,6 +134,7 @@ def load_monkeyble_config(arg_config_path): # load from cli args if arg_config_path is not None: config_path = arg_config_path + logger.debug(f"Try to open file {config_path}") with open(config_path, "r") as stream: try: monkeyble_config = yaml.full_load(stream) diff --git a/setup.py b/setup.py index 390311c..1a0e5a7 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,18 @@ +from os import path + from setuptools import setup, find_packages +# Get the long description from the README file +basedir = path.abspath(path.dirname(__file__)) +with open(path.join(basedir, 'README.md'), encoding='utf-8') as f: + long_description = f.read() + setup( name='monkeyble', description='End-to-end testing framework for Ansible', - version='1.1.0b', + version='1.1.0.dev0', + long_description=long_description, + long_description_content_type='text/markdown', packages=find_packages(exclude=['contrib', 'docs', 'tests']), url='https://hewlettpackard.github.io/monkeyble/', license='GNU General Public License v3 (GPLv3)', diff --git a/tests/mocks.yml b/tests/mocks.yml index 5517448..2a06dce 100644 --- a/tests/mocks.yml +++ b/tests/mocks.yml @@ -4,7 +4,7 @@ test_input_config_1: - assert_equal: arg_name: msg - expected: "Hello Monkeyble" + expected: "Goodbye Monkeyble" # mocks replace_debug_mock: diff --git a/tests/units/test_cli.py b/tests/units/test_cli.py new file mode 100644 index 0000000..353ec82 --- /dev/null +++ b/tests/units/test_cli.py @@ -0,0 +1,132 @@ +import io +import os +import unittest +from unittest import mock +from unittest.mock import patch, mock_open, call + +from monkeyble.cli.const import TEST_PASSED, TEST_FAILED, MONKEYBLE_DEFAULT_ANSIBLE_CMD +from monkeyble.cli.exceptions import MonkeybleCLIException + +from monkeyble.cli.models import MonkeybleResult, ScenarioResult + +from monkeyble.cli.monkeyble_cli import load_monkeyble_config, do_exit, run_monkeyble_test, run_ansible + + +class TestMonkeybleModule(unittest.TestCase): + + def test_load_monkeyble_config_default_config(self): + with patch("builtins.open", mock_open(read_data="data")) as mock_open_file: + load_monkeyble_config(None) + mock_open_file.assert_called_with("monkeyble.yml", 'r') + + @mock.patch.dict(os.environ, {"MONKEYBLE_CONFIG": "monkeyble_from_env.yml"}) + def test_load_monkeyble_config_from_env(self): + with patch("builtins.open", mock_open(read_data="data")) as mock_open_file: + load_monkeyble_config(None) + mock_open_file.assert_called_with("monkeyble_from_env.yml", 'r') + + def test_load_monkeyble_config_from_args(self): + with patch("builtins.open", mock_open(read_data="data")) as mock_open_file: + load_monkeyble_config("/path/to/monkeyble.yml") + mock_open_file.assert_called_with("/path/to/monkeyble.yml", 'r') + + @patch("monkeyble.cli.monkeyble_cli.MONKEYBLE_DEFAULT_CONFIG_PATH", "test_config/monkeyble.yml") + def test_load_monkeyble_config(self): + data = load_monkeyble_config(None) + expected = {'ansible_cmd': 'ansible-playbook -v', + 'monkeyble_tests': [{'playbook': 'test_playbook.yml', + 'inventory': 'inventory', + 'extra_vars': ['mocks.yml', 'monkeyble_scenarios.yml'], + 'scenarios': ['validate_test_1', 'validate_test_2']}]} + + self.assertDictEqual(data, expected) + + @patch('sys.exit') + def test_do_exit_all_test_passed(self, mock_exit): + scenario_result1 = ScenarioResult(scenario="scenario1", result=TEST_PASSED) + scenario_result2 = ScenarioResult(scenario="scenario2", result=TEST_PASSED) + monkeyble_result = MonkeybleResult(playbook="playbook", scenario_results=[scenario_result1, scenario_result2]) + + do_exit([monkeyble_result]) + mock_exit.assert_called_with(0) + + @patch('sys.exit') + def test_do_exit_all_test_failed(self, mock_exit): + scenario_result1 = ScenarioResult(scenario="scenario1", result=TEST_PASSED) + scenario_result2 = ScenarioResult(scenario="scenario2", result=TEST_FAILED) + monkeyble_result = MonkeybleResult(playbook="playbook", scenario_results=[scenario_result1, scenario_result2]) + + do_exit([monkeyble_result]) + mock_exit.assert_called_with(1) + + @patch('sys.exit') + def test_run_monkeyble_test_no_tests_defined(self, mock_exit): + monkeyble_config = dict() + with self.assertRaises(MonkeybleCLIException): + run_monkeyble_test(monkeyble_config) + mock_exit.assert_called_with(1) + + @patch('sys.exit') + def test_run_monkeyble_test_no_playbook_defined(self, mock_exit): + monkeyble_config = { + "monkeyble_tests": [ + {"inventory": "test"} + ] + } + with self.assertRaises(MonkeybleCLIException): + run_monkeyble_test(monkeyble_config) + mock_exit.assert_called_with(1) + + @patch('sys.exit') + def test_run_monkeyble_test_no_scenario_defined(self, mock_exit): + monkeyble_config = { + "monkeyble_tests": [ + {"playbook": "playbook.yml"} + ] + } + with self.assertRaises(MonkeybleCLIException): + run_monkeyble_test(monkeyble_config) + mock_exit.assert_called_with(1) + + def test_run_monkeyble_test_run_ansible_called(self): + monkeyble_config = { + "monkeyble_tests": [ + { + "playbook": "playbook.yml", + "inventory": "my_inventory", + "extra_vars": ["extra_vars1.yml", "extra_vars2.yml"], + "scenarios": ["scenario1", "scenario2"] + } + ] + } + + with mock.patch("monkeyble.cli.monkeyble_cli.run_ansible") as mock_run_ansible: + run_monkeyble_test(monkeyble_config) + self.assertEqual(mock_run_ansible.call_count, 2) + call_1 = call(MONKEYBLE_DEFAULT_ANSIBLE_CMD, + "playbook.yml", + "my_inventory", + ["extra_vars1.yml", "extra_vars2.yml"], + "scenario1") + call_2 = call(MONKEYBLE_DEFAULT_ANSIBLE_CMD, + "playbook.yml", + "my_inventory", + ["extra_vars1.yml", "extra_vars2.yml"], + "scenario2") + mock_run_ansible.assert_has_calls([call_1, call_2]) + + @patch("subprocess.Popen") + def test_run_ansible(self, mock_subproc_popen): + mock_subproc_popen.return_value.stdout = io.BytesIO(b"playbook output") + + run_ansible(MONKEYBLE_DEFAULT_ANSIBLE_CMD, + "playbook.yml", + "my_inventory", + ["extra_vars1.yml", "extra_vars2.yml"], + "scenario1") + self.assertTrue(mock_subproc_popen.called) + + expected_call = call(['ansible-playbook', 'playbook.yml', '-i', 'my_inventory', + '-e', '@extra_vars1.yml', '-e', '@extra_vars2.yml', + '-e', 'monkeyble_scenario=scenario1'], stdout=-1, stderr=-2) + mock_subproc_popen.assert_has_calls([expected_call]) diff --git a/tests/units/test_config/monkeyble.yml b/tests/units/test_config/monkeyble.yml new file mode 100644 index 0000000..26b0b1c --- /dev/null +++ b/tests/units/test_config/monkeyble.yml @@ -0,0 +1,10 @@ +ansible_cmd: "ansible-playbook -v" +monkeyble_tests: + - playbook: "test_playbook.yml" + inventory: "inventory" + extra_vars: + - "mocks.yml" + - "monkeyble_scenarios.yml" + scenarios: + - validate_test_1 + - validate_test_2