From 596e9534182a214f870c7c0e12225211b2446430 Mon Sep 17 00:00:00 2001 From: WiredNerd Date: Fri, 5 Jan 2024 14:21:10 -0600 Subject: [PATCH 1/4] :wrench: Error message for TOMLDecodeError --- README.md | 20 +++++++++++------- pyproject.toml | 2 +- src/poodle/config.py | 20 ++++++++++++------ tests/test_config.py | 20 +++++++++++++++++- tools.ipynb | 50 -------------------------------------------- 5 files changed, 46 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 6f74cd4..0aa1479 100644 --- a/README.md +++ b/README.md @@ -42,14 +42,18 @@ Usage: poodle [OPTIONS] [SOURCES]... Poodle Mutation Test Tool. Options: - -c PATH Configuration File. - -q Quiet mode: q, qq, or qqq - -v Verbose mode: v, vv, or vvv - -w INTEGER Maximum number of parallel workers. - --exclude TEXT Add a glob exclude file filter. Multiple allowed. - --only TEXT Glob pattern for files to mutate. Multiple allowed. - --report TEXT Enable reporter by name. Multiple allowed. - --help Show this message and exit. + -c PATH Configuration File. + -q Quiet mode: q, qq, or qqq + -v Verbose mode: v, vv, or vvv + -w INTEGER Maximum number of parallel workers. + --exclude TEXT Add a glob exclude file filter. Multiple allowed. + --only TEXT Glob pattern for files to mutate. Multiple allowed. + --report TEXT Enable reporter by name. Multiple allowed. + --html PATH Folder name to store HTML report in. + --json PATH File to create with JSON report. + --fail_under FLOAT Fail if mutation score is under this value. + --version Show the version and exit. + --help Show this message and exit. ``` ## Documentation: diff --git a/pyproject.toml b/pyproject.toml index 9753c36..c45f3cd 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.3.0" +version = "1.3.1" license = { file = "LICENSE" } keywords = [ "test", diff --git a/src/poodle/config.py b/src/poodle/config.py index 14902f5..66ecbb8 100644 --- a/src/poodle/config.py +++ b/src/poodle/config.py @@ -230,16 +230,24 @@ def get_project_info(config_file: Path | None) -> tuple[str, str]: def get_config_file_data_toml(config_file: Path) -> dict: """Retrieve Poodle configuration from a 'toml' Config File.""" - config_data = tomllib.load(config_file.open(mode="rb")) - config_data: dict = config_data.get("tool", config_data) # type: ignore[no-redef] - return config_data.get("poodle", {}) + try: + config_data = tomllib.load(config_file.open(mode="rb")) + config_data: dict = config_data.get("tool", config_data) # type: ignore[no-redef] + return config_data.get("poodle", {}) + except tomllib.TOMLDecodeError as err: + msgs = [f"Error decoding toml file: {config_file}"] + msgs.extend(err.args) + raise PoodleInputError(*msgs) from None def get_project_info_toml(config_file: Path) -> tuple[str, str]: """Retrieve Project Name and Version from a 'toml' Config File.""" - config_data = tomllib.load(config_file.open(mode="rb")) - config_data: dict = config_data.get("project", config_data) # type: ignore[no-redef] - return config_data.get("name", ""), config_data.get("version", "") + try: + config_data = tomllib.load(config_file.open(mode="rb")) + config_data: dict = config_data.get("project", config_data) # type: ignore[no-redef] + return config_data.get("name", ""), config_data.get("version", "") + except tomllib.TOMLDecodeError: + return "", "" def get_source_folders(command_line_sources: tuple[Path], config_data: dict) -> list[Path]: diff --git a/tests/test_config.py b/tests/test_config.py index 0e98181..de5eff1 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -9,7 +9,7 @@ import pytest from wcmatch import glob -from poodle import PoodleInputError, config +from poodle import PoodleInputError, config, tomllib @pytest.fixture(autouse=True) @@ -720,6 +720,18 @@ def test_get_config_file_data_toml_no_data(self): assert config.get_config_file_data_toml(file_path) == {} file_path.open.assert_called_with(mode="rb") + @mock.patch("poodle.config.tomllib.load") + def test_get_config_file_data_toml_decode_error(self, tomllib_load): + tomllib_load.side_effect = tomllib.TOMLDecodeError("Could not Decode", "Bad data") + config_file = mock.MagicMock() + with pytest.raises(PoodleInputError) as err: + config.get_config_file_data_toml(config_file) + assert err.value.args == ( + f"Error decoding toml file: {config_file}", + "Could not Decode", + "Bad data", + ) + class TestGetProjectInfoToml: def test_get_project_info_toml_poodle(self): @@ -734,6 +746,12 @@ def test_get_project_info_toml_no_data(self): assert config.get_project_info_toml(file_path) == ("", "") file_path.open.assert_called_with(mode="rb") + @mock.patch("poodle.config.tomllib.load") + def test_get_project_info_data_decode_error(self, tomllib_load): + tomllib_load.side_effect = tomllib.TOMLDecodeError("Could not Decode", "Bad data") + config_file = mock.MagicMock() + assert config.get_project_info_toml(config_file) == ("", "") + class TestGetSourceFolders: @mock.patch("poodle.config.get_path_list_from_config") diff --git a/tools.ipynb b/tools.ipynb index 71cf68e..74d41b5 100644 --- a/tools.ipynb +++ b/tools.ipynb @@ -144,56 +144,6 @@ "# twine upload --repository pypi dist/*" ] }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "!pysass src/poodle/templates/html-report.scss src/poodle/templates/html-report.css\n", - "# %autoreload\n", - "\n", - "import click\n", - "import importlib\n", - "from poodle.reporters import html\n", - "from tests.data_types.test_data import PoodleConfigStub\n", - "from tests.reporters import test_html\n", - "\n", - "importlib.reload(html)\n", - "\n", - "config = PoodleConfigStub(\n", - " reporter_opts={\n", - " \"html\": {\n", - " \"project_name\": \"Example\",\n", - " \"project_version\": \"0.0.1\",\n", - " \"include_found_trials\": False,\n", - " }\n", - " }\n", - ")\n", - "\n", - "html.html_reporter(\n", - " config,\n", - " click.echo,\n", - " test_html.build_testing_results(\n", - " test_html.build_mutant_trials_augassign(), \n", - " test_html.build_mutant_trials_compare(),\n", - " ),\n", - ")\n", - "\n", - "!pysass src/poodle/templates/html-report.scss mutation_reports/html-report.css\n", - "# !start mutation_reports/index.html\n", - "# !start mutation_reports/detail.html" - ] - }, { "cell_type": "code", "execution_count": null, From 2e0aeb4713eedaf311784623625a5db897775a74 Mon Sep 17 00:00:00 2001 From: WiredNerd Date: Fri, 5 Jan 2024 18:52:07 -0600 Subject: [PATCH 2/4] :wrench: Add colored output for errors --- src/poodle/cli.py | 16 +- src/poodle/core.py | 25 ++- tests/test_cli.py | 56 +++--- tests/test_core.py | 437 ++++++++++++++++++++++++++++----------------- 4 files changed, 327 insertions(+), 207 deletions(-) diff --git a/src/poodle/cli.py b/src/poodle/cli.py index c113ef4..2596a0c 100644 --- a/src/poodle/cli.py +++ b/src/poodle/cli.py @@ -56,33 +56,33 @@ def main( # noqa: C901, PLR0912 ) except PoodleInputError as err: for arg in err.args: - click.echo(arg) + click.secho(arg, fg="red") sys.exit(4) try: core.main_process(config) except PoodleTestingFailedError as err: for arg in err.args: - click.echo(arg) + click.secho(arg, fg="yellow") sys.exit(1) except KeyboardInterrupt: - click.echo("Aborted due to Keyboard Interrupt!") + click.secho("Aborted due to Keyboard Interrupt!", fg="yellow") sys.exit(2) except PoodleTrialRunError as err: for arg in err.args: - click.echo(arg) + click.secho(arg, fg="red") sys.exit(3) except PoodleInputError as err: for arg in err.args: - click.echo(arg) + click.secho(arg, fg="red") sys.exit(4) except PoodleNoMutantsFoundError as err: for arg in err.args: - click.echo(arg) + click.secho(arg, fg="yellow") sys.exit(5) except: # noqa: E722 - click.echo("Aborted due to Internal Error!") - click.echo(traceback.format_exc()) + click.secho("Aborted due to Internal Error!", fg="red") + click.secho(traceback.format_exc(), fg="red") sys.exit(3) sys.exit(0) diff --git a/src/poodle/core.py b/src/poodle/core.py index 01fb681..bd615c9 100644 --- a/src/poodle/core.py +++ b/src/poodle/core.py @@ -4,6 +4,7 @@ import logging import shutil +from typing import TYPE_CHECKING from . import PoodleNoMutantsFoundError, PoodleTestingFailedError, __version__ from .data_types import PoodleConfig, PoodleWork @@ -12,6 +13,9 @@ from .run import clean_run_each_source_folder, get_runner, run_mutant_trails from .util import calc_timeout, create_temp_zips, create_unified_diff, pprint_str +if TYPE_CHECKING: + from pathlib import Path + logger = logging.getLogger(__name__) @@ -21,10 +25,7 @@ def main_process(config: PoodleConfig) -> None: print_header(work) logger.info("\n%s", pprint_str(config)) - if config.work_folder.exists(): - logger.info("delete %s", config.work_folder) - shutil.rmtree(config.work_folder) - + delete_folder(config.work_folder) create_temp_zips(work) work.mutators = initialize_mutators(work) @@ -46,11 +47,10 @@ def main_process(config: PoodleConfig) -> None: for reporter in work.reporters: reporter(config=config, echo=work.echo, testing_results=results) - logger.info("delete %s", config.work_folder) - shutil.rmtree(config.work_folder) + delete_folder(config.work_folder) if config.fail_under and results.summary.success_rate < config.fail_under / 100: - msg = f"Mutation score {results.summary.coverage_display} is below {config.fail_under:.2f}%" + msg = f"Mutation score {results.summary.coverage_display} is below goal of {config.fail_under:.2f}%" raise PoodleTestingFailedError(msg) @@ -69,11 +69,20 @@ def main_process(config: PoodleConfig) -> None: def print_header(work: PoodleWork) -> None: """Print a header to the console.""" - work.echo(poodle_header_str.format(version=__version__), fg="blue") + work.echo(poodle_header_str.format(version=__version__), fg="cyan") work.echo("Running with the following configuration:") work.echo(f" - Source Folders: {[str(folder) for folder in work.config.source_folders]}") work.echo(f" - Config File: {work.config.config_file}") work.echo(f" - Max Workers: {work.config.max_workers}") work.echo(f" - Runner: {work.config.runner}") work.echo(f" - Reporters: {work.config.reporters}") + if work.config.fail_under: + work.echo(f" - Coverage Goal: {work.config.fail_under:.2f}%") work.echo() + + +def delete_folder(folder: Path) -> None: + """Delete a folder.""" + if folder.exists(): + logger.info("delete %s", folder) + shutil.rmtree(folder) diff --git a/tests/test_cli.py b/tests/test_cli.py index 9ebe6f1..0f4e574 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -29,9 +29,9 @@ def build_config(): @pytest.fixture() -def echo(): - with mock.patch("poodle.cli.click.echo") as echo: - yield echo +def secho(): + with mock.patch("poodle.cli.click.secho") as secho: + yield secho @pytest.fixture() @@ -295,7 +295,7 @@ class TestErrors: def test_main_build_config_input_error( self, main_process: mock.MagicMock, - echo: mock.MagicMock, + secho: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner, ): @@ -303,10 +303,10 @@ def test_main_build_config_input_error( result = runner.invoke(cli.main, []) assert result.exit_code == 4 build_config.assert_called() - echo.assert_has_calls( + secho.assert_has_calls( [ - mock.call("bad input"), - mock.call("input error"), + mock.call("bad input", fg="red"), + mock.call("input error", fg="red"), ] ) main_process.assert_not_called() @@ -314,7 +314,7 @@ def test_main_build_config_input_error( def test_main_process_testing_failed( self, main_process: mock.MagicMock, - echo: mock.MagicMock, + secho: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner, ): @@ -322,10 +322,10 @@ def test_main_process_testing_failed( result = runner.invoke(cli.main, []) assert result.exit_code == 1 build_config.assert_called() - echo.assert_has_calls( + secho.assert_has_calls( [ - mock.call("testing failed"), - mock.call("error message"), + mock.call("testing failed", fg="yellow"), + mock.call("error message", fg="yellow"), ] ) main_process.assert_called_with(build_config.return_value) @@ -333,7 +333,7 @@ def test_main_process_testing_failed( def test_main_process_keyboard_interrupt( self, main_process: mock.MagicMock, - echo: mock.MagicMock, + secho: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner, ): @@ -341,13 +341,13 @@ def test_main_process_keyboard_interrupt( result = runner.invoke(cli.main, []) assert result.exit_code == 2 build_config.assert_called() - echo.assert_called_once_with("Aborted due to Keyboard Interrupt!") + secho.assert_called_once_with("Aborted due to Keyboard Interrupt!", fg="yellow") main_process.assert_called_with(build_config.return_value) def test_main_process_trial_run_error( self, main_process: mock.MagicMock, - echo: mock.MagicMock, + secho: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner, ): @@ -355,10 +355,10 @@ def test_main_process_trial_run_error( result = runner.invoke(cli.main, []) assert result.exit_code == 3 build_config.assert_called() - echo.assert_has_calls( + secho.assert_has_calls( [ - mock.call("testing failed"), - mock.call("error message"), + mock.call("testing failed", fg="red"), + mock.call("error message", fg="red"), ] ) main_process.assert_called_with(build_config.return_value) @@ -366,7 +366,7 @@ def test_main_process_trial_run_error( def test_main_process_input_error( self, main_process: mock.MagicMock, - echo: mock.MagicMock, + secho: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner, ): @@ -374,10 +374,10 @@ def test_main_process_input_error( result = runner.invoke(cli.main, []) assert result.exit_code == 4 build_config.assert_called() - echo.assert_has_calls( + secho.assert_has_calls( [ - mock.call("testing failed"), - mock.call("error message"), + mock.call("testing failed", fg="red"), + mock.call("error message", fg="red"), ] ) main_process.assert_called_with(build_config.return_value) @@ -385,7 +385,7 @@ def test_main_process_input_error( def test_main_process_no_mutants_error( self, main_process: mock.MagicMock, - echo: mock.MagicMock, + secho: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner, ): @@ -393,10 +393,10 @@ def test_main_process_no_mutants_error( result = runner.invoke(cli.main, []) assert result.exit_code == 5 build_config.assert_called() - echo.assert_has_calls( + secho.assert_has_calls( [ - mock.call("testing failed"), - mock.call("error message"), + mock.call("testing failed", fg="yellow"), + mock.call("error message", fg="yellow"), ] ) main_process.assert_called_with(build_config.return_value) @@ -406,7 +406,7 @@ def test_main_process_other_error( self, traceback: mock.MagicMock, main_process: mock.MagicMock, - echo: mock.MagicMock, + secho: mock.MagicMock, build_config: mock.MagicMock, runner: CliRunner, ): @@ -414,6 +414,6 @@ def test_main_process_other_error( result = runner.invoke(cli.main, []) assert result.exit_code == 3 build_config.assert_called() - echo.assert_any_call("Aborted due to Internal Error!") - echo.assert_any_call(traceback.format_exc.return_value) + secho.assert_any_call("Aborted due to Internal Error!", fg="red") + secho.assert_any_call(traceback.format_exc.return_value, fg="red") main_process.assert_called_with(build_config.return_value) diff --git a/tests/test_core.py b/tests/test_core.py index 1249bac..36e1a33 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -17,216 +17,271 @@ def test_logger(): assert core.logger.name == "poodle.core" -class TestMain: - @mock.patch("poodle.core.PoodleWork") - @mock.patch("poodle.core.pprint_str") - @mock.patch("poodle.core.shutil") - @mock.patch("poodle.core.create_temp_zips") - @mock.patch("poodle.core.initialize_mutators") - @mock.patch("poodle.core.get_runner") - @mock.patch("poodle.core.generate_reporters") - @mock.patch("poodle.core.create_mutants_for_all_mutators") - @mock.patch("poodle.core.clean_run_each_source_folder") - @mock.patch("poodle.core.run_mutant_trails") - @mock.patch("poodle.core.print_header") - def test_main( +class TestMainProcess: + @pytest.fixture() + def poodle_work_class(self): + with mock.patch("poodle.core.PoodleWork") as poodle_work_class: + yield poodle_work_class + + @pytest.fixture() + def print_header(self): + with mock.patch("poodle.core.print_header") as print_header: + yield print_header + + @pytest.fixture() + def pprint_str(self): + with mock.patch("poodle.core.pprint_str") as pprint_str: + yield pprint_str + + @pytest.fixture() + def delete_folder(self): + with mock.patch("poodle.core.delete_folder") as delete_folder: + yield delete_folder + + @pytest.fixture() + def create_temp_zips(self): + with mock.patch("poodle.core.create_temp_zips") as create_temp_zips: + yield create_temp_zips + + @pytest.fixture() + def initialize_mutators(self): + with mock.patch("poodle.core.initialize_mutators") as initialize_mutators: + yield initialize_mutators + + @pytest.fixture() + def get_runner(self): + with mock.patch("poodle.core.get_runner") as get_runner: + yield get_runner + + @pytest.fixture() + def generate_reporters(self): + with mock.patch("poodle.core.generate_reporters") as generate_reporters: + yield generate_reporters + + @pytest.fixture() + def create_mutants_for_all_mutators(self): + with mock.patch("poodle.core.create_mutants_for_all_mutators") as create_mutants_for_all_mutators: + yield create_mutants_for_all_mutators + + @pytest.fixture() + def clean_run_each_source_folder(self): + with mock.patch("poodle.core.clean_run_each_source_folder") as clean_run_each_source_folder: + yield clean_run_each_source_folder + + @pytest.fixture() + def calc_timeout(self): + with mock.patch("poodle.core.calc_timeout") as calc_timeout: + yield calc_timeout + + @pytest.fixture() + def run_mutant_trails(self): + with mock.patch("poodle.core.run_mutant_trails") as run_mutant_trails: + yield run_mutant_trails + + @pytest.fixture() + def create_unified_diff(self): + with mock.patch("poodle.core.create_unified_diff") as create_unified_diff: + yield create_unified_diff + + @pytest.fixture() + def _setup_main_process( self, + poodle_work_class: mock.MagicMock, print_header: mock.MagicMock, - run_mutant_trails: mock.MagicMock, - clean_run_each_source_folder: mock.MagicMock, - create_mutants_for_all_mutators: mock.MagicMock, - generate_reporters: mock.MagicMock, - get_runner: mock.MagicMock, - initialize_mutators: mock.MagicMock, - create_temp_zips: mock.MagicMock, - shutil: mock.MagicMock, pprint_str: mock.MagicMock, + delete_folder: mock.MagicMock, + create_temp_zips: mock.MagicMock, + initialize_mutators: mock.MagicMock, + get_runner: mock.MagicMock, + generate_reporters: mock.MagicMock, + create_mutants_for_all_mutators: mock.MagicMock, + clean_run_each_source_folder: mock.MagicMock, + calc_timeout: mock.MagicMock, + run_mutant_trails: mock.MagicMock, + create_unified_diff: mock.MagicMock, + logger_mock: mock.MagicMock, + ): + poodle_work_class.reset_mock() + print_header.reset_mock() + pprint_str.reset_mock() + delete_folder.reset_mock() + create_temp_zips.reset_mock() + initialize_mutators.reset_mock() + get_runner.reset_mock() + generate_reporters.reset_mock() + create_mutants_for_all_mutators.reset_mock() + clean_run_each_source_folder.reset_mock() + calc_timeout.reset_mock() + run_mutant_trails.reset_mock() + create_unified_diff.reset_mock() + logger_mock.reset_mock() + + @pytest.mark.usefixtures("_setup_main_process") + def test_main_process_setup( + self, poodle_work_class: mock.MagicMock, + print_header: mock.MagicMock, + pprint_str: mock.MagicMock, logger_mock: mock.MagicMock, + delete_folder: mock.MagicMock, + create_temp_zips: mock.MagicMock, ): - work_folder = mock.MagicMock() - work_folder.exists.return_value = True - config = PoodleConfigStub(work_folder=work_folder, echo_enabled=True, min_timeout=10, timeout_multiplier=10) + config = PoodleConfigStub() + + core.main_process(config) + + poodle_work_class.assert_called_once_with(config) + work = poodle_work_class.return_value + + print_header.assert_called_once_with(work) + + pprint_str.assert_called_once_with(config) + logger_mock.info.assert_called_once_with("\n%s", pprint_str.return_value) + + delete_folder.assert_called_with(config.work_folder) + assert delete_folder.call_count == 2 + + create_temp_zips.assert_called_once_with(work) + + @pytest.mark.usefixtures("_setup_main_process") + def test_main_process_init( + self, + poodle_work_class: mock.MagicMock, + initialize_mutators: mock.MagicMock, + get_runner: mock.MagicMock, + generate_reporters: mock.MagicMock, + ): + config = PoodleConfigStub() reporter1 = mock.MagicMock() reporter2 = mock.MagicMock() generate_reporters.return_value = iter([reporter1, reporter2]) - mutant1 = mock.MagicMock() - mutant2 = mock.MagicMock() - create_mutants_for_all_mutators.return_value = [mutant1, mutant2] + core.main_process(config) - clean_run_each_source_folder.return_value = {"folder": MutantTrial(mutant=None, result=None, duration=1.0)} # type: ignore [arg-type] + work = poodle_work_class.return_value + + initialize_mutators.assert_called_once_with(work) + get_runner.assert_called_once_with(config) + generate_reporters.assert_called_once_with(config) + + assert work.mutators == initialize_mutators.return_value + assert work.runner == get_runner.return_value + assert work.reporters == [reporter1, reporter2] + + @pytest.mark.usefixtures("_setup_main_process") + def test_main_process_mutants( + self, + poodle_work_class: mock.MagicMock, + create_mutants_for_all_mutators: mock.MagicMock, + ): + config = PoodleConfigStub() core.main_process(config) - poodle_work_class.assert_called_with(config) work = poodle_work_class.return_value - print_header.assert_called_with(work) + create_mutants_for_all_mutators.assert_called_once_with(work) + mutants = create_mutants_for_all_mutators.return_value + work.echo.assert_called_once_with(f"Identified {len(mutants)} mutants") - pprint_str.assert_called_with(config) - logger_mock.info.assert_has_calls( - [ - mock.call("\n%s", pprint_str.return_value), - mock.call("delete %s", work_folder), - ] - ) - assert logger_mock.info.call_count == 3 + @pytest.mark.usefixtures("_setup_main_process") + def test_main_process_mutants_empty( + self, + create_mutants_for_all_mutators: mock.MagicMock, + ): + config = PoodleConfigStub() - work_folder.exists.assert_called() - logger_mock.info.assert_any_call("delete %s", work_folder) + create_mutants_for_all_mutators.return_value = [] - shutil.rmtree.assert_called_with(work_folder) - assert shutil.rmtree.call_count == 2 + with pytest.raises(PoodleNoMutantsFoundError, match=r"^No mutants were found to test!$"): + core.main_process(config) - create_temp_zips.assert_called_with(work) + @pytest.mark.usefixtures("_setup_main_process") + def test_main_process_run( + self, + poodle_work_class: mock.MagicMock, + create_mutants_for_all_mutators: mock.MagicMock, + clean_run_each_source_folder: mock.MagicMock, + calc_timeout: mock.MagicMock, + run_mutant_trails: mock.MagicMock, + ): + config = PoodleConfigStub() - initialize_mutators.assert_called_with(work) - assert work.mutators == initialize_mutators.return_value - get_runner.assert_called_with(config) - assert work.runner == get_runner.return_value - generate_reporters.assert_called_with(config) - assert work.reporters == [reporter1, reporter2] + core.main_process(config) - create_mutants_for_all_mutators.assert_called_with(work) - work.echo.assert_called_with("Identified 2 mutants") + work = poodle_work_class.return_value + mutants = create_mutants_for_all_mutators.return_value - clean_run_each_source_folder.assert_called_with(work) - run_mutant_trails.assert_called_with(work, [mutant1, mutant2], 10.0) - results = run_mutant_trails.return_value + clean_run_each_source_folder.assert_called_once_with(work) + clean_run_results = clean_run_each_source_folder.return_value + calc_timeout.assert_called_once_with(config, clean_run_results) + timeout = calc_timeout.return_value + run_mutant_trails.assert_called_once_with(work, mutants, timeout) - reporter1.assert_called_with(config=config, echo=work.echo, testing_results=results) - reporter2.assert_called_with(config=config, echo=work.echo, testing_results=results) - - logger_mock.info.assert_any_call("delete %s", work_folder) - - @mock.patch("poodle.core.PoodleWork") - @mock.patch("poodle.core.pprint_str") - @mock.patch("poodle.core.shutil") - @mock.patch("poodle.core.create_temp_zips") - @mock.patch("poodle.core.initialize_mutators") - @mock.patch("poodle.core.get_runner") - @mock.patch("poodle.core.generate_reporters") - @mock.patch("poodle.core.create_mutants_for_all_mutators") - @mock.patch("poodle.core.clean_run_each_source_folder") - @mock.patch("poodle.core.run_mutant_trails") - def test_main_not_exists( + @pytest.mark.usefixtures("_setup_main_process") + def test_main_process_report( self, + poodle_work_class: mock.MagicMock, run_mutant_trails: mock.MagicMock, - clean_run_each_source_folder: mock.MagicMock, - create_mutants_for_all_mutators: mock.MagicMock, generate_reporters: mock.MagicMock, - get_runner: mock.MagicMock, - initialize_mutators: mock.MagicMock, - create_temp_zips: mock.MagicMock, - shutil: mock.MagicMock, - pprint_str: mock.MagicMock, - poodle_work_class: mock.MagicMock, - logger_mock: mock.MagicMock, + create_unified_diff: mock.MagicMock, ): - work_folder = mock.MagicMock() - work_folder.exists.return_value = False - config = PoodleConfigStub(work_folder=work_folder, echo_enabled=True, min_timeout=10, timeout_multiplier=10) + config = PoodleConfigStub() - reporter1 = mock.MagicMock() - reporter2 = mock.MagicMock() + results = run_mutant_trails.return_value + trial1 = MutantTrial(mutant=mock.MagicMock(name="mutant1"), result=mock.MagicMock(name="result1"), duration=1.0) + trial2 = MutantTrial(mutant=mock.MagicMock(name="mutant2"), result=mock.MagicMock(name="result2"), duration=1.0) + results.mutant_trials = [trial1, trial2] + + reporter1 = mock.MagicMock(name="reporter1") + reporter2 = mock.MagicMock(name="reporter2") generate_reporters.return_value = iter([reporter1, reporter2]) - mutant1 = mock.MagicMock() - mutant2 = mock.MagicMock() - create_mutants_for_all_mutators.return_value = [mutant1, mutant2] + core.main_process(config) - clean_run_each_source_folder.return_value = {"folder": MutantTrial(mutant=None, result=None, duration=1.0)} # type: ignore [arg-type] + work = poodle_work_class.return_value - core.main_process(config) + create_unified_diff.assert_has_calls([mock.call(trial1.mutant), mock.call(trial2.mutant)]) + assert trial1.mutant.unified_diff == create_unified_diff.return_value + assert trial2.mutant.unified_diff == create_unified_diff.return_value - logger_mock.info.assert_has_calls( - [ - mock.call("\n%s", pprint_str.return_value), - mock.call("delete %s", work_folder), - ] - ) - assert logger_mock.info.call_count == 2 - - shutil.rmtree.assert_called_with(work_folder) - assert shutil.rmtree.call_count == 1 - - @mock.patch("poodle.core.PoodleWork") - @mock.patch("poodle.core.pprint_str") - @mock.patch("poodle.core.shutil") - @mock.patch("poodle.core.create_temp_zips") - @mock.patch("poodle.core.initialize_mutators") - @mock.patch("poodle.core.get_runner") - @mock.patch("poodle.core.generate_reporters") - @mock.patch("poodle.core.create_mutants_for_all_mutators") - @mock.patch("poodle.core.clean_run_each_source_folder") - @mock.patch("poodle.core.run_mutant_trails") - def test_input_error( + reporter1.assert_called_once_with(config=config, echo=work.echo, testing_results=results) + reporter2.assert_called_once_with(config=config, echo=work.echo, testing_results=results) + + @pytest.mark.usefixtures("_setup_main_process") + def test_main_process_fail_under_pass( self, + poodle_work_class: mock.MagicMock, run_mutant_trails: mock.MagicMock, - clean_run_each_source_folder: mock.MagicMock, - create_mutants_for_all_mutators: mock.MagicMock, generate_reporters: mock.MagicMock, - get_runner: mock.MagicMock, - initialize_mutators: mock.MagicMock, - create_temp_zips: mock.MagicMock, - shutil: mock.MagicMock, - pprint_str: mock.MagicMock, - poodle_work_class: mock.MagicMock, - logger_mock: mock.MagicMock, + create_unified_diff: mock.MagicMock, ): - work_folder = mock.MagicMock() - work_folder.exists.return_value = False + config = PoodleConfigStub(fail_under=80.0) - create_mutants_for_all_mutators.return_value = [] + results = run_mutant_trails.return_value + results.summary.success_rate = 0.8 + results.summary.coverage_display = "80.00%" + + core.main_process(config) + # no error reported - with pytest.raises(PoodleNoMutantsFoundError, match="^No mutants were found to test!$"): - core.main_process(PoodleConfigStub(work_folder=work_folder)) - - @mock.patch("poodle.core.PoodleWork") - @mock.patch("poodle.core.pprint_str") - @mock.patch("poodle.core.shutil") - @mock.patch("poodle.core.create_temp_zips") - @mock.patch("poodle.core.initialize_mutators") - @mock.patch("poodle.core.get_runner") - @mock.patch("poodle.core.generate_reporters") - @mock.patch("poodle.core.create_mutants_for_all_mutators") - @mock.patch("poodle.core.clean_run_each_source_folder") - @mock.patch("poodle.core.run_mutant_trails") - def test_fail_under_error( + @pytest.mark.usefixtures("_setup_main_process") + def test_main_process_fail_under_fail( self, + poodle_work_class: mock.MagicMock, run_mutant_trails: mock.MagicMock, - clean_run_each_source_folder: mock.MagicMock, - create_mutants_for_all_mutators: mock.MagicMock, generate_reporters: mock.MagicMock, - get_runner: mock.MagicMock, - initialize_mutators: mock.MagicMock, - create_temp_zips: mock.MagicMock, - shutil: mock.MagicMock, - pprint_str: mock.MagicMock, - poodle_work_class: mock.MagicMock, - logger_mock: mock.MagicMock, + create_unified_diff: mock.MagicMock, ): - work_folder = mock.MagicMock() - work_folder.exists.return_value = False + config = PoodleConfigStub(fail_under=80.0) results = run_mutant_trails.return_value + results.summary.success_rate = 0.7999 + results.summary.coverage_display = "79.99%" - results.summary.coverage_display = "80.00%" - results.summary.success_rate = 0.8 - - clean_run_each_source_folder.return_value = {"folder": MutantTrial(mutant=None, result=None, duration=1.0)} # type: ignore [arg-type] - - with pytest.raises(PoodleTestingFailedError, match=r"^Mutation score 80.00% is below 99.00%$"): - core.main_process( - PoodleConfigStub( - work_folder=work_folder, - fail_under=99, - min_timeout=10, - timeout_multiplier=10, - ) - ) + with pytest.raises(PoodleTestingFailedError, match=r"^Mutation score 79.99% is below goal of 80.00%$"): + core.main_process(config) poodle_header_str = r""" @@ -251,6 +306,7 @@ def test_print_header(self): max_workers=10, runner="pytest", reporters=["summary", "json"], + fail_under=None, ) ) work.echo = mock.MagicMock() @@ -260,7 +316,7 @@ def test_print_header(self): work.echo.assert_has_calls( [ - mock.call(poodle_header_str, fg="blue"), + mock.call(poodle_header_str, fg="cyan"), mock.call("Running with the following configuration:"), mock.call(" - Source Folders: ['src']"), mock.call(" - Config File: config_file.toml"), @@ -270,3 +326,58 @@ def test_print_header(self): mock.call(), ] ) + + def test_print_header_goal(self): + work = PoodleWork( + config=PoodleConfigStub( + source_folders=["src"], + config_file="config_file.toml", + max_workers=10, + runner="pytest", + reporters=["summary", "json"], + fail_under=53.4, + ) + ) + work.echo = mock.MagicMock() + + with mock.patch("poodle.core.__version__", "1.2.3"): + core.print_header(work) + + work.echo.assert_has_calls( + [ + mock.call(poodle_header_str, fg="cyan"), + mock.call("Running with the following configuration:"), + mock.call(" - Source Folders: ['src']"), + mock.call(" - Config File: config_file.toml"), + mock.call(" - Max Workers: 10"), + mock.call(" - Runner: pytest"), + mock.call(" - Reporters: ['summary', 'json']"), + mock.call(" - Coverage Goal: 53.40%"), + mock.call(), + ] + ) + + +class TestDeleteFolder: + @pytest.fixture() + def shutil(self): + with mock.patch("poodle.core.shutil") as shutil: + yield shutil + + def test_delete_folder_exists(self, shutil, logger_mock): + folder = mock.MagicMock() + folder.exists.return_value = True + + core.delete_folder(folder) + + logger_mock.info.assert_called_once_with("delete %s", folder) + shutil.rmtree.assert_called_once_with(folder) + + def test_delete_folder_not_exists(self, shutil, logger_mock): + folder = mock.MagicMock() + folder.exists.return_value = False + + core.delete_folder(folder) + + logger_mock.info.assert_not_called() + shutil.rmtree.assert_not_called() From b7972fd98d009b08b64ddcf0668eb2eb6fa02a33 Mon Sep 17 00:00:00 2001 From: WiredNerd Date: Sat, 6 Jan 2024 00:52:24 +0000 Subject: [PATCH 3/4] :robot: Update version to 1.3.1 --- 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 086d1e2..29a8da1 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.3.0" +__version__ = "1.3.1" class PoodleTestingFailedError(Exception): From ef85b1ebe08b07657ad478b9ec51f09669225656 Mon Sep 17 00:00:00 2001 From: WiredNerd Date: Sat, 6 Jan 2024 09:34:10 -0600 Subject: [PATCH 4/4] :wrench: Display Percentage fixes --- src/poodle/core.py | 5 +++-- src/poodle/data_types/data.py | 4 +++- src/poodle/util.py | 5 +++++ tests/data_types/test_data.py | 4 ++-- tests/test_core.py | 10 ++-------- tests/test_util.py | 18 ++++++++++++++++++ 6 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/poodle/core.py b/src/poodle/core.py index bd615c9..ecf5a76 100644 --- a/src/poodle/core.py +++ b/src/poodle/core.py @@ -11,7 +11,7 @@ from .mutate import create_mutants_for_all_mutators, initialize_mutators from .report import generate_reporters from .run import clean_run_each_source_folder, get_runner, run_mutant_trails -from .util import calc_timeout, create_temp_zips, create_unified_diff, pprint_str +from .util import calc_timeout, create_temp_zips, create_unified_diff, display_percent, pprint_str if TYPE_CHECKING: from pathlib import Path @@ -50,7 +50,8 @@ def main_process(config: PoodleConfig) -> None: delete_folder(config.work_folder) if config.fail_under and results.summary.success_rate < config.fail_under / 100: - msg = f"Mutation score {results.summary.coverage_display} is below goal of {config.fail_under:.2f}%" + display_fail_under = display_percent(config.fail_under / 100) + msg = f"Mutation score {results.summary.coverage_display} is below goal of {display_fail_under}" raise PoodleTestingFailedError(msg) diff --git a/src/poodle/data_types/data.py b/src/poodle/data_types/data.py index e3b1ea0..25ea085 100644 --- a/src/poodle/data_types/data.py +++ b/src/poodle/data_types/data.py @@ -6,6 +6,8 @@ from pathlib import Path from typing import TYPE_CHECKING, Any +from poodle import util + if TYPE_CHECKING: from typing_extensions import Self @@ -165,7 +167,7 @@ def success_rate(self) -> float: @property def coverage_display(self) -> str: """Return a formatted string for the coverage percentage.""" - return f"{self.success_rate * 100:.2f}%" + return util.display_percent(self.success_rate) def __iadd__(self, result: MutantTrialResult) -> Self: """Update Testing Summary with data from MutantTrialResult.""" diff --git a/src/poodle/util.py b/src/poodle/util.py index 02714ff..ca08763 100644 --- a/src/poodle/util.py +++ b/src/poodle/util.py @@ -126,3 +126,8 @@ def to_json(obj: PoodleSerialize, indent: int | str | None = None) -> str: def from_json(data: str, datatype: type[PoodleSerialize]) -> PoodleSerialize: """Convert json string to dataclass.""" return datatype(**json.loads(data, object_hook=datatype.from_dict)) + + +def display_percent(value: float) -> str: + """Convert float to string with percent sign.""" + return f"{value * 1000 // 1 / 10:.3g}%" diff --git a/tests/data_types/test_data.py b/tests/data_types/test_data.py index 681dac8..730f45c 100644 --- a/tests/data_types/test_data.py +++ b/tests/data_types/test_data.py @@ -348,7 +348,7 @@ def test_success_rate_zero(self): def test_coverage_display(self): summary = TestingSummary(trials=9, found=6) - assert summary.coverage_display == "66.67%" + assert summary.coverage_display == "66.6%" def test_iadd(self): summary = TestingSummary(trials=10) @@ -438,7 +438,7 @@ def summary_dict(self): "timeout": 6, "errors": 5, "success_rate": 0.8, - "coverage_display": "80.00%", + "coverage_display": "80%", } def test_serialize(self): diff --git a/tests/test_core.py b/tests/test_core.py index 36e1a33..62d9bb5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -252,10 +252,7 @@ def test_main_process_report( @pytest.mark.usefixtures("_setup_main_process") def test_main_process_fail_under_pass( self, - poodle_work_class: mock.MagicMock, run_mutant_trails: mock.MagicMock, - generate_reporters: mock.MagicMock, - create_unified_diff: mock.MagicMock, ): config = PoodleConfigStub(fail_under=80.0) @@ -269,18 +266,15 @@ def test_main_process_fail_under_pass( @pytest.mark.usefixtures("_setup_main_process") def test_main_process_fail_under_fail( self, - poodle_work_class: mock.MagicMock, run_mutant_trails: mock.MagicMock, - generate_reporters: mock.MagicMock, - create_unified_diff: mock.MagicMock, ): config = PoodleConfigStub(fail_under=80.0) results = run_mutant_trails.return_value results.summary.success_rate = 0.7999 - results.summary.coverage_display = "79.99%" + results.summary.coverage_display = "79.9%" - with pytest.raises(PoodleTestingFailedError, match=r"^Mutation score 79.99% is below goal of 80.00%$"): + with pytest.raises(PoodleTestingFailedError, match=r"^Mutation score 79.9% is below goal of 80%$"): core.main_process(config) diff --git a/tests/test_util.py b/tests/test_util.py index b200d6b..ed421b2 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -279,3 +279,21 @@ def test_create_unified_diff_no_file(self): source_file=None, ) assert util.create_unified_diff(mutant) is None + + +class TestDisplayPercent: + @pytest.mark.parametrize( + ("value", "expected"), + [ + (0.0014, "0.1%"), + (0.0016, "0.1%"), + (0.1239, "12.3%"), + (0.12, "12%"), + (0.1, "10%"), + (0.99, "99%"), + (0.999, "99.9%"), + (0.9999, "99.9%"), + ], + ) + def test_display_percent(self, value, expected): + assert util.display_percent(value) == expected