Commit 107b5a3
authored
feat: Mutation caching and transitive dependency tracking (#509)
* feat: per-function hashing for incremental cache invalidation
When a source file changes, only re-test mutants in functions whose AST
hash changed; preserve prior results for unchanged functions in the same
file.
- compute_function_hashes / _compute_mutated_function_hashes in
file_mutation.py: class-qualified mangled keys (x_foo /
xǁClassǁmethod) -> 12-char sha256 of the function AST. Methods and
nested-class methods are indexed under the same key the merge looks up,
closing the latent silent-preservation bug for changed methods.
- mutate_file_contents returns a 3-tuple (code, names, hashes).
- SourceFileMutationData gains hash_by_function_name, persisted in .meta
with a pop-with-default so old files still load.
- create_mutants_for_file: mtime short-circuit now preserves all prior
results instead of resetting them; on a real change, load-and-merge
compares new hashes against old, resets only changed/unhashed mutants,
and preserves the rest.
- Tests: update all mutate_file_contents unpack sites; add tests for
hash stability, body-change detection, comment-insensitivity, method
key inclusion, two-function preserve/reset integration, and the method
regression guard.
* feat: cross-call dependency tracking for incremental stats invalidation
Records caller->callee edges at stats collection time so stale outgoing
call edges can be cleared when a callee's code changes.
- state.py: MutmutState singleton holding old_function_hashes,
current_function_hashes, and function_dependencies (callee → callers).
- core.py: MutmutCallStack ContextVar propagates caller context through
call chains.
- trampoline.py stats branch: resolves caller via MutmutCallStack,
passes it to record_trampoline_hit, sets updated context for inner
calls, respects MUTMUT_DEPENDENCY_DEPTH env ceiling.
- record_trampoline_hit gains caller param; upstream's source-path-
resolving max_stack_depth walk preserved verbatim; dependency edge
written only when track_dependencies=True.
- FileMutationResult gains changed_functions/current_hashes (deferred
from commit 1); create_mutants accumulates current_hashes into
state().current_function_hashes across worker results.
- create_mutants_for_file builds module-qualified current_hashes and
changed_functions for return to parent.
- load_stats/save_stats persist function_hashes and function_dependencies
alongside existing test associations (backwards-compatible pop-with-
default on load).
- _cleanup_stale_stats: removes test associations and dependency edges
for modules absent from current_function_hashes.
- _invalidate_stale_dependency_edges: clears changed functions from all
caller sets so stale outgoing edges are rebuilt on next stats run.
- collect_or_load_stats: on incremental load, runs cleanup always and
invalidation when track_dependencies; persists the result.
- Config gains track_dependencies (default True) and
dependency_tracking_depth (default None); run_stats_collection sets
MUTMUT_DEPENDENCY_DEPTH from config.
- Tests: record_trampoline_hit with/without track_dependencies,
_cleanup_stale_stats removes unknown modules, _invalidate_stale_
dependency_edges clears changed callers and no-ops on first run,
config defaults asserted.
* e2e: add benchmark project with 1k mutants
- Add e2e_projects/benchmark_1k/ with ~1000 mutants for testing
- Includes modules: numbers, strings, booleans, operators, comparisons,
arguments, returns, complex (recursion, higher-order functions)
- Configurable delays via BENCHMARK_IMPORT_DELAY, BENCHMARK_CONFTEST_DELAY,
BENCHMARK_TEST_DELAY environment variables to simulate the performance
under variable test and startup runtimes.
* feat: invalidate cache on config and dependency changes
Cached verdicts were only invalidated when a function body changed, so
changes to config or dependency files silently produced stale results.
- Config.config_fingerprint() hashes result-affecting config, grouped so
we reset only what each change can affect:
- timeout change -> reset only timeout verdicts
- type_check_command change -> reset mutants whose type-check status
flips (symmetric difference of old exit-37 and newly-caught)
- pytest_add_cli_args / test-selection change -> reset all results and
force full stats recollection
- set-affecting config (source_paths, only_mutate, ...) is ignored:
new mutants are uncached and dropped ones stop being walked
- compute_watched_file_hashes() hashes dependency/build files
(pyproject.toml, setup.cfg/py, requirements*.txt, lockfiles) plus user
globs from the new cache_invalidation_files config. The
on_dependency_change config ("warn" | "rerun" | "ignore", default
"warn") controls whether a change warns or resets all results.
- Fingerprints persist in mutmut-stats.json with pop-with-default, so
old
caches load and a missing fingerprint triggers no invalidation.
* feat: use git to detect non-Python dependency file changes
Replace the fixed watched-file list with git-based change detection.
mutmut now uses `git diff`/`git ls-files` to find every non-.py file
changed since the last full run, falling back to the curated list when
git is unavailable. A default exclude set (*.md, *.rst, docs/, LICENSE,
etc.) drops files that never affect tests; users can extend it with
`cache_invalidation_exclude`. The git commit and file hashes are
persisted together as a baseline so a later git-less environment (e.g.
a separate CI stage) can still detect changes to previously-tracked
files by re-hashing them. New options: `use_git_change_detection`
(default true) and `cache_invalidation_exclude`.
* HISTORY
* lock
* fix: address three cache-correctness bugs from Copilot review
- Return cached function hashes for mtime-skipped files so
_cleanup_stale_stats and _invalidate_stale_dependency_edges don't
treat unchanged files as deleted; use get_mutant_name at both call
sites instead of inlining the path→module conversion
- Change dependency_tracking_depth default from None to -1 so
setup.cfg values are correctly coerced to int by the config loader;
narrow type from int | None to int and drop the conditional in
run_stats_collection
- Fix benchmark fixture to yield unconditionally so it doesn't fail
when BENCHMARK_TEST_DELAY=01 parent bd6057f commit 107b5a3
41 files changed
Lines changed: 4598 additions & 41 deletions
File tree
- e2e_projects/benchmark_1k
- src/benchmark
- tests
- src/mutmut
- mutation
- utils
- tests
- mutation
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1 | 1 | | |
2 | 2 | | |
3 | 3 | | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
4 | 20 | | |
5 | 21 | | |
6 | 22 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
401 | 401 | | |
402 | 402 | | |
403 | 403 | | |
| 404 | + | |
| 405 | + | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
| 410 | + | |
| 411 | + | |
| 412 | + | |
| 413 | + | |
| 414 | + | |
| 415 | + | |
| 416 | + | |
| 417 | + | |
| 418 | + | |
| 419 | + | |
| 420 | + | |
| 421 | + | |
| 422 | + | |
| 423 | + | |
| 424 | + | |
| 425 | + | |
| 426 | + | |
| 427 | + | |
| 428 | + | |
| 429 | + | |
| 430 | + | |
| 431 | + | |
| 432 | + | |
| 433 | + | |
| 434 | + | |
| 435 | + | |
| 436 | + | |
| 437 | + | |
| 438 | + | |
| 439 | + | |
| 440 | + | |
| 441 | + | |
| 442 | + | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
404 | 475 | | |
405 | 476 | | |
406 | 477 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
0 commit comments