From 372e01708830d58923b7c6b1ad208610eb7f384f Mon Sep 17 00:00:00 2001 From: WiredNerd Date: Wed, 3 Jan 2024 20:06:25 -0600 Subject: [PATCH 1/4] :poodle: New CLI options: --html and --json --- .vscode/settings.json | 1 + pyproject.toml | 3 +- requirements.txt | 1 + src/poodle/cli.py | 6 +- src/poodle/config.py | 48 ++-- tests/test_cli.py | 40 ++++ tests/test_config.py | 536 ++++++++++++++++++++++++++++-------------- 7 files changed, 444 insertions(+), 191 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index c4364d0..b8f2881 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -31,6 +31,7 @@ "fromlist", "getaffinity", "keepends", + "mergedeep", "Mult", "PYTHONDONTWRITEBYTECODE", "redef", diff --git a/pyproject.toml b/pyproject.toml index 92bdcde..ed8e8cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta" [project] name = "poodle" description = "Mutation Testing Tool" -version = "1.2.1" +version = "1.2.2" license = { file = "LICENSE" } keywords = [ "test", @@ -33,6 +33,7 @@ dependencies = [ "wcmatch>=8.5", "tomli>=2; python_version<'3.11'", "jinja2>=3.0.3", + "mergedeep>=1.0", ] [project.urls] diff --git a/requirements.txt b/requirements.txt index 9933e2f..9ce4773 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ tomli>=2 wcmatch>=8.5 jinja2>=3.0.3 pysass +mergedeep pytest pytest-sort>=1.3.0 diff --git a/src/poodle/cli.py b/src/poodle/cli.py index 01d9048..a5e8499 100644 --- a/src/poodle/cli.py +++ b/src/poodle/cli.py @@ -25,6 +25,8 @@ @click.option("--exclude", help="Add a glob exclude file filter. Multiple allowed.", multiple=True) @click.option("--only", help="Glob pattern for files to mutate. Multiple allowed.", multiple=True) @click.option("--report", help="Enable reporter by name. Multiple allowed.", multiple=True) +@click.option("--html", help="Folder name to store HTML report in.", type=click.Path(path_type=Path)) +@click.option("--json", help="File to create with JSON report.", type=click.Path(path_type=Path)) def main( sources: tuple[Path], config_file: Path | None, @@ -34,10 +36,12 @@ def main( exclude: tuple[str], only: tuple[str], report: tuple[str], + html: Path | None, + json: Path | None, ) -> None: """Poodle Mutation Test Tool.""" try: - config = build_config(sources, config_file, quiet, verbose, workers, exclude, only, report) + config = build_config(sources, config_file, quiet, verbose, workers, exclude, only, report, html, json) except PoodleInputError as err: click.echo(err.args) sys.exit(4) diff --git a/src/poodle/config.py b/src/poodle/config.py index bb5160d..a0d7d82 100644 --- a/src/poodle/config.py +++ b/src/poodle/config.py @@ -8,6 +8,7 @@ from pathlib import Path from typing import Any +from mergedeep import merge from wcmatch import glob from . import PoodleInputError, poodle_config, tomllib @@ -24,15 +25,11 @@ default_file_copy_filters = ["__pycache__/**"] default_work_folder = Path(".poodle-temp") -default_mutator_opts: dict[str, Any] = {} - default_min_timeout = 10 default_timeout_multiplier = 10 default_runner = "command_line" -default_runner_opts: dict[str, Any] = {} default_reporters = ["summary", "not_found"] -default_reporter_opts: dict[str, Any] = {} def default_max_workers() -> int: @@ -54,6 +51,8 @@ def build_config( # noqa: PLR0913 cmd_excludes: tuple[str], cmd_only_files: tuple[str], cmd_report: tuple[str], + cmd_html: Path | None, + cmd_json: Path | None, ) -> PoodleConfig: """Build PoodleConfig object.""" config_file_path = get_config_file_path(cmd_config_file) @@ -74,8 +73,11 @@ def build_config( # noqa: PLR0913 # file_filters += get_str_list_from_config("exclude", config_file_data, default=[]) # noqa: ERA001 file_filters += cmd_excludes - reporters = get_str_list_from_config("reporters", config_file_data, default=default_reporters) - reporters += [reporter for reporter in cmd_report if reporter not in reporters] + cmd_reporter_opts = {} + if cmd_html: + merge(cmd_reporter_opts, {"html": {"report_folder": cmd_html}}) + if cmd_json: + merge(cmd_reporter_opts, {"json_report_file": cmd_json}) return PoodleConfig( project_name=get_str_from_config("project_name", config_file_data, default=project_name), @@ -107,18 +109,34 @@ def build_config( # noqa: PLR0913 command_line=get_cmd_line_echo_enabled(cmd_quiet), ), echo_no_color=get_bool_from_config("echo_no_color", config_file_data), - mutator_opts=get_dict_from_config("mutator_opts", config_file_data, default=default_mutator_opts), + mutator_opts=get_dict_from_config("mutator_opts", config_file_data), skip_mutators=get_str_list_from_config("skip_mutators", config_file_data, default=[]), add_mutators=get_any_list_from_config("add_mutators", config_file_data), min_timeout=get_int_from_config("min_timeout", config_file_data) or default_min_timeout, timeout_multiplier=get_int_from_config("timeout_multiplier", config_file_data) or default_timeout_multiplier, runner=get_str_from_config("runner", config_file_data, default=default_runner), - runner_opts=get_dict_from_config("runner_opts", config_file_data, default=default_runner_opts), - reporters=reporters, - reporter_opts=get_dict_from_config("reporter_opts", config_file_data, default=default_reporter_opts), + runner_opts=get_dict_from_config("runner_opts", config_file_data), + reporters=get_reporters(config_file_data, cmd_report, cmd_html, cmd_json), + reporter_opts=get_dict_from_config("reporter_opts", config_file_data, command_line=cmd_reporter_opts), ) +def get_reporters( + config_file_data: dict, + cmd_report: tuple[str], + cmd_html: Path | None, + cmd_json: Path | None, +) -> list[str]: + """Retrieve list of reporters to use.""" + reporters = get_str_list_from_config("reporters", config_file_data, default=default_reporters) + reporters += [reporter for reporter in cmd_report if reporter not in reporters] + if cmd_html: + reporters.append("html") + if cmd_json: + reporters.append("json") + return reporters + + def get_cmd_line_log_level(cmd_quiet: int, cmd_verbose: int) -> int | None: """Map verbosity input to logging level.""" if cmd_quiet >= 3: @@ -493,19 +511,19 @@ def get_dict_from_config( if option_name in config_data: try: - option_value.update(config_data[option_name]) - except ValueError: + merge(option_value, config_data[option_name]) + except TypeError: msg = f"{option_name} in config file must be a valid dict" raise PoodleInputError(msg) from None if hasattr(poodle_config, option_name): try: - option_value.update(getattr(poodle_config, option_name)) - except ValueError: + merge(option_value, getattr(poodle_config, option_name)) + except TypeError: msg = f"poodle_config.{option_name} must be a valid dict" raise PoodleInputError(msg) from None if command_line: - option_value.update(command_line) + merge(option_value, command_line) return option_value diff --git a/tests/test_cli.py b/tests/test_cli.py index 54ee561..94b8866 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -112,6 +112,30 @@ def test_cli_help_report(self, runner: CliRunner): is not None ) + def test_cli_help_html(self, runner: CliRunner): + result = runner.invoke(cli.main, ["--help"]) + assert result.exit_code == 0 + assert ( + re.match( + r".*--html PATH\s+Folder name to store HTML report in\..*", + result.output, + flags=re.DOTALL, + ) + is not None + ) + + def test_cli_help_json(self, runner: CliRunner): + result = runner.invoke(cli.main, ["--help"]) + assert result.exit_code == 0 + assert ( + re.match( + r".*--json PATH\s+File to create with JSON report\..*", + result.output, + flags=re.DOTALL, + ) + is not None + ) + def assert_build_config_called_with( self, build_config: mock.MagicMock, @@ -123,6 +147,8 @@ def assert_build_config_called_with( exclude: tuple[str] = (), # type: ignore [assignment] only: tuple[str] = (), # type: ignore [assignment] report: tuple[str] = (), # type: ignore [assignment] + html: Path | None = None, + json: Path | None = None, ): build_config.assert_called_with( sources, @@ -133,6 +159,8 @@ def assert_build_config_called_with( exclude, only, report, + html, + json, ) def test_cli(self, main_process: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner): @@ -223,6 +251,18 @@ def test_main_report(self, main_process: mock.MagicMock, build_config: mock.Magi self.assert_build_config_called_with(build_config, report=("json",)) main_process.assert_called_with(build_config.return_value) + def test_main_html(self, main_process: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner): + result = runner.invoke(cli.main, ["--html", "html_report"]) + assert result.exit_code == 0 + self.assert_build_config_called_with(build_config, html=Path("html_report")) + main_process.assert_called_with(build_config.return_value) + + def test_main_json(self, main_process: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner): + result = runner.invoke(cli.main, ["--json", "summary.json"]) + assert result.exit_code == 0 + self.assert_build_config_called_with(build_config, json=Path("summary.json")) + main_process.assert_called_with(build_config.return_value) + def test_main_input_error( self, main_process: mock.MagicMock, diff --git a/tests/test_config.py b/tests/test_config.py index eb65e60..959e95e 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -21,9 +21,9 @@ def _test_wrapper(): @pytest.fixture() -def logging_mock(): - with mock.patch("poodle.config.logging") as logging_mock: - yield logging_mock +def mock_logging(): + with mock.patch("poodle.config.logging") as mock_logging: + yield mock_logging @pytest.fixture() @@ -47,15 +47,11 @@ def test_defaults(): assert config.default_file_copy_filters == ["__pycache__/**"] assert config.default_work_folder == Path(".poodle-temp") - assert config.default_mutator_opts == {} - assert config.default_min_timeout == 10 assert config.default_timeout_multiplier == 10 assert config.default_runner == "command_line" - assert config.default_runner_opts == {} assert config.default_reporters == ["summary", "not_found"] - assert config.default_reporter_opts == {} class TestMaxWorkers: @@ -90,146 +86,265 @@ def test_default_max_workers_neither(self, os_mock): class TestBuildConfig: - @mock.patch("poodle.config.os") - @mock.patch("poodle.config.get_config_file_path") - @mock.patch("poodle.config.get_config_file_data") - @mock.patch("poodle.config.get_project_info") - @mock.patch("poodle.config.get_cmd_line_log_level") - @mock.patch("poodle.config.get_source_folders") - @mock.patch("poodle.config.get_str_from_config") - @mock.patch("poodle.config.get_str_list_from_config") - @mock.patch("poodle.config.get_path_from_config") - @mock.patch("poodle.config.get_dict_from_config") - @mock.patch("poodle.config.get_bool_from_config") - @mock.patch("poodle.config.get_int_from_config") - @mock.patch("poodle.config.get_any_from_config") - @mock.patch("poodle.config.get_any_list_from_config") - @mock.patch("poodle.config.get_cmd_line_echo_enabled") - def test_build_config( + @pytest.fixture() + def mock_os(self): + with mock.patch("poodle.config.os") as mock_os: + yield mock_os + + @pytest.fixture() + def get_reporters(self): + with mock.patch("poodle.config.get_reporters") as get_reporters: + yield get_reporters + + @pytest.fixture() + def get_config_file_path(self): + with mock.patch("poodle.config.get_config_file_path") as get_config_file_path: + yield get_config_file_path + + @pytest.fixture() + def get_config_file_data(self): + with mock.patch("poodle.config.get_config_file_data") as get_config_file_data: + yield get_config_file_data + + @pytest.fixture() + def get_project_info(self): + with mock.patch("poodle.config.get_project_info") as get_project_info: + get_project_info.return_value = ("example", "1,2,3") + yield get_project_info + + @pytest.fixture() + def get_cmd_line_log_level(self): + with mock.patch("poodle.config.get_cmd_line_log_level") as get_cmd_line_log_level: + yield get_cmd_line_log_level + + @pytest.fixture() + def get_source_folders(self): + with mock.patch("poodle.config.get_source_folders") as get_source_folders: + yield get_source_folders + + @pytest.fixture() + def get_str_from_config(self): + with mock.patch("poodle.config.get_str_from_config") as get_str_from_config: + yield get_str_from_config + + @pytest.fixture() + def get_str_list_from_config(self): + with mock.patch("poodle.config.get_str_list_from_config") as get_str_list_from_config: + yield get_str_list_from_config + + @pytest.fixture() + def get_path_from_config(self): + with mock.patch("poodle.config.get_path_from_config") as get_path_from_config: + yield get_path_from_config + + @pytest.fixture() + def get_dict_from_config(self): + with mock.patch("poodle.config.get_dict_from_config") as get_dict_from_config: + yield get_dict_from_config + + @pytest.fixture() + def get_bool_from_config(self): + with mock.patch("poodle.config.get_bool_from_config") as get_bool_from_config: + yield get_bool_from_config + + @pytest.fixture() + def get_int_from_config(self): + with mock.patch("poodle.config.get_int_from_config") as get_int_from_config: + yield get_int_from_config + + @pytest.fixture() + def get_any_from_config(self): + with mock.patch("poodle.config.get_any_from_config") as get_any_from_config: + yield get_any_from_config + + @pytest.fixture() + def get_any_list_from_config(self): + with mock.patch("poodle.config.get_any_list_from_config") as get_any_list_from_config: + yield get_any_list_from_config + + @pytest.fixture() + def get_cmd_line_echo_enabled(self): + with mock.patch("poodle.config.get_cmd_line_echo_enabled") as get_cmd_line_echo_enabled: + yield get_cmd_line_echo_enabled + + @pytest.fixture() + def default_max_workers(self): + with mock.patch("poodle.config.default_max_workers") as default_max_workers: + yield default_max_workers + + @pytest.fixture() + def setup_build_config_mocks( self, - get_cmd_line_echo_enabled, - get_any_list_from_config, - get_any_from_config, - get_int_from_config, - get_bool_from_config, - get_dict_from_config, - get_path_from_config, - get_str_list_from_config, - get_str_from_config, - get_source_folders, - get_cmd_line_log_level, - get_project_info, - get_config_file_data, - get_config_file_path, - mock_os, - logging_mock, + get_cmd_line_echo_enabled: mock.MagicMock, + get_any_list_from_config: mock.MagicMock, + get_any_from_config: mock.MagicMock, + get_int_from_config: mock.MagicMock, + get_bool_from_config: mock.MagicMock, + get_dict_from_config: mock.MagicMock, + get_path_from_config: mock.MagicMock, + get_str_list_from_config: mock.MagicMock, + get_str_from_config: mock.MagicMock, + get_source_folders: mock.MagicMock, + get_cmd_line_log_level: mock.MagicMock, + get_project_info: mock.MagicMock, + get_config_file_data: mock.MagicMock, + get_config_file_path: mock.MagicMock, + get_reporters: mock.MagicMock, + mock_os: mock.MagicMock, + mock_logging: mock.MagicMock, ): - get_dict_from_config.side_effect = [ - {"mutator": "value"}, - {"runner": "value"}, - {"reporter": "value"}, - ] - - get_project_info.return_value = ("example", "1,2,3") - - cmd_sources = (Path("src"),) - cmd_config_file = Path("config.toml") - - config_file_path = get_config_file_path.return_value - config_file_data = get_config_file_data.return_value - - mock_os.sched_getaffinity.return_value = [1, 2, 3, 4, 5, 6] - - assert config.build_config( + get_cmd_line_echo_enabled.reset_mock() + get_any_list_from_config.reset_mock() + get_any_from_config.reset_mock() + get_int_from_config.reset_mock() + get_bool_from_config.reset_mock() + get_dict_from_config.reset_mock() + get_path_from_config.reset_mock() + get_str_list_from_config.reset_mock() + get_str_from_config.reset_mock() + get_source_folders.reset_mock() + get_cmd_line_log_level.reset_mock() + get_project_info.reset_mock() + get_config_file_data.reset_mock() + get_config_file_path.reset_mock() + get_reporters.reset_mock() + mock_os.reset_mock() + mock_logging.reset_mock() + + def build_config_with( + self, + cmd_sources: tuple[Path] = (Path("src"),), + cmd_config_file: Path | None = None, + cmd_quiet: int = 0, + cmd_verbose: int = 0, + cmd_max_workers: int | None = None, + cmd_excludes: tuple[str] = (), + cmd_only_files: tuple[str] = (), + cmd_report: tuple[str] = (), + cmd_html: Path | None = None, + cmd_json: Path | None = None, + ): + return config.build_config( cmd_sources, cmd_config_file, - cmd_quiet=1, - cmd_verbose=2, - cmd_max_workers=3, - cmd_excludes=("notcov.py",), - cmd_only_files=("example.py",), - cmd_report=("myreporter",), - ) == config.PoodleConfig( - project_name=get_str_from_config.return_value, - project_version=get_str_from_config.return_value, - config_file=config_file_path, - source_folders=get_source_folders.return_value, - only_files=get_str_list_from_config.return_value, - file_flags=get_int_from_config.return_value, - file_filters=get_str_list_from_config.return_value.__iadd__.return_value, - file_copy_flags=get_int_from_config.return_value, - file_copy_filters=get_str_list_from_config.return_value, - work_folder=get_path_from_config.return_value, - max_workers=get_int_from_config.return_value, - log_format=get_str_from_config.return_value, - log_level=get_any_from_config.return_value, - echo_enabled=get_bool_from_config.return_value, - echo_no_color=get_bool_from_config.return_value, - mutator_opts={"mutator": "value"}, - skip_mutators=get_str_list_from_config.return_value, - add_mutators=get_any_list_from_config.return_value, - min_timeout=get_int_from_config.return_value, - timeout_multiplier=get_int_from_config.return_value, - runner=get_str_from_config.return_value, - runner_opts={"runner": "value"}, - reporters=get_str_list_from_config.return_value.__iadd__.return_value, - reporter_opts={"reporter": "value"}, + cmd_quiet, + cmd_verbose, + cmd_max_workers, + cmd_excludes, + cmd_only_files, + cmd_report, + cmd_html, + cmd_json, ) - # return PoodleConfig - - # project_name - get_str_from_config.assert_any_call("project_name", config_file_data, default="example") - - # project_version - get_str_from_config.assert_any_call("project_version", config_file_data, default="1,2,3") - - # source_folders - get_source_folders.assert_called_once_with(cmd_sources, config_file_data) + def test_build_config_project_info(self, get_project_info: mock.MagicMock): + get_project_info.return_value = ("example", "1.2.3") + config_data = self.build_config_with() + assert config_data.project_name == "example" + assert config_data.project_version == "1.2.3" + + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_config_file(self, get_config_file_path, get_config_file_data): + config_data = self.build_config_with(cmd_config_file=Path("config.toml")) + assert config_data.config_file == get_config_file_path.return_value + get_config_file_path.assert_called_with(Path("config.toml")) + get_config_file_data.assert_called_with(get_config_file_path.return_value) + + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_source_folders(self, get_source_folders, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with(cmd_sources=(Path("source"),)) + assert config_data.source_folders == get_source_folders.return_value + get_source_folders.assert_called_with((Path("source"),), config_file_data) - # only_files + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_only_files(self, get_str_list_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with(cmd_only_files=("example.py",)) + assert config_data.only_files == get_str_list_from_config.return_value get_str_list_from_config.assert_any_call( "only_files", config_file_data, default=[], command_line=("example.py",), ) - # file_flags + + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_file_flags(self, get_int_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.file_flags == get_int_from_config.return_value get_int_from_config.assert_any_call("file_flags", config_file_data, default=config.default_file_flags) - # file_filters - get_str_list_from_config.assert_any_call("file_filters", config_file_data, default=config.default_file_filters) - get_str_list_from_config.return_value.__iadd__.assert_any_call(("notcov.py",)) - # file_copy_flags + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_file_filters(self, get_str_list_from_config): + get_str_list_from_config.return_value = ["test_*.py", "*_test.py", "poodle_config.py", "setup.py"] + config_data = self.build_config_with(cmd_excludes=("notcov.py",)) + assert config_data.file_filters == ["test_*.py", "*_test.py", "poodle_config.py", "setup.py", "notcov.py"] + + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_file_copy_flags(self, get_int_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.file_copy_flags == get_int_from_config.return_value get_int_from_config.assert_any_call("file_copy_flags", config_file_data, default=config.default_file_copy_flags) - # file_copy_filters + + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_file_copy_filters(self, get_str_list_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.file_copy_filters == get_str_list_from_config.return_value get_str_list_from_config.assert_any_call( "file_copy_filters", config_file_data, default=config.default_file_copy_filters, ) - # work_folder + + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_work_folder(self, get_path_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.work_folder == get_path_from_config.return_value get_path_from_config.assert_any_call("work_folder", config_file_data, default=config.default_work_folder) - # max_workers + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_max_workers(self, get_int_from_config, default_max_workers, get_config_file_data): + config_file_data = get_config_file_data.return_value + default_max_workers.return_value = 5 + config_data = self.build_config_with(cmd_max_workers=3) + assert config_data.max_workers == get_int_from_config.return_value get_int_from_config.assert_any_call("max_workers", config_file_data, default=5, command_line=3) - # log_format + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_logging( + self, + get_str_from_config, + get_any_from_config, + mock_logging, + get_config_file_data, + get_cmd_line_log_level, + ): + config_file_data = get_config_file_data.return_value + get_str_from_config.return_value = "example log format" + get_any_from_config.return_value = logging.CRITICAL + + config_data = self.build_config_with(cmd_quiet=1, cmd_verbose=2) + assert config_data.log_format == "example log format" + assert config_data.log_level == logging.CRITICAL get_str_from_config.assert_any_call("log_format", config_file_data, default=config.default_log_format) - # log_level get_any_from_config.assert_any_call( "log_level", config_file_data, default=config.default_log_level, command_line=get_cmd_line_log_level.return_value, ) - get_cmd_line_log_level.assert_called_with(1, 2) - logging_mock.basicConfig.assert_called_once_with( - format=get_str_from_config.return_value, - level=get_any_from_config.return_value, - ) + mock_logging.basicConfig.assert_called_once_with(format="example log format", level=logging.CRITICAL) - # echo_enabled + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_echo_enabled(self, get_bool_from_config, get_cmd_line_echo_enabled, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with(cmd_quiet=1) + assert config_data.echo_enabled == get_bool_from_config.return_value get_bool_from_config.assert_any_call( "echo_enabled", config_file_data, @@ -238,42 +353,104 @@ def test_build_config( ) get_cmd_line_echo_enabled.assert_called_once_with(1) - # echo_no_color + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_echo_no_color(self, get_bool_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.echo_no_color == get_bool_from_config.return_value get_bool_from_config.assert_any_call("echo_no_color", config_file_data) - # mutator_opts - get_dict_from_config.assert_any_call("mutator_opts", config_file_data, default=config.default_mutator_opts) + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_mutator_opts(self, get_dict_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.mutator_opts == get_dict_from_config.return_value + get_dict_from_config.assert_any_call("mutator_opts", config_file_data) - # skip_mutators + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_skip_mutators(self, get_str_list_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.skip_mutators == get_str_list_from_config.return_value get_str_list_from_config.assert_any_call("skip_mutators", config_file_data, default=[]) - # add_mutators + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_add_mutators(self, get_any_list_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.add_mutators == get_any_list_from_config.return_value get_any_list_from_config.assert_any_call("add_mutators", config_file_data) - # min_timeout + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_min_timeout(self, get_int_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.min_timeout == get_int_from_config.return_value get_int_from_config.assert_any_call("min_timeout", config_file_data) - # timeout_multiplier + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_min_timeout_default(self, get_int_from_config): + get_int_from_config.return_value = None + config_data = self.build_config_with() + assert config_data.min_timeout == config.default_min_timeout + + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_timeout_multiplier(self, get_int_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.timeout_multiplier == get_int_from_config.return_value get_int_from_config.assert_any_call("timeout_multiplier", config_file_data) - # runner + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_timeout_multiplier_default(self, get_int_from_config): + get_int_from_config.return_value = None + config_data = self.build_config_with() + assert config_data.timeout_multiplier == config.default_timeout_multiplier + + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_runner(self, get_str_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.runner == get_str_from_config.return_value get_str_from_config.assert_any_call("runner", config_file_data, default=config.default_runner) - # runner_opts - get_dict_from_config.assert_any_call("runner_opts", config_file_data, default=config.default_runner_opts) + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_runner_opts(self, get_dict_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with() + assert config_data.runner_opts == get_dict_from_config.return_value + get_dict_from_config.assert_any_call("runner_opts", config_file_data) - # reporters - get_str_list_from_config.assert_any_call("reporters", config_file_data, default=config.default_reporters) - get_str_list_from_config.return_value.__iadd__.assert_any_call(["myreporter"]) + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_reporters(self, get_reporters, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with( + cmd_report=("myreporter",), + cmd_html=Path("html"), + cmd_json=Path("json"), + ) + assert config_data.reporters == get_reporters.return_value + get_reporters.assert_called_once_with(config_file_data, ("myreporter",), Path("html"), Path("json")) - # reporter_opts - get_dict_from_config.assert_any_call("reporter_opts", config_file_data, default=config.default_reporter_opts) + @pytest.mark.usefixtures("setup_build_config_mocks") + def test_build_config_reporter_opts(self, get_dict_from_config, get_config_file_data): + config_file_data = get_config_file_data.return_value + config_data = self.build_config_with( + cmd_html=Path("html"), + cmd_json=Path("json"), + ) + assert config_data.reporter_opts == get_dict_from_config.return_value + get_dict_from_config.assert_any_call( + "reporter_opts", + config_file_data, + command_line={"json_report_file": Path("json"), "html": {"report_folder": Path("html")}}, + ) @mock.patch("poodle.config.get_config_file_data") - @mock.patch("poodle.config.get_project_info_toml") - def test_build_config_defaults(self, get_project_info_toml, get_config_file_data): + @mock.patch("poodle.config.get_project_info") + def test_build_config_defaults(self, get_project_info, get_config_file_data): get_config_file_data.return_value = {} - get_project_info_toml.return_value = (None, None) + get_project_info.return_value = (None, None) assert config.build_config( cmd_sources=(), @@ -284,6 +461,8 @@ def test_build_config_defaults(self, get_project_info_toml, get_config_file_data cmd_excludes=(), cmd_only_files=(), cmd_report=(), + cmd_html=None, + cmd_json=None, ) == config.PoodleConfig( project_name=None, project_version=None, @@ -311,47 +490,48 @@ def test_build_config_defaults(self, get_project_info_toml, get_config_file_data reporter_opts={}, ) - @mock.patch("poodle.config.get_config_file_data") - @mock.patch("poodle.config.get_project_info_toml") - def test_build_config_duplicate_reporters(self, get_project_info_toml, get_config_file_data): - get_config_file_data.return_value = {} - get_project_info_toml.return_value = (None, None) - assert config.build_config( - cmd_sources=(), - cmd_config_file=None, - cmd_quiet=0, - cmd_verbose=0, - cmd_max_workers=None, - cmd_excludes=(), - cmd_only_files=(), - cmd_report=("myreporter", "summary"), - ) == config.PoodleConfig( - project_name=None, - project_version=None, - config_file=Path("pyproject.toml"), - source_folders=[Path("src")], - only_files=[], - file_flags=config.default_file_flags, - file_filters=config.default_file_filters, - file_copy_flags=config.default_file_copy_flags, - file_copy_filters=config.default_file_copy_filters, - work_folder=Path(".poodle-temp"), - max_workers=config.default_max_workers(), - log_format=config.default_log_format, - log_level=logging.WARN, - echo_enabled=True, - echo_no_color=None, - mutator_opts={}, - skip_mutators=[], - add_mutators=[], - min_timeout=10, - timeout_multiplier=10, - runner="command_line", - runner_opts={}, - reporters=["summary", "not_found", "myreporter"], - reporter_opts={}, - ) +class TestGetReporters: + @pytest.fixture() + def get_str_list_from_config(self): + with mock.patch("poodle.config.get_str_list_from_config") as get_str_list_from_config: + yield get_str_list_from_config + + def test_get_reporters(self, get_str_list_from_config): + config_file_data = mock.MagicMock() + get_str_list_from_config.return_value = ["example1", "example2"] + assert config.get_reporters(config_file_data, ("example3",), None, None) == ["example1", "example2", "example3"] + + def test_get_reporters_html(self, get_str_list_from_config): + config_file_data = mock.MagicMock() + get_str_list_from_config.return_value = ["example1", "example2"] + assert config.get_reporters(config_file_data, ("example3",), Path("output"), None) == [ + "example1", + "example2", + "example3", + "html", + ] + + def test_get_reporters_json(self, get_str_list_from_config): + config_file_data = mock.MagicMock() + get_str_list_from_config.return_value = ["example1", "example2"] + assert config.get_reporters(config_file_data, ("example3",), None, Path("output")) == [ + "example1", + "example2", + "example3", + "json", + ] + + def test_get_reporters_all(self, get_str_list_from_config): + config_file_data = mock.MagicMock() + get_str_list_from_config.return_value = ["example1", "example2"] + assert config.get_reporters(config_file_data, ("example3",), Path("output"), Path("output")) == [ + "example1", + "example2", + "example3", + "html", + "json", + ] class TestGetCommandLineLoggingOptions: @@ -1228,11 +1408,15 @@ def test_default_only(self): def test_config_data(self): assert config.get_dict_from_config( option_name="mutator_opts", - default={"bin_op_level": "std"}, + default={ + "bin_op_level": "max", + "custom": {"output": "XYZ"}, + }, config_data={ "mutator_opts": { "bin_op_level": "min", "config_file_option": "ABCD", + "custom": {"custom_option": "EFGH"}, }, "runner_opts": { "mutator_file_option": "QWERTY", @@ -1241,6 +1425,7 @@ def test_config_data(self): ) == { "bin_op_level": "min", "config_file_option": "ABCD", + "custom": {"custom_option": "EFGH", "output": "XYZ"}, } def test_config_data_invalid(self): @@ -1256,6 +1441,7 @@ def test_poodle_config(self): mutator_opts={ "bin_op_level": "max", "poodle_config_option": "EFGH", + "custom": {"output": "XYZ"}, }, ) assert config.get_dict_from_config( @@ -1265,14 +1451,16 @@ def test_poodle_config(self): "mutator_opts": { "bin_op_level": "min", "config_file_option": "ABCD", + "custom": {"custom_option": "EFGH"}, }, }, - command_line={"cmd_option": "cmd_value"}, + command_line={"cmd_option": "cmd_value", "custom": {"custom_option": "UIO"}}, ) == { "bin_op_level": "max", "config_file_option": "ABCD", "poodle_config_option": "EFGH", "cmd_option": "cmd_value", + "custom": {"custom_option": "UIO", "output": "XYZ"}, } def test_poodle_config_invalid(self): From ea636ce8c2637e1be40d9c7949bc93fe585ec3a5 Mon Sep 17 00:00:00 2001 From: WiredNerd Date: Thu, 4 Jan 2024 02:06:43 +0000 Subject: [PATCH 2/4] :robot: Update version to 1.2.2 --- src/poodle/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/poodle/__init__.py b/src/poodle/__init__.py index 0707c7a..4c73286 100644 --- a/src/poodle/__init__.py +++ b/src/poodle/__init__.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Any -__version__ = "1.2.1" +__version__ = "1.2.2" class PoodleInputError(ValueError): From 076aabdef3697ca872c2ec8fd19c4c4d02d7c5e7 Mon Sep 17 00:00:00 2001 From: WiredNerd Date: Wed, 3 Jan 2024 20:17:03 -0600 Subject: [PATCH 3/4] :wrench: fixes from scans --- src/poodle/config.py | 4 ++-- tests/test_config.py | 54 ++++++++++++++++++++++---------------------- 2 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/poodle/config.py b/src/poodle/config.py index a0d7d82..8b2b952 100644 --- a/src/poodle/config.py +++ b/src/poodle/config.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Any -from mergedeep import merge +from mergedeep import merge # type: ignore[import-untyped] from wcmatch import glob from . import PoodleInputError, poodle_config, tomllib @@ -73,7 +73,7 @@ def build_config( # noqa: PLR0913 # file_filters += get_str_list_from_config("exclude", config_file_data, default=[]) # noqa: ERA001 file_filters += cmd_excludes - cmd_reporter_opts = {} + cmd_reporter_opts: dict[str, Any] = {} if cmd_html: merge(cmd_reporter_opts, {"html": {"report_folder": cmd_html}}) if cmd_json: diff --git a/tests/test_config.py b/tests/test_config.py index 959e95e..109f3b7 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -173,7 +173,7 @@ def default_max_workers(self): yield default_max_workers @pytest.fixture() - def setup_build_config_mocks( + def _setup_build_config_mocks( self, get_cmd_line_echo_enabled: mock.MagicMock, get_any_list_from_config: mock.MagicMock, @@ -218,9 +218,9 @@ def build_config_with( cmd_quiet: int = 0, cmd_verbose: int = 0, cmd_max_workers: int | None = None, - cmd_excludes: tuple[str] = (), - cmd_only_files: tuple[str] = (), - cmd_report: tuple[str] = (), + cmd_excludes: tuple[str] = (), # type: ignore[assignment] + cmd_only_files: tuple[str] = (), # type: ignore[assignment] + cmd_report: tuple[str] = (), # type: ignore[assignment] cmd_html: Path | None = None, cmd_json: Path | None = None, ): @@ -243,21 +243,21 @@ def test_build_config_project_info(self, get_project_info: mock.MagicMock): assert config_data.project_name == "example" assert config_data.project_version == "1.2.3" - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_config_file(self, get_config_file_path, get_config_file_data): config_data = self.build_config_with(cmd_config_file=Path("config.toml")) assert config_data.config_file == get_config_file_path.return_value get_config_file_path.assert_called_with(Path("config.toml")) get_config_file_data.assert_called_with(get_config_file_path.return_value) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_source_folders(self, get_source_folders, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with(cmd_sources=(Path("source"),)) assert config_data.source_folders == get_source_folders.return_value get_source_folders.assert_called_with((Path("source"),), config_file_data) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_only_files(self, get_str_list_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with(cmd_only_files=("example.py",)) @@ -269,27 +269,27 @@ def test_build_config_only_files(self, get_str_list_from_config, get_config_file command_line=("example.py",), ) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_file_flags(self, get_int_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.file_flags == get_int_from_config.return_value get_int_from_config.assert_any_call("file_flags", config_file_data, default=config.default_file_flags) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_file_filters(self, get_str_list_from_config): get_str_list_from_config.return_value = ["test_*.py", "*_test.py", "poodle_config.py", "setup.py"] config_data = self.build_config_with(cmd_excludes=("notcov.py",)) assert config_data.file_filters == ["test_*.py", "*_test.py", "poodle_config.py", "setup.py", "notcov.py"] - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_file_copy_flags(self, get_int_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.file_copy_flags == get_int_from_config.return_value get_int_from_config.assert_any_call("file_copy_flags", config_file_data, default=config.default_file_copy_flags) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_file_copy_filters(self, get_str_list_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() @@ -300,14 +300,14 @@ def test_build_config_file_copy_filters(self, get_str_list_from_config, get_conf default=config.default_file_copy_filters, ) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_work_folder(self, get_path_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.work_folder == get_path_from_config.return_value get_path_from_config.assert_any_call("work_folder", config_file_data, default=config.default_work_folder) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_max_workers(self, get_int_from_config, default_max_workers, get_config_file_data): config_file_data = get_config_file_data.return_value default_max_workers.return_value = 5 @@ -315,7 +315,7 @@ def test_build_config_max_workers(self, get_int_from_config, default_max_workers assert config_data.max_workers == get_int_from_config.return_value get_int_from_config.assert_any_call("max_workers", config_file_data, default=5, command_line=3) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_logging( self, get_str_from_config, @@ -340,7 +340,7 @@ def test_build_config_logging( ) mock_logging.basicConfig.assert_called_once_with(format="example log format", level=logging.CRITICAL) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_echo_enabled(self, get_bool_from_config, get_cmd_line_echo_enabled, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with(cmd_quiet=1) @@ -353,75 +353,75 @@ def test_build_config_echo_enabled(self, get_bool_from_config, get_cmd_line_echo ) get_cmd_line_echo_enabled.assert_called_once_with(1) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_echo_no_color(self, get_bool_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.echo_no_color == get_bool_from_config.return_value get_bool_from_config.assert_any_call("echo_no_color", config_file_data) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_mutator_opts(self, get_dict_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.mutator_opts == get_dict_from_config.return_value get_dict_from_config.assert_any_call("mutator_opts", config_file_data) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_skip_mutators(self, get_str_list_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.skip_mutators == get_str_list_from_config.return_value get_str_list_from_config.assert_any_call("skip_mutators", config_file_data, default=[]) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_add_mutators(self, get_any_list_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.add_mutators == get_any_list_from_config.return_value get_any_list_from_config.assert_any_call("add_mutators", config_file_data) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_min_timeout(self, get_int_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.min_timeout == get_int_from_config.return_value get_int_from_config.assert_any_call("min_timeout", config_file_data) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_min_timeout_default(self, get_int_from_config): get_int_from_config.return_value = None config_data = self.build_config_with() assert config_data.min_timeout == config.default_min_timeout - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_timeout_multiplier(self, get_int_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.timeout_multiplier == get_int_from_config.return_value get_int_from_config.assert_any_call("timeout_multiplier", config_file_data) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_timeout_multiplier_default(self, get_int_from_config): get_int_from_config.return_value = None config_data = self.build_config_with() assert config_data.timeout_multiplier == config.default_timeout_multiplier - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_runner(self, get_str_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.runner == get_str_from_config.return_value get_str_from_config.assert_any_call("runner", config_file_data, default=config.default_runner) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_runner_opts(self, get_dict_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with() assert config_data.runner_opts == get_dict_from_config.return_value get_dict_from_config.assert_any_call("runner_opts", config_file_data) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_reporters(self, get_reporters, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with( @@ -432,7 +432,7 @@ def test_build_config_reporters(self, get_reporters, get_config_file_data): assert config_data.reporters == get_reporters.return_value get_reporters.assert_called_once_with(config_file_data, ("myreporter",), Path("html"), Path("json")) - @pytest.mark.usefixtures("setup_build_config_mocks") + @pytest.mark.usefixtures("_setup_build_config_mocks") def test_build_config_reporter_opts(self, get_dict_from_config, get_config_file_data): config_file_data = get_config_file_data.return_value config_data = self.build_config_with( From a6f9433d0f21584fe77553f100c96c8b84fb34c5 Mon Sep 17 00:00:00 2001 From: WiredNerd Date: Wed, 3 Jan 2024 20:24:09 -0600 Subject: [PATCH 4/4] Update workflows --- .github/workflows/python-platform-test.yml | 4 ++-- .github/workflows/update-coverage.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-platform-test.yml b/.github/workflows/python-platform-test.yml index 19ec336..57263b9 100644 --- a/.github/workflows/python-platform-test.yml +++ b/.github/workflows/python-platform-test.yml @@ -35,9 +35,9 @@ jobs: - name: Poodle example project run : | cd example - poodle --report summary --report not_found --report json --report html + poodle --report summary --report not_found --json results.json --html html_report - name: Poodle example flat project run : | cd example2 - poodle --report summary --report not_found --report json --report html + poodle --report summary --report not_found --json results.json --html html_report \ No newline at end of file diff --git a/.github/workflows/update-coverage.yml b/.github/workflows/update-coverage.yml index 36f09cd..5d2c645 100644 --- a/.github/workflows/update-coverage.yml +++ b/.github/workflows/update-coverage.yml @@ -38,13 +38,13 @@ jobs: name: Coverage Report path: cov-html - name: Poodle - run: poodle --report json --report html + run: poodle --json mutation-testing-report.json --html html-report - name: Upload Report HTML if: ${{ always() }} uses: actions/upload-artifact@v3 with: name: Mutation testing report HTML - path: mutation_reports + path: html-report - name: save-json-report if: ${{ always() && github.ref_name == 'main' }} uses: EndBug/add-and-commit@v9