diff --git a/e2e_projects/mutate_only_covered_lines/pyproject.toml b/e2e_projects/mutate_only_covered_lines/pyproject.toml index e09b3712..e51233f0 100644 --- a/e2e_projects/mutate_only_covered_lines/pyproject.toml +++ b/e2e_projects/mutate_only_covered_lines/pyproject.toml @@ -16,6 +16,11 @@ dev = [ "pytest>=8.3.5", ] +[tool.coverage.run] +omit = [ + "src/mutate_only_covered_lines/omit_me.py", +] + [tool.mutmut] debug = true mutate_only_covered_lines = true diff --git a/e2e_projects/mutate_only_covered_lines/src/mutate_only_covered_lines/__init__.py b/e2e_projects/mutate_only_covered_lines/src/mutate_only_covered_lines/__init__.py index 2c0c2bde..83a65e59 100644 --- a/e2e_projects/mutate_only_covered_lines/src/mutate_only_covered_lines/__init__.py +++ b/e2e_projects/mutate_only_covered_lines/src/mutate_only_covered_lines/__init__.py @@ -1,3 +1,5 @@ +from mutate_only_covered_lines.omit_me import this_function_shall_NOT_be_mutated + def hello_mutate_only_covered_lines(simple_branch: bool) -> str: if simple_branch: return "Hello from mutate_only_covered_lines! (true)" @@ -40,3 +42,6 @@ def mutate_only_covered_lines_multiline(simple_branch: bool) -> str: ] return f"Hello from mutate_only_covered_lines!" \ f" (false) {x} {y}" + +def do_not_mutate_external_ommited_function(): + return this_function_shall_NOT_be_mutated() diff --git a/e2e_projects/mutate_only_covered_lines/src/mutate_only_covered_lines/omit_me.py b/e2e_projects/mutate_only_covered_lines/src/mutate_only_covered_lines/omit_me.py new file mode 100644 index 00000000..60c1c7f3 --- /dev/null +++ b/e2e_projects/mutate_only_covered_lines/src/mutate_only_covered_lines/omit_me.py @@ -0,0 +1,2 @@ +def this_function_shall_NOT_be_mutated(): + return 3 + 4 diff --git a/e2e_projects/mutate_only_covered_lines/tests/main/test_mutate_only_covered_lines.py b/e2e_projects/mutate_only_covered_lines/tests/main/test_mutate_only_covered_lines.py index f1b135a4..2fe08be7 100644 --- a/e2e_projects/mutate_only_covered_lines/tests/main/test_mutate_only_covered_lines.py +++ b/e2e_projects/mutate_only_covered_lines/tests/main/test_mutate_only_covered_lines.py @@ -1,5 +1,5 @@ from mutate_only_covered_lines.ignore_me import this_function_shall_NOT_be_mutated -from mutate_only_covered_lines import hello_mutate_only_covered_lines, mutate_only_covered_lines_multiline, function_with_pragma +from mutate_only_covered_lines import hello_mutate_only_covered_lines, mutate_only_covered_lines_multiline, function_with_pragma, do_not_mutate_external_ommited_function """This tests the mutate_only_covered_lines feature.""" @@ -14,3 +14,6 @@ def test_mutate_only_covered_lines_multiline(): def call_ignored_function(): assert this_function_shall_NOT_be_mutated() == 3 + +def test_do_not_mutate_external_ommited_function(): + assert do_not_mutate_external_ommited_function() == 7 diff --git a/pyproject.toml b/pyproject.toml index 2f29f048..c4e6a8de 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -95,7 +95,6 @@ ignore = [ "F402", # import shadowed by loop variable "F841", # local variable assigned but never used "UP007", # use X | Y for type annotations - "UP038", # use X | Y in isinstance ] [tool.ruff.lint.isort] diff --git a/src/mutmut/__main__.py b/src/mutmut/__main__.py index 2a710c87..4076ef90 100644 --- a/src/mutmut/__main__.py +++ b/src/mutmut/__main__.py @@ -5,6 +5,7 @@ import sys from collections.abc import Iterable from collections.abc import Iterator +from typing import TYPE_CHECKING from typing import Any from mutmut.type_checking import TypeCheckingError @@ -64,6 +65,9 @@ from mutmut.file_mutation import mutate_file_contents from mutmut.trampoline_templates import CLASS_NAME_SEPARATOR +if TYPE_CHECKING: + from coverage import Coverage + # Document: surviving mutants are retested when you ask mutmut to retest them, interactively in the UI or via command line # TODO: pragma no mutate should end up in `skipped` category @@ -540,6 +544,9 @@ def prepare_main_test_run(self) -> None: def run_tests(self, *, mutant_name: str | None, tests: Iterable[str]) -> int: raise NotImplementedError() + def collect_main_test_coverage(self, cov: Coverage) -> int: + raise NotImplementedError() + def list_all_tests(self) -> ListAllTestsResult: raise NotImplementedError() @@ -607,6 +614,14 @@ def execute_pytest(self, params: list[str], **kwargs: Any) -> int: raise BadTestExecutionCommandsException(params) return exit_code + def _pytest_args_regular_run(self, tests: Iterable[str]) -> list[str]: + pytest_args = ["-x", "-q", "-p", "no:randomly", "-p", "no:random-order"] + if tests: + pytest_args += list(tests) + else: + pytest_args += self._pytest_add_cli_args_test_selection + return pytest_args + def run_stats(self, *, tests: Iterable[str]) -> int: class StatsCollector: # noinspection PyMethodMayBeStatic @@ -626,27 +641,20 @@ def pytest_runtest_makereport(self, item: Any, call: Any) -> None: stats_collector = StatsCollector() - pytest_args = ["-x", "-q"] - if tests: - pytest_args += list(tests) - else: - pytest_args += self._pytest_add_cli_args_test_selection with change_cwd("mutants"): - return int(self.execute_pytest(pytest_args, plugins=[stats_collector])) + return int(self.execute_pytest(self._pytest_args_regular_run(tests), plugins=[stats_collector])) def run_tests(self, *, mutant_name: str | None, tests: Iterable[str]) -> int: - pytest_args = ["-x", "-q", "-p", "no:randomly", "-p", "no:random-order"] - if tests: - pytest_args += list(tests) - else: - pytest_args += self._pytest_add_cli_args_test_selection with change_cwd("mutants"): - return int(self.execute_pytest(pytest_args)) + return int(self.execute_pytest(self._pytest_args_regular_run(tests))) + + def collect_main_test_coverage(self, cov: Coverage) -> int: + with change_cwd("mutants"), cov.collect(): + self.prepare_main_test_run() + return int(self.execute_pytest(self._pytest_args_regular_run([]))) def run_forced_fail(self) -> int: - pytest_args = ["-x", "-q"] + self._pytest_add_cli_args_test_selection - with change_cwd("mutants"): - return int(self.execute_pytest(pytest_args)) + return self.run_tests(mutant_name=None, tests=[]) def list_all_tests(self) -> ListAllTestsResult: class TestsCollector: @@ -977,6 +985,7 @@ def load_config() -> Config: Path("setup.cfg"), Path("pyproject.toml"), Path("pytest.ini"), + Path(".coveragerc"), Path(".gitignore"), ] + list(Path(".").glob("test*.py")), diff --git a/src/mutmut/code_coverage.py b/src/mutmut/code_coverage.py index d8d69fbf..17e43f31 100644 --- a/src/mutmut/code_coverage.py +++ b/src/mutmut/code_coverage.py @@ -41,10 +41,8 @@ def gather_coverage(runner: TestRunner, source_files: Iterable[Path]) -> dict[st mutants_path = Path("mutants") # Run the tests and gather coverage - cov = coverage.Coverage(source=[str(mutants_path.absolute())], data_file=None) - with cov.collect(): - runner.prepare_main_test_run() - runner.run_tests(mutant_name=None, tests=[]) + cov = coverage.Coverage(data_file=None) + runner.collect_main_test_coverage(cov) # Build mapping of filenames to covered lines # The CoverageData object is a wrapper around sqlite, and this diff --git a/tests/e2e/test_e2e_coverage.py b/tests/e2e/test_e2e_coverage.py index 0bb351c0..112180cb 100644 --- a/tests/e2e/test_e2e_coverage.py +++ b/tests/e2e/test_e2e_coverage.py @@ -42,6 +42,7 @@ def test_mutate_only_covered_lines_result_snapshot(): "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_30": 1, "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_31": 1, "mutate_only_covered_lines.x_mutate_only_covered_lines_multiline__mutmut_32": 1, - } + }, + "mutants/src/mutate_only_covered_lines/omit_me.py.meta": {}, } )