Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions e2e_projects/mutate_only_covered_lines/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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)"
Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def this_function_shall_NOT_be_mutated():
return 3 + 4
Original file line number Diff line number Diff line change
@@ -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."""

Expand All @@ -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
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
39 changes: 24 additions & 15 deletions src/mutmut/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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")),
Expand Down
6 changes: 2 additions & 4 deletions src/mutmut/code_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tests/e2e/test_e2e_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {},
}
)
Loading