Skip to content

Mutation testing: cargo-mutants runner and summariser survivors #342

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

These two scripts run cargo-mutants and merge its shard reports; like the mutmut runner, gaps here weaken mutation testing estate-wide. In mutation_run_cargo.py, the --shard emission boundary survives (shard_count > 1> 2, so two-shard runs would silently run unsharded) and the exit-code meaning strings are unasserted. In mutation_summarize_cargo.py, the malformed-scenario fallbacks (_survivor_from placeholders, _start_line default), the loop robustness (continuebreak skips all artefacts after one bad entry), the merged timeout/unviable counts, the root-first report ordering, the diagnostic emit keys, and the summary Markdown all escape. All 49 survivors are real test gaps except one accepted encoding mutant (below).

Suggested tests

  1. build_arguments with shard_count=2: assert --shard 0/2 is present (kills the boundary mutant).
  2. interpret_exit_code: parametrise over 0, 1, 2, 3, 4, 70, and an unknown code, asserting both the success flag and the exact meaning string.
  3. parse_outcomes with a non-dict outcome entry followed by a valid missed mutant: the survivor must still be collected (kills continuebreak).
  4. _survivor_from with a scenario whose Mutant is missing or not a dict, and with a mutant missing file/name/span: assert the exact ("?", 0, "?") placeholders (kills the dozen placeholder mutants and the str(None) defaults); _start_line with a missing span asserts 0.
  5. collect_reports over a fixture tree containing an unmatched directory name, a directory without outcomes.json, an invalid JSON file, and two shards of one slug plus a second slug sorting before root: assert the skipped/missing/invalid emit diagnostics via capsys (exact keys and values), the summed caught/missed/timeout/unviable counts, and root-first ordering.
  6. A snapshot (syrupy) test of render_summary for reports with and without survivors, including a mutation name containing | (pins headings, count lines, table markup, and the cell escaping); plus the empty-reports message.
  7. main: assert mutation_summary_targets= is emitted with the exact slug-to-missed JSON mapping.

Accepted survivors (no test to write)

  • collect_reports mutant 27 (workflow_scripts/mutation_summarize_cargo.py:187): locale-dependent encoding=None, equivalent on UTF-8 CI runners. Candidate # pragma: no mutate.

Surviving mutants

  • workflow_scripts/mutation_run_cargo.py:134 in build_arguments (mutants 14; 1-test-gap)
    • original: if invocation.shard_count > 1:
    • 14: if invocation.shard_count > 2:
  • workflow_scripts/mutation_run_cargo.py:158 in interpret_exit_code (mutants 2, 6, 7; 1-test-gap)
    • original: meaning = EXIT_CODE_MEANINGS.get(code, "unexpected exit code")
    • 2: meaning = EXIT_CODE_MEANINGS.get(None, "unexpected exit code")
    • 6: meaning = EXIT_CODE_MEANINGS.get(code, "XXunexpected exit codeXX")
    • 7: meaning = EXIT_CODE_MEANINGS.get(code, "UNEXPECTED EXIT CODE")
  • workflow_scripts/mutation_summarize_cargo.py:130 in collect_reports (mutants 12; 1-test-gap)
    • original: continue
    • 12: break
  • workflow_scripts/mutation_summarize_cargo.py:130 in parse_outcomes (mutants 14; 1-test-gap)
    • original: continue
    • 14: break
  • workflow_scripts/mutation_summarize_cargo.py:146 in _survivor_from (mutants 7, 8, 9, 10, 11, 12, 13, 14, 15; 1-test-gap)
    • original: return SurvivingMutant(file="?", line=0, name="?")
    • 7: return SurvivingMutant(file=None, line=0, name="?")
    • 8: return SurvivingMutant(file="?", line=None, name="?")
    • 9: return SurvivingMutant(file="?", line=0, name=None)
    • 10: return SurvivingMutant(line=0, name="?")
    • 11: return SurvivingMutant(file="?", name="?")
    • 12: return SurvivingMutant(file="?", line=0, )
    • 13: return SurvivingMutant(file="XX?XX", line=0, name="?")
    • 14: return SurvivingMutant(file="?", line=1, name="?")
    • 15: return SurvivingMutant(file="?", line=0, name="XX?XX")
  • workflow_scripts/mutation_summarize_cargo.py:148 in _survivor_from (mutants 24, 26, 29; 1-test-gap)
    • original: file=str(mutant.get("file", "?")),
    • 24: file=str(mutant.get("file", None)),
    • 26: file=str(mutant.get("file", )),
    • 29: file=str(mutant.get("file", "XX?XX")),
  • workflow_scripts/mutation_summarize_cargo.py:150 in _survivor_from (mutants 33, 35, 38; 1-test-gap)
    • original: name=str(mutant.get("name", "?")),
    • 33: name=str(mutant.get("name", None)),
    • 35: name=str(mutant.get("name", )),
    • 38: name=str(mutant.get("name", "XX?XX")),
  • workflow_scripts/mutation_summarize_cargo.py:159 in _start_line (mutants 13; 1-test-gap)
    • original: return line if isinstance(line, int) else 0
    • 13: return line if isinstance(line, int) else 1
  • workflow_scripts/mutation_summarize_cargo.py:180 in collect_reports (mutants 6, 7, 10, 11; 1-test-gap)
    • original: emit("mutation_summary_skipped_dir", artefact_dir.name)
    • 6: emit(None, artefact_dir.name)
    • 7: emit("mutation_summary_skipped_dir", None)
    • 10: emit("XXmutation_summary_skipped_dirXX", artefact_dir.name)
    • 11: emit("MUTATION_SUMMARY_SKIPPED_DIR", artefact_dir.name)
  • workflow_scripts/mutation_summarize_cargo.py:184 in collect_reports (mutants 18, 19, 22, 23; 1-test-gap)
    • original: emit("mutation_summary_missing_outcomes", artefact_dir.name)
    • 18: emit(None, artefact_dir.name)
    • 19: emit("mutation_summary_missing_outcomes", None)
    • 22: emit("XXmutation_summary_missing_outcomesXX", artefact_dir.name)
    • 23: emit("MUTATION_SUMMARY_MISSING_OUTCOMES", artefact_dir.name)
  • workflow_scripts/mutation_summarize_cargo.py:187 in collect_reports (mutants 27, 29; 2-equivalent/4-untestable)
    • original: payload = json.loads(outcomes_path.read_text(encoding="utf-8"))
    • 27: payload = json.loads(outcomes_path.read_text(encoding=None)) — encoding=None locale-dependent
    • 29: payload = json.loads(outcomes_path.read_text(encoding="UTF-8")) — UTF-8 codec alias
  • workflow_scripts/mutation_summarize_cargo.py:189 in collect_reports (mutants 30, 31, 34, 35; 1-test-gap)
    • original: emit("mutation_summary_invalid_outcomes", f"{artefact_dir.name}: {error}")
    • 30: emit(None, f"{artefact_dir.name}: {error}")
    • 31: emit("mutation_summary_invalid_outcomes", None)
    • 34: emit("XXmutation_summary_invalid_outcomesXX", f"{artefact_dir.name}: {error}")
    • 35: emit("MUTATION_SUMMARY_INVALID_OUTCOMES", f"{artefact_dir.name}: {error}")
  • workflow_scripts/mutation_summarize_cargo.py:205 in collect_reports (mutants 59; 1-test-gap)
    • original: timeout=totals["Timeout"],
    • 59: timeout=None,
  • workflow_scripts/mutation_summarize_cargo.py:206 in collect_reports (mutants 60; 1-test-gap)
    • original: unviable=totals["Unviable"],
    • 60: unviable=None,
  • workflow_scripts/mutation_summarize_cargo.py:211 in collect_reports (mutants 87, 88; 1-test-gap)
    • original: return sorted(reports, key=lambda report: (report.slug != "root", report.slug))
    • 87: return sorted(reports, key=lambda report: (report.slug != "XXrootXX", report.slug))
    • 88: return sorted(reports, key=lambda report: (report.slug != "ROOT", report.slug))
  • workflow_scripts/mutation_summarize_cargo.py:235 in render_summary (mutants 2; 1-test-gap)
    • original: return "## Mutation testing results\\n\\nNo reports were produced.\\n
    • 2: return "XX## Mutation testing results\\n\\nNo reports were produced.\\nXX
  • workflow_scripts/mutation_summarize_cargo.py:241 in render_summary (mutants 7, 8, 13; 1-test-gap)
    • original: ,
    • 7: XXXX",
    • 8: XXXX",
    • 13: XXXX",
  • workflow_scripts/mutation_summarize_cargo.py:252 in render_summary (mutants 10; 1-test-gap)
    • original: ### Surviving mutants",
    • 10: XX### Surviving mutantsXX",
  • workflow_scripts/mutation_summarize_cargo.py:254 in render_summary (mutants 14, 15, 16; 1-test-gap)
    • original: | File | Line | Mutation |",
    • 14: XX| File | Line | Mutation |XX",
    • 15: | file | line | mutation |",
    • 16: | FILE | LINE | MUTATION |",
  • workflow_scripts/mutation_summarize_cargo.py:255 in render_summary (mutants 17; 1-test-gap)
    • original: | ---- | ---- | -------- |",
    • 17: XX| ---- | ---- | -------- |XX",
  • workflow_scripts/mutation_summarize_cargo.py:262 in render_summary (mutants 22; 1-test-gap)
    • original: lines.append("")
    • 22: lines.append("XXXX")
  • workflow_scripts/mutation_summarize_cargo.py:263 in render_summary (mutants 24; 1-test-gap)
    • original: return "\\n".join(lines)
    • 24: return "XX\\nXX".join(lines)

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions