Build1 publisher3 min readPublished
canfail breaks the guarded file on purpose to see whether the check notices
Mutation testing needs a function to mutate, so the YAML and Terraform that gate production go untested. canfail edits an anchored string in those files, runs your check and reports whether it failed for the reason you declared.
The Engineer · Build desk

What happened
- canfail reads a JSON file of declared breaks, each naming a file, an anchor string, its replacement and the failure expected, then applies the edit, runs the check and reports whether the check noticed.
- The tool targets guards mutation testing cannot reach, where the guarded thing is YAML, Terraform or a Dockerfile, and its author says to keep using Stryker wherever there is a function to mutate.
- The repository's example declares four breaks against a single check and closes with the tally line "4 declared break(s): 1 caught, 1 not caught, 2 not settled".
- An early version reported a genuinely blind guard as catching, but only when that break ran second, a result the author found by pointing canfail at its own source with its own suite as the check.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- capability A green check over config files can now be interrogated. Mutation tools have only ever offered that assurance to code with functions in it.
- cost Each guard costs a hand-written break: an anchor that appears exactly once in the file plus the failure text you expect, so guards nobody can author a break for stay unverified.
- constraint Because a timed-out check and a caught break share the same red, a pipeline that grades canfail on exit status alone will file ambiguity as success.
- precedent Harnesses that edit, run and restore inside one second have to disable mtime-keyed caches outright, since make, ninja, bytecode caches and file watchers will not see the edit at all.
Most of the design is in refusing to call a red build a catch. If the break leaves the guarded file unparseable, the check goes red and canfail scores it `wrong-failure`, because the failure has to match the reason you named [7]. That kind of red tells you about the parser. A check that hits its timeout exits 124, and exit 124 looks exactly like going red, so the break settles nothing [8]. The anchor string has to match exactly once: zero matches means the edit never happened, and two means the check was asked about code nobody was thinking of [9]. Nothing runs at all until the check passes on the clean tree [6].
The example in the repository is small on purpose. It flips `item["price"] * item["quantity"]` to `item["price"] + item["quantity"]` in a price-total function and expects the failure text to match `assert|Error` [17]. Install is `pip install canfail`, and the run is `canfail canfail.json` [1].
Every break ends with the file restored and the restore verified by digest, which is the `restore-verified` package's job [10]. canfail used to carry 78 lines of that guard inline, about a quarter of the module, so it could claim no dependencies [11]. That puts the module at roughly 310 lines [12]. The author deleted the copy once the real package existed, and wrote that "a second copy of a guarantee is a second thing to get wrong, and the copy is the one that never gets the upstream's tests" [13].
"Blind" has two readings that send you to opposite ends of the CI file: the check ran and did not notice, or the check never ran [26]. The first says the guard is weak, the second says it is missing [26]. Declaring an `evidence` object on a check hands that question to `didrun`, so a break that stops the check from running comes back as a refusal instead of a finding [27]. Omit it and the report states that it cannot tell the two apart [27]. An `evidence` object naming no known predicate is a config error, since a misspelled key that silently downgrades a check is this tool's own failure mode pointed at itself [28].
The cause of the ordering bug was Python's bytecode cache. After the first break, `__pycache__` held bytecode compiled from the broken source, and the second break's run executed that stale bytecode and failed for the previous break's reason [19]. The author's fix was to stop restoring mtime on the guarded file [20]. The mutation pass against canfail's own source showed the fix does nothing, because re-enabling bytecode caching breaks the test that pins the ordering bug whether or not mtime is restored [22]. mtime invalidation has one-second granularity, the tool edits, runs and restores in milliseconds, and a `.pyc` written from the broken source looks fresh either way [23]. `PYTHONDONTWRITEBYTECODE` is the guard that holds, and not restoring mtime is redundant beside it [24].
Read the tally as a property of that fixture. Four declared breaks producing one of each outcome is what a teaching example is built to do, and two of the four come back with no verdict [14][16]. CI asserts the tally string. Exit 1 on its own would be satisfied by finding the wrong thing [15]. The README has a section titled "Things this got wrong about itself", and its entries are verdicts the tool reported on its own guards [29].
What to watch
- Whether timeouts get a distinct report from a genuine red, which would convert two of the example's four outcomes into verdicts.
- Whether didrun grows predicates beyond the set canfail recognises, since an evidence object naming an unknown predicate is currently a config error.
- Whether the restore-verified split holds now that canfail depends on an upstream package for the digest check it used to inline.