Skip to content
Open
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
17 changes: 4 additions & 13 deletions src/mutmut/__main__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
from typing import Iterable
from mutmut.type_checking import TypeCheckingError
from mutmut.type_checking import run_type_checker
from typing import Any
Expand Down Expand Up @@ -55,7 +54,6 @@

import click
import libcst as cst
import libcst.matchers as m
from rich.text import Text
from setproctitle import setproctitle

Expand Down Expand Up @@ -137,7 +135,7 @@ def guess_paths_to_mutate():


def record_trampoline_hit(name):
assert not name.startswith('src.'), f'Failed trampoline hit. Module name starts with `src.`, which is invalid'
assert not name.startswith('src.'), 'Failed trampoline hit. Module name starts with `src.`, which is invalid'
if mutmut.config.max_stack_depth != -1:
f = inspect.currentframe()
c = mutmut.config.max_stack_depth
Expand Down Expand Up @@ -293,13 +291,7 @@ def create_mutants_for_file(filename: Path, output_path: Path) -> FileMutationRe
# source_mtime == mutant_mtime: only copied, otherwise the mutant file is untouched
# source_mtime < mutant_mtime: the mutations have been saved after copying; source file untouched
if source_mtime < mutant_mtime:
# reset the mutation stats
source_file_mutation_data = SourceFileMutationData(path=filename)
source_file_mutation_data.load()
for key in source_file_mutation_data.exit_code_by_key:
source_file_mutation_data.exit_code_by_key[key] = None
source_file_mutation_data.save()

# Source unchanged, mutations already generated — preserve cached results
return FileMutationResult(unmodified=True)
except OSError:
pass
Expand Down Expand Up @@ -626,7 +618,6 @@ def pytest_deselected(self, items):

collector = TestsCollector()

tests_dir = mutmut.config.tests_dir
pytest_args = ['-x', '-q', '--collect-only'] + self._pytest_add_cli_args_test_selection

with change_cwd('mutants'):
Expand Down Expand Up @@ -1127,7 +1118,7 @@ def print_time_estimates(mutant_names):

for time, key in sorted(times_and_keys):
if not time:
print(f'<no tests>', key)
print('<no tests>', key)
else:
print(f'{int(time*1000)}ms', key)

Expand Down Expand Up @@ -1620,7 +1611,7 @@ def on_data_table_row_highlighted(self, event):
duration = source_file_mutation_data.durations_by_key.get(mutant_name, '?')
type_check_error = source_file_mutation_data.type_check_error_by_key.get(mutant_name, '?')

view_tests_description = f'(press t to view tests executed for this mutant)'
view_tests_description = '(press t to view tests executed for this mutant)'

match status:
case 'killed':
Expand Down
54 changes: 54 additions & 0 deletions tests/e2e/test_e2e_incremental_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import shutil
from pathlib import Path

from tests.e2e.e2e_utils import change_cwd, read_all_stats_for_project, write_json_file, read_json_file

import mutmut
from mutmut.__main__ import _run


def test_rerun_preserves_cached_results():
"""Rerunning mutmut on unchanged source must not reset cached exit codes to None.

Strategy: run mutmut once, then inject a sentinel exit code (99) into the
meta file. If the second run preserves the cache, the sentinel survives.
If it resets and re-tests, 99 gets replaced with a real exit code.
"""
project_path = Path("..").parent / "e2e_projects" / "my_lib"
mutants_path = project_path / "mutants"
shutil.rmtree(mutants_path, ignore_errors=True)

try:
# First run: generate and test all mutants
mutmut._reset_globals()
with change_cwd(project_path):
_run([], None)

# Inject sentinel exit code (99) into every mutant result
meta_files = list(mutants_path.rglob("*.meta"))
assert meta_files, "Expected .meta files after first run"

sentinel = 99
for meta_file in meta_files:
meta = read_json_file(meta_file)
for key in meta["exit_code_by_key"]:
meta["exit_code_by_key"][key] = sentinel
write_json_file(meta_file, meta)

# Second run: source unchanged, sentinel values should survive
mutmut._reset_globals()
with change_cwd(project_path):
_run([], None)

second_run_stats = read_all_stats_for_project(project_path)

# Every result should still be the sentinel — not None, not a real exit code
for meta_path, results in second_run_stats.items():
for mutant_name, exit_code in results.items():
assert exit_code == sentinel, (
f"Cached result for {mutant_name} in {meta_path} was not preserved. "
f"Expected sentinel {sentinel}, got {exit_code}."
)
finally:
# Cleanup
shutil.rmtree(mutants_path, ignore_errors=True)