Language / 言語: English · 日本語
I am growing Fullseye, a personal library of about 2,200 image-processing and measurement functions (“ops”). Here I call every check that decides whether an op’s result may pass a gate: pytest tests, checks that generated files are up to date, CI jobs — all of them exist to stop what should not pass.
The gates run about 15,000 test cases over more than 7,000 test functions, and they are all green. Even so, the gates were green while the results were false more than once. This article sorts those lies into four patterns and gives, for each, real incidents and how the gate was rebuilt. None of the patterns is specific to Fullseye; anyone who writes tests steps on them.
| Pattern | What happens | How to rebuild |
|---|---|---|
| The gate exists but does not run where the accident happens | Green on the development checkout, red in the distribution or on Linux | Count where the accident happens (wheel, Linux, CI) |
| Green, but the content is empty | Empty output, an empty list or a discarded verdict counts as “agreement” | Count the amount of content separately; break the gate on purpose and watch it fire |
| Ran on everything, covered nothing | One input or one parameter set feels like “we looked at all of it” | Add structurally different inputs; when the exemption list is long, suspect the gate first |
| It only agrees with its own spec | The answer key is the formula you wrote yourself | Bring in independent ground truth: published values, a second implementation, invariance under a transformation |
26 % of the ops were missing from the wheel. When rebuilding the distribution wheel, a module was left out of py-modules in pyproject.toml, and 224 ops were gone in an environment that installed the wheel. A gate checking “zero backends failed to load” existed — but it only ran on the development checkout, where every file is present, so in principle it could never reproduce this accident.
The fix was a CI job that actually builds the wheel, installs it into a separate virtual environment and compares the op set against the checkout. Later, the same mistake (backends_texture left out) turned that job red at once, and the fix was one line.
Three more incidents of the same pattern:
.gitignore: a ledger category named build matched build/ in .gitignore, so three notes were never committed. Everything was green locally; CI alone had 10 failures. The “file exists” gate never asked whether git tracked the file. → A gate that generated paths never match an ignore rule.highpass.md existed for both 2-D and 1-D ops; Windows found the 2-D one first and Linux the 1-D one, so a gate failed only in CI. os.walk order depends on the file system, and the same lookup had been copied into four gates and one tool.Lesson: after writing a gate, check that it runs where the accident happens. Green on the development machine is not evidence of a fix.
Empty output agrees with everything. The drift gate (regenerate everything and check the working tree does not change) stays green even if a generator stops producing anything. It actually let through an index claiming “0 op notes” in six languages and help pages with 0 images and 60 pages of raw Markdown. → A separate gate counts the amount of content (notes with examples, body length, zero empty cells).
Three more:
assert code == 0 always passed, so three PoCs without a single assert went unnoticed.assert all(x for x in xs) and for x in xs: assert ... pass unconditionally when xs is empty. After three rounds of tightening, 126 remained (in 84 files); the count is now pinned in a ledger and turns red if it grows — a ratchet that only moves down.Lesson: a gate needs a gate that checks it does not pass emptiness. The surest way is to break the gate on purpose and watch it turn red. In Fullseye, gates come with tests such as “the gate really sees every PoC” and “the public-name gate is not empty”.
One probe is not coverage. The gate that calls every op with a probe stood in the right place. Still, the threshold op xsitk_huang_thresh succeeded 4 of 4 times on a sin/cos stripe probe and failed 4 of 4 times (all-zero output) on gradient, circle and checkerboard probes. With three structurally different probes, the ledger of ops exempted as “degenerate input” shrank from 27 to 12. Most exemptions were not properties of the ops but the blind spot of a single probe.
Parameters do the same. 168 ops declared that splitting the image into tiles does not change the result, and the gate checked them with one setting, tile=64, halo=16. With other settings 18 of them broke.
The most painful one: a differential fuzzer reported 0 findings over 60,000 cases. It compared how many objects were found but never observed their order. A deliberate mutation that reversed the order survived 3,000 cases; adding one line of observation killed it at the second case. Zero findings was not robustness; it was a sign of not looking.
Lesson: “ran on all of them” is not “covered all of them”. When an exemption list is long, suspect the probe and the observation before the op. Trust a fuzzer’s zero only after it has killed a planted mutation.
52 synthetic gates stayed green; the standard’s worked example found 5 defects. Nine ops for measurement system analysis (gauge R&R) and measurement uncertainty (GUM) were checked by 52 gates on synthetic data, all green. Adding the worked examples printed in the standards as ground truth exposed five defects: no path for pooling the interaction term (EV off by 7.3 %), a missing truncation of the effective degrees of freedom (coverage factor 2.1132 against the standard’s 2.12), and so on. All five were of the kind “the spec never mentions it”, which synthetic data built from one’s own spec can never find.
A second implementation finds what single-implementation tests cannot. When the image-processing C ABI was rewritten in Rust, the first 8 cases all agreed — read as “the probe is weak”, not “passed”. Nine cases aimed at the places where the spec is silent showed that the connected-components op was 4-connected on the numpy path and 8-connected on the OpenCV path: 32 components versus 1 on an 8×8 checkerboard. Each path passed its own tests.
Invariance under a transformation is a gate even without ground truth. A point-cloud descriptor claimed to be rotation-invariant flipped sign at 42.4 % of points after a 90-degree rotation, because its helper chose normal signs arbitrarily. A gate per claim — “unchanged under 12 random rotations” — exposes the lie without knowing a single true value.
Lesson: do not grade with the formula you wrote. Bring ground truth from outside: published values, a second implementation in another language, invariance and scale laws (scale by k and the feature changes by k^p).
On the day of writing I added ops and PoCs that measure the C. elegans connectome (the wiring diagram of a worm’s nervous system). Here is what the gates caught that day, next to the patterns.
round rounds half to even (2.5 → 2), so half-integers pile into even bins — dangerously easy to read as a biological periodicity. Counting the integer sum removed it.| Kind | Ground truth | Example tests |
|---|---|---|
| Closed form / identity | theorems, analytic solutions | sum of degrees = number of edges / mean curvature of a minimal surface = 0 |
| Second implementation | another language, another backend, brute force | Rust agrees with Python / brute-force 3-cycles / generated C is bit-identical |
| Published values | worked examples and figures in standards | gauge R&R worked example / the 59 GenICam pixel formats |
| Fail-closed | refuse out-of-contract input | unknown type names, non-positive σ, never return identity when alignment fails |
| Invariance / scale law | unchanged under a transformation, or changed by a declared exponent | invariant under 12 rotations / scale by k gives k^p |
| Controls (nulls) | chance with the degrees kept | every null sample keeps the degree sequence exactly |
| Degenerate / uniform input | empty, constant, one pixel | a uniform image never becomes a full-frame detection |
| Probes | call every op with structurally different inputs | zero fallbacks / every knob has an effect |
| Drift | regenerate everything, the tree does not change | regen_all --check |
| Counting registration surfaces | numbers counted from the real thing | counts in the README, the index, __all__ |
| Counting from the distribution | the wheel, py-modules |
functions without a public path do not grow |
| Translation fingerprints | fingerprint of the source text | a stale translation is detected when the source changes |
| Running the deliverables | exit 0 and PASS in a separate process | more than 150 PoCs, code samples in the docs |
| Environment / git | tracked, no local paths, order | never matched by ignore / no private paths in published files |
| Gates of gates | the gate itself fires | does not pass emptiness / turns red when broken / ledger of vacuous asserts |
Scale is given for reference, not as a headline: 420 test files, 7,192 test functions, about 15,000 cases, and about 35 minutes for the full suite. CI runs Python 3.10 / 3.11 / 3.12 × 4 shards = 12 jobs, plus a job that actually builds and counts the wheel, the drift check, and a weekly order shuffle; a release can only be cut from a commit whose CI is green.
I set the questions and the direction; implementation, new gates and adversarial review went to Claude Code. Many of the incidents here are records of that review.