Skip to content

Mutation testing with cargo-mutants: measure assertion strength, not just execution #107

Description

@CarlAllenn

Why

Coverage (#105, #106) measures execution, not verification: a line counts as covered even if no test asserts anything about it. Mutation testing measures the thing coverage proxies badly — whether the suite notices when the logic changes. cargo-mutants systematically flips operators (<<=), deletes negations, replaces function bodies with defaults, and reports every mutant the test suite fails to kill. On a spec-exact parser with documented fail-closed behaviour, surviving mutants are usually real findings: either a missing assertion or (occasionally) code that provably doesn't matter.

This complements the existing strength layers (legacy edtf.js oracle, property tests, fuzz targets, D/N-decision-anchored assertions) rather than replacing them.

Approach

  • Run cargo mutants per crate, starting with the highest-value targets: edtf-core (parser.rs, bounds.rs, relation.rs) and edtf-normalize (engine.rs).
  • Exclude edtf-postgres (pgrx harness; same rationale as the coverage exclusion in ci.yml).
  • Triage each surviving mutant: add the missing assertion, or document why the mutant is equivalent (no observable behaviour change). Equivalent mutants go in .cargo/mutants.toml exclusions with a reason, mirroring the D/N-decision style.
  • Runtime is the constraint (mutants × suite runtime). Options, in order of preference:
    1. Local/one-shot sprint first to burn down the backlog; decide on CI afterwards.
    2. If CI-worthy: run only on changed files per PR (cargo mutants --in-diff), as a published-not-gated report like coverage — thresholds rot into gaming here too.
  • Pin the tool via mise like everything else.

Exit criteria

  • Zero unexplained surviving mutants in edtf-core and edtf-normalize.
  • A documented decision on whether an --in-diff CI job earns its runtime.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions