Skip to content

Mutation testing: mutation_run_mutmut runner survivors #341

Description

@leynos

Context

Local mutation-testing run against main at 3c1fa01bcf4c26dac79975c18b74c01d577dcf95 (mutmut 3.6.0, committed [tool.mutmut] configuration: source_paths = ["workflow_scripts/"], test selection workflow_scripts/tests/, act-gated workflow tests excluded). Run totals: 1463 mutants — 979 killed, 478 survived, 5 no-tests, 1 timeout.

These scripts power the estate's CI, so mutation gaps here matter most. This issue covers one coherent area; sibling issues cover the rest. Equivalent mutants are recorded in the triage worklist and deliberately omitted here. Inspect any survivor with uv run --with mutmut==3.6.0 mutmut show <name>; names below are workflow_scripts.<module>.x_<function>__mutmut_<n> with the numbers listed per line.

Analysis

mutation_run_mutmut.py is the helper that runs mutmut inside caller repositories — a gap here weakens mutation testing across the whole estate. The uv run --with mutmut==<version> mutmut ... command construction is asserted only loosely (individual flag mutants such as --WITH, --ALL, and TRUE survive, each of which would break the real invocation), the failure path's stderr routing and diagnostics are unasserted, and the job-summary Markdown is not snapshot-tested. count_statuses survives an accumulation bug (+= 1= 1) because no fixture has two mutants sharing a status. 50 survivors are real test gaps; 6 are accepted (below).

Suggested tests

  1. Assert the exact argv for both invocations (cmd-mox or the emitted mutation_mutmut_command line): uv run --with mutmut==<version> mutmut run <extra> <globs> and ... mutmut results --all true. One equality assertion per command kills every flag-token mutant, including _mutmut_command variants.
  2. The failing-run path: a stubbed non-zero mutmut run must emit mutation_mutmut_exit_code=<code> on stdout, the mutation_mutmut_error=... diagnostic on stderr (assert the stream and the exact message including the code), and raise SystemExit with the same code.
  3. Snapshot (syrupy) tests of render_summary and _render_survivor_table covering: no results, results with survivors and no tests entries, and results without survivors — pinning the heading, count lines, and table markup kills all Markdown-string mutants.
  4. count_statuses with two mutants sharing a status, asserting the count is 2.
  5. _parse_result_line with a space-containing name (must return None) to kill the mangled-separator guard.
  6. _publish_results: assert the results artefact file content equals the captured output and that mutation_mutmut_counts= is emitted with the exact JSON payload.

Accepted survivors (no test to write)

  • _publish_results mutants 12, 14, 21, 23 (workflow_scripts/mutation_run_mutmut.py:273,275): locale-dependent encoding=None/omitted-encoding variants, equivalent on UTF-8 CI runners. Candidate # pragma: no mutate.
  • _run_mutation_testing mutants 18 and 19 (workflow_scripts/mutation_run_mutmut.py:257): RETCODE(FG=...) only toggles plumbum's console streaming; the captured exit code is identical, so no in-process test can observe the change. Candidate # pragma: no mutate.

Surviving mutants

  • workflow_scripts/mutation_run_mutmut.py:145 in _parse_result_line (mutants 3; 2-equivalent)
    • original: name, separator, status = line.partition(": ")
    • 3: name, separator, status = line.rpartition(": ") — mutmut result lines contain a single ': ' separator; rpartition matches
  • workflow_scripts/mutation_run_mutmut.py:148 in _parse_result_line (mutants 8; 1-test-gap)
    • original: is_mutant_name = " " not in name and "__mutmut_" in name
    • 8: is_mutant_name = "XX XX" not in name and "__mutmut_" in name
  • workflow_scripts/mutation_run_mutmut.py:189 in count_statuses (mutants 2; 1-test-gap)
    • original: counts[result.status] += 1
    • 2: counts[result.status] = 1
  • workflow_scripts/mutation_run_mutmut.py:198 in _render_survivor_table (mutants 3; 1-test-gap)
    • original: ### Surviving mutants",
    • 3: XX### Surviving mutantsXX",
  • workflow_scripts/mutation_run_mutmut.py:199 in _render_survivor_table (mutants 6, 10; 1-test-gap)
    • original: ,
    • 6: XXXX",
    • 10: XXXX",
  • workflow_scripts/mutation_run_mutmut.py:200 in _render_survivor_table (mutants 7, 8, 9; 1-test-gap)
    • original: Inspect a survivor with uv run mutmut show .",
    • 7: XXInspect a survivor with uv run mutmut show .XX",
    • 8: inspect a survivor with uv run mutmut show .",
    • 9: INSPECT A SURVIVOR WITH UV RUN MUTMUT SHOW .",
  • workflow_scripts/mutation_run_mutmut.py:202 in _render_survivor_table (mutants 11, 12, 13; 1-test-gap)
    • original: | Mutant | Status |",
    • 11: XX| Mutant | Status |XX",
    • 12: | mutant | status |",
    • 13: | MUTANT | STATUS |",
  • workflow_scripts/mutation_run_mutmut.py:203 in _render_survivor_table (mutants 14; 1-test-gap)
    • original: | ------ | ------ |",
    • 14: XX| ------ | ------ |XX",
  • workflow_scripts/mutation_run_mutmut.py:206 in _render_survivor_table (mutants 16; 1-test-gap)
    • original: return [*header, *rows, ""]
    • 16: return [*header, *rows, "XXXX"]
  • workflow_scripts/mutation_run_mutmut.py:225 in render_summary (mutants 2, 5; 1-test-gap)
    • original: lines = ["## Mutation testing results (mutmut)", ""]
    • 2: lines = ["XX## Mutation testing results (mutmut)XX", ""]
    • 5: lines = ["## Mutation testing results (mutmut)", "XXXX"]
  • workflow_scripts/mutation_run_mutmut.py:227 in render_summary (mutants 8, 9, 12; 1-test-gap)
    • original: return "\\n".join((*lines, "No mutants were tested.", ""))
    • 8: return "XX\\nXX".join((*lines, "No mutants were tested.", ""))
    • 9: return "\\n".join((*lines, "XXNo mutants were tested.XX", ""))
    • 12: return "\\n".join((*lines, "No mutants were tested.", "XXXX"))
  • workflow_scripts/mutation_run_mutmut.py:230 in render_summary (mutants 18; 1-test-gap)
    • original: lines.append("")
    • 18: lines.append("XXXX")
  • workflow_scripts/mutation_run_mutmut.py:233 in render_summary (mutants 24; 1-test-gap)
    • original: return "\\n".join(lines)
    • 24: return "XX\\nXX".join(lines)
  • workflow_scripts/mutation_run_mutmut.py:238 in _mutmut_command (mutants 1, 3, 4; 1-test-gap)
    • original: return ["run", "--with", f"mutmut=={version}", "mutmut"]
    • 1: return ["XXrunXX", "--with", f"mutmut=={version}", "mutmut"]
    • 3: return ["run", "XX--withXX", f"mutmut=={version}", "mutmut"]
    • 4: return ["run", "--WITH", f"mutmut=={version}", "mutmut"]
  • workflow_scripts/mutation_run_mutmut.py:251 in _run_mutation_testing (mutants 2; 1-test-gap)
    • original: *_mutmut_command(mutmut_version),
    • 2: *_mutmut_command(None),
  • workflow_scripts/mutation_run_mutmut.py:256 in _run_mutation_testing (mutants 6, 7, 10, 11, 12, 13; 1-test-gap)
    • original: emit("mutation_mutmut_command", ["uv", *run_arguments])
    • 6: emit(None, ["uv", *run_arguments])
    • 7: emit("mutation_mutmut_command", None)
    • 10: emit("XXmutation_mutmut_commandXX", ["uv", *run_arguments])
    • 11: emit("MUTATION_MUTMUT_COMMAND", ["uv", *run_arguments])
    • 12: emit("mutation_mutmut_command", ["XXuvXX", *run_arguments])
    • 13: emit("mutation_mutmut_command", ["UV", *run_arguments])
  • workflow_scripts/mutation_run_mutmut.py:257 in _run_mutation_testing (mutants 18, 19; 4-untestable)
    • original: code = local["uv"][run_arguments] & RETCODE(FG=True)
    • 18: code = local["uv"][run_arguments] & RETCODE(FG=None) — plumbum RETCODE(FG=...) only toggles console streaming; exit code identical
    • 19: code = local["uv"][run_arguments] & RETCODE(FG=False) — plumbum RETCODE(FG=...) only toggles console streaming; exit code identical
  • workflow_scripts/mutation_run_mutmut.py:258 in _run_mutation_testing (mutants 20, 21, 24, 25; 1-test-gap)
    • original: emit("mutation_mutmut_exit_code", code)
    • 20: emit(None, code)
    • 21: emit("mutation_mutmut_exit_code", None)
    • 24: emit("XXmutation_mutmut_exit_codeXX", code)
    • 25: emit("MUTATION_MUTMUT_EXIT_CODE", code)
  • workflow_scripts/mutation_run_mutmut.py:261 in _run_mutation_testing (mutants 28, 34, 35; 1-test-gap)
    • original: mutation_mutmut_error",
    • 28: None,
    • 34: XXmutation_mutmut_errorXX",
    • 35: MUTATION_MUTMUT_ERROR",
  • workflow_scripts/mutation_run_mutmut.py:262 in _run_mutation_testing (mutants 29; 1-test-gap)
    • original: f"mutmut run failed with exit code {code} (failing baseline?)",
    • 29: None,
  • workflow_scripts/mutation_run_mutmut.py:263 in _run_mutation_testing (mutants 30; 1-test-gap)
    • original: stream=sys.stderr,
    • 30: stream=None,
  • workflow_scripts/mutation_run_mutmut.py:263 in _run_mutation_testing (mutants 33; 1-test-gap)
    • original: stream=sys.stderr, / )
    • 33: )
  • workflow_scripts/mutation_run_mutmut.py:271 in _publish_results (mutants 4, 5, 7, 8, 9, 10; 1-test-gap)
    • original: *_mutmut_command(mutmut_version), "results", "--all", "true
    • 4: *_mutmut_command(None), "results", "--all", "true
    • 5: *_mutmut_command(mutmut_version), "XXresultsXX", "--all", "true
    • 7: *_mutmut_command(mutmut_version), "results", "XX--allXX", "true
    • 8: *_mutmut_command(mutmut_version), "results", "--ALL", "true
    • 9: *_mutmut_command(mutmut_version), "results", "--all", "XXtrueXX
    • 10: *_mutmut_command(mutmut_version), "results", "--all", "TRUE
  • workflow_scripts/mutation_run_mutmut.py:273 in _publish_results (mutants 12, 14, 17; 2-equivalent/4-untestable)
    • original: Path(results_file).write_text(results_text, encoding="utf-8")
    • 12: Path(results_file).write_text(results_text, encoding=None) — encoding=None locale-dependent
    • 14: Path(results_file).write_text(results_text, ) — encoding omitted, locale-dependent
    • 17: Path(results_file).write_text(results_text, encoding="UTF-8") — UTF-8 codec alias
  • workflow_scripts/mutation_run_mutmut.py:275 in _publish_results (mutants 21, 23, 28; 2-equivalent/4-untestable)
    • original: with Path(summary_path).open("a", encoding="utf-8") as handle:
    • 21: with Path(summary_path).open("a", encoding=None) as handle: — encoding=None locale-dependent
    • 23: with Path(summary_path).open("a", ) as handle: — encoding omitted, locale-dependent
    • 28: with Path(summary_path).open("a", encoding="UTF-8") as handle: — UTF-8 codec alias
  • workflow_scripts/mutation_run_mutmut.py:277 in _publish_results (mutants 31, 32, 35, 36; 1-test-gap)
    • original: emit("mutation_mutmut_counts", count_statuses(results))
    • 31: emit(None, count_statuses(results))
    • 32: emit("mutation_mutmut_counts", None)
    • 35: emit("XXmutation_mutmut_countsXX", count_statuses(results))
    • 36: emit("MUTATION_MUTMUT_COUNTS", count_statuses(results))

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions