Evidence
Measuring, testing, and trusting a result ¶
The expensive mistakes here have not been wrong code. They have been correct-looking results, and nobody could have falsified them. A sweep found nothing because the detector was broken, nobody checked a rate for precision, and a green test could not have gone red. This page collects them.
Everything on it is one question in different clothes.
flowchart LR
R(["a result you believe"]) --> Q{"what would this have looked<br/>like if it were wrong?"}
Q -->|"different"| E["evidence"]
Q -->|"the same"| N["not evidence yet"]
N --> P["plant a known instance,<br/>then measure again"]
P --> R
classDef bad stroke-dasharray: 4 3;
class N bad;
Measuring, and trusting a measurement ¶
The fifteen rules below are grouped by what was doing the lying.
The instrument ¶
1. A NEGATIVE RESULT NEEDS A POSITIVE CONTROL. “Zero hits across 62 documents” is a claim about the
instrument until you show the instrument can find a known instance. Three absence claims here were
artifacts of the detector: a table shape the heuristic could not see, a regex that could not match
V CC, and a corpus sweep running a different code path from the one shipping. Plant a known instance
first. The fixtures usually already contain one.
Two habits make that cheap. Instrument the gates, counting what each filter REJECTED and not only what it matched. And exercise the shipped configuration, because a sweep run with different flags has validated a different program.
2. A CHECK RUN BEFORE THE EDIT IS NOT EVIDENCE ABOUT THE EDIT. The instrument can be right, the
invocation can be right, and the run can still be stale. It happened before the code existed, and it
is then cited as evidence about that code. A tsc --noEmit passed, two more files were written, the
gate then failed on one of them, and the pass was reported as proof that the gate and the local check
“resolve differently”. They do not, since all three spellings of that command catch the error, and
re-running it would have said so in two seconds. Nothing about the output looks stale.
The cheap habit is to re-run the check as the LAST thing before you believe it. When a local pass and a gate failure disagree, suspect the ordering before you theorise about the tools. The theory is the expensive answer and it is almost never the right one.
3. A REPLICA OF THE ALGORITHM MEASURES THE REPLICA. Agni issue 682 reported that a capture’s freshness stamp treated a fetched board as no fixture at all, with a table of three stamps showing two of them identical. The table was produced by re-implementing the hash rather than calling it. The shipped hash has a guard that refuses a fixture with no tracked files outright, and the re-implementation left it out. The ticket’s conclusion survived in spirit, because the fetched corpus genuinely could not be used as a fixture. Every stated fact about the mechanism was wrong. Re-implementing is tempting whenever the real function is unexported or wants a whole harness. In exactly those cases, write the throwaway test that calls the real function. The one here took four lines and would have printed the refusal.
4. A detector that FIRES is a claim about the instrument too. A sweep for “does this document print a
document number” reported 86% coverage on a regex that accepted TPS22918 and TCAN1145, both PART
numbers, while missing SLVSAG5, a real one. Wrong in both directions, and the headline would have
justified building an extractor on it. Run a new detector past known positives AND known negatives by
hand before quoting a rate. When it cannot tell the two apart, that is the finding.
5. A SUBSTRING count is not an occurrence count, and it fails upward. Sizing the glossary sweep meant
counting how often each term appears. A case-insensitive substring match reported TP 47 times in one
section. The word-boundary count there is ZERO, because the match was finding tp inside http and
output. The same sweep read pull-up at 89 and ESD at 43. Excluding rule names like i2c-pull-up
and fenced code roughly halved both, to 44 and 13. Every error ran the same way, inflating the work.
Scoping off the first numbers would have oversized the job about twofold. Worse, it would have
recorded a term as well covered when the course never mentions it. Anchor the pattern, strip code and
identifiers, and re-count before the number decides anything.
The fixture ¶
6. A feature premised on a house CONVENTION needs its base rate measured on real designs, and the fixtures cannot tell you. Every fixture here names things the way the built-in vocabulary expects. That naming made the fixtures work, and it also makes a convention-shaped feature always look well-founded against them. One issue was written around a net-name convention that encodes endpoints. That convention covered 1.1% and 0.06% of nets on two real boards, and the tutorial project does not use it at all. Measuring the base rate takes one query, and it has twice changed what was worth building.
7. A feature no fixture EXERCISES cannot fail a test, and that reads exactly like working. The datasheet role tier shipped against a corpus where not one seeded spec declared pins. So every test passed, the real boards were unchanged, and nothing could have gone red if it were wrong. The fix was to give the shipped fixture the data the feature reads. The feature then went from unfalsifiable to demonstrable, with 0 rails becoming 3 on the tutorial netlist.
8. A fixture written to test a behaviour may be unable to EXHIBIT it, and then the finding is about
the fixture. hier_bus_root.kicad_sch labels its bus DATA[1:0]. A session concluded from it that
KiCad keeps bus members apart across a sheet boundary, and that conclusion held up a fix for a while.
KiCad’s vector syntax is [first..last], so that fixture contains no bus at all and could never have
shown members crossing. Change nothing but the spelling and kicad-cli joins them. The fixture is
kept, with a comment saying it is really a control. A sibling fixture holds the real case, and the
pair is only meaningful together. Before trusting a negative result from a fixture, check that a
POSITIVE one was reachable from it.
9. Ask the reference tool rather than reasoning about the format. In one session three defects each turned on a detail that no reasonable derivation got right. Bus members pair off by ascending index and not in written order, a symbol is rotated before it is mirrored, and a colon-spelled range is not a bus. Sign conventions, a Y-flip and two composition orders were all in play. Build the smallest input that distinguishes the cases, run it through the tool that owns the format, and read the answer off the output. A schematic with labelled wire stubs at the compass points reports where a pin ended up. One with a tap per bus member reports which members crossed.
What reading cannot see ¶
10. Reading the code is not running it. Three passes through one read path did not reveal that no
producer fills SourceDoc.title as its contract specifies. Opening the app and looking at the field
showed it immediately, and invalidated part of a PR merged an hour earlier. Review catches a layer that
is wrong. It does not catch layers that are each internally consistent and collectively wrong, because
every file reads fine on its own.
11. A claim about LAYOUT needs a paint-level check, not a rectangle comparison.
getBoundingClientRect knows nothing about an ancestor clipping the box, so a child of a scrolling cell
reports coordinates far outside that cell while being perfectly contained on screen. Reading those
numbers as “it still overflows” is wrong in the safe direction. Ask the document instead, since
elementFromPoint inside the NEIGHBOURING element returns whatever actually paints there. An A/B that flips the rules on a live page has to re-measure in each layout, since restoring the
old rules reflows the row and moves the element you were probing.
When the world moved underneath ¶
12. When a run contradicts your PREDICTION, the contradiction is the finding. A test was written to
prove that a malformed project.yaml fails the run. It came back saying the run had SUCCEEDED.
Chasing the gap, rather than blaming the fixture, showed that the descriptor was never reachable at
all. Agni issue 312 was really about that unreachable descriptor, and not about the two call sites it
named.
13. A long-lived ticket’s PREMISE erodes silently. Three substantial issues in one month had aged out before anyone picked them up. One had mostly shipped already. One assumed a convention that the only real boards contradicted. One had been done in pieces under other work. Nothing was wrong with any of them when filed. Adjacent work moved underneath while the ticket text kept asserting the old world. Read the comment thread as well as the body.
14. A ratio needs its DENOMINATOR constrained, not just its numerator. A query counted the passives with exactly one probe-covered net. The number was plausible, within a few percent of an independent tool’s, and the query read correctly in English. It was wrong. The query constrained how many nets were COVERED and never how many nets the part HAD. So it also matched parts with only one net, where that net was covered, and multi-pin arrays with one net of four covered. Two opposite errors came from one missing clause.
Nothing internal could have caught it, because the query returns rows, the rows are all real parts, and the count is stable across runs. Only a second tool’s answer falsified it. The general form reaches past coverage. Whenever a result is “the things where N of M hold”, a filter on N alone silently admits every row whose M is not what you assumed. State M as a clause even when you believe every subject has the same one.
15. A positive control catches a fixture that satisfies part of a check. A test asserted that a complete directory passes validation, and it failed. The check wants six files, and the fixture listed four of them. Had the fixture been used only in the NEGATIVE test, it would have failed for the missing files and read exactly like the guard working. A fixture that is supposed to pass has to actually pass.
Trusting a test ¶
A new test is a measurement too, and its positive control is the red-check. A red-check neutralises a
behaviour and confirms that the test covering it fails. The cheap way is to stash ONE tracked file
(git stash push -- path/to/file.go, run, pop).
flowchart TB
S["neutralise the behaviour<br/>git stash push -- one/file.go"] --> R{"run the test"}
R -->|"red, naming the defect"| OK["the test is real"]
R -->|"[build failed]"| B["proves nothing, and looks like it did.<br/>The stash took a declaration too.<br/>Revert the BEHAVIOUR in place instead:<br/>flip the branch to if false."]
R -->|"green"| G["the test asserts nothing.<br/>Replace the heuristic with the PROPERTY."]
classDef bad stroke-dasharray: 4 3;
class B,G bad;
A compile error is not a failing assertion, so read the red output before believing it. The green branch has a worked example. A render test was built on a gap heuristic (“the two notes must be at least N apart”), and it passed with the fix removed. Another mechanism had shrunk the fixture enough that the gap held either way. The test now asserts the PROPERTY instead. It adds a marker at the same anchor and asserts that the two render at the same y, and it failed with the actual defect named. Run the red-check on every new test, not only the ones you doubt.
The neutralisation is code too, and it can silently do nothing. A red-check here reported that the
test correctly found no difference, and that read as “the check is decorative”. The edit had not
applied, because the string it replaced was not in the file. So the mutation was a no-op, and the run
measured an untouched tree. If a red-check does not confirm that its own edit applied, it is a green
run wearing a red hat. Assert the substitution (assert old in s, grep -c the new text) before
believing what the suite then says.
Perturb one WIRING POINT at a time when a change spans several. Take a feature wired through a
pass, a loader field, a config plumbing hop and an ordering. It invites one big neutralisation, and
then the suite goes red for reasons nobody separates. Four separate perturbations cost four runs, and
they tell you more than one run can. Neutering the datasheet class pass failed the pass, loader and
CLI tests. Cutting the read option out of Overlay.ReadOptions failed ONLY the CLI test. Replacing
the shared ordering with set order failed ONLY the glyph tests. That pattern maps each wiring point to
the tests guarding it. If perturbing a wiring point fails nothing, that point has no test at all.
A comparison of two files that do not exist reads as a difference. cmp -s a b on two missing
paths exits non-zero, and a script that prints “MOVED” on non-zero then reports a change nobody made.
It happens when a command was supposed to write the files and wrote nothing. agni render --report -o f.svg does that, since it prints the report and ignores -o. So both sides of the comparison were
absent, and the run still exited 0. Stat the inputs before trusting a comparison, or compare content
you have actually read.
Count WHICH tests flip, not merely that the suite went red. Neutering reviewGate.trip() failed 6
of 8 gate tests. The two survivors were the opt-in guard and the flag-parse guard, and neither has any
business depending on trip. “The suite went red” would have been equally true if the wrong six had
failed. Naming which ones should survive costs one sentence of thought.
Ways a test passes while asserting nothing ¶
flowchart LR
G(["a test that passes"]) --> W["...and could not<br/>have failed"]
W --> A["the fixture never creates<br/>the condition"]
W --> B["the probe is in the<br/>wrong place"]
W --> C["the assertion was already true<br/>before the action"]
W --> D["the oracle is the code<br/>under test"]
W --> E["the sweep is empty"]
W --> F["both sides run through<br/>the same helper"]
W --> H["the meaning changed and<br/>the compiler said nothing"]
W --> I["the baseline run did not<br/>build, and you grepped for<br/>matches"]
That last one is about MEASURING a change rather than testing it, and it produces a confident number
out of nothing. You can compare a branch against main by stashing one file, but that gives a
baseline only if the tree still compiles. A test file elsewhere may reference a symbol defined in the
stashed file, and then the build breaks. A pipeline that greps the output for differences then
reports none. Three such comparisons in a row came back clean and were all empty. Build the baseline
in a git worktree at the commit you mean, where nothing can half-apply, and check that the run
produced output at all before reading it.
| The shape | The case that taught it | What makes it real |
|---|---|---|
| The fixture never creates the condition | Two versions of one browser test went green with the containment CSS deleted. The public fixtures top out at three sheets, so with short names nothing overflowed whatever the rules said. | Squeeze the column to its 48px minimum by dragging its own grip. |
| The probe is in the wrong place | The second version still passed, because it probed the neighbouring cell’s CENTRE. The escaping chip reached 66px past its own cell while the neighbour was 301px wide, so the probe sat 84px clear of the bleed. | Bleed arrives at the BOUNDARY and fades. Probe just inside the edge nearest the offender. |
innerText does not mean visible |
An assertion written expect(await locator.innerText()).toBe("...") passed with display: none on the element, because the spec falls back to textContent for a node that is not rendered. The comment above it claimed it tested visibility. |
Assert a non-zero boundingBox(), or read the computed style. |
| The assertion was already true before the action | A composition test clicks a finding and asserts expect(called).toContain("HighlightSheet"). It passes with the handler unwired, because deep-link restore highlights during boot, so it asserts that the page booted. |
Count across the action (before and after filtered lengths) or clear the log first. Every “did X happen” assertion over a running system has this shape. |
| The oracle is the code under test | if skipRefDes(x) { t.Error(...) } went green under a deliberately broken skipRefDes, while its siblings written against literals went red. The test and the code agree by construction. |
Assert against literals, or against a set the production path produced. |
| The round trip goes through the forgiving half | TestWriteRoundTripsIR read an EDIF file, wrote it, read it back and compared, and was still green through four defects that made the output unreadable to anyone else. Our reader resolves references after parsing the whole file, takes any token as an identifier, and guesses a port reference is a pin designator when no mapping says otherwise. The writer leaned on all three, and the test could not see it because the same reader closed the loop. |
Assert the emitted TEXT, or bring in an implementation that did not grow up with yours. Both, if the format is one other tools read. |
| The sweep is empty | A catalog-wide test iterating “every rule that sets this field” passes trivially when no rule sets it. | Count the rules it asserted over and fail at zero. A positive control belongs IN the test, not beside it. |
| The fixture encodes the rule’s own assumption | Two i2c-pull-up fixtures gave their “pull-up” resistor exactly one net, no second end and no rail, so a rule correctly requiring a rail turned them red. |
Complete the fixture. Loosening the check until it passes again looks identical from inside the failing run. |
| The counter-example survives for the wrong reason | A considered-set test asserted that a NO_CONNECT pin was OUT of a rule’s domain, and still could not tell a supply-scoped domain from one sweeping in every pin on the part, because the NC pin sat on no net and dropped out of both. | A fixture case differing from the failing one in EXACTLY the property under test, here a signal pin alone on its own net. |
These five need their own room rather than a row.
1. A test can compare a function against itself. A parity test held a rule’s Eval to its verdict
projection, VerdictsToFindings(Eval(m)) against Eval(m). For a converted rule those are the same
call, so it ran one function against itself and would have passed for any projection including a
broken one. It now rebuilds the expectation independently, keeping the failing verdicts and taking
their findings. The tell is structural rather than about the assertion. If both sides of an
equality flow through the same helper, the helper is untested no matter how the assertion reads.
2. When two types carry the same field names, the compiler cannot help you. check.Verdict and
check.Finding both have Subject, Kind, Pin and NetID, so when Rule.Eval moved from
returning findings to returning verdicts, every
for _, f := range rule.Eval(m) { got[f.Subject] = true }
kept building and quietly started counting passes as failures. Two tests were already wrong this way
and passed only by accident, because their rules were still wrapped in check.FailuresOnly. To
find this, take each call site that the compiler did NOT complain about, and ask whether its meaning
survived. Use Rule.Findings(ctx, m) where a test means violations.
3. scrollTop is always 0 under jsdom, so a scroll assertion passes with the bug present. jsdom has
no layout engine, so nothing scrolls and nothing has a size. A panel that threw the reader back to the
top on every click would have satisfied expect(el.scrollTop).toBe(prev) perfectly. Assert the CAUSE
instead. Here the cause is that the rows were recreated. Node identity across the action
(expect(after[i]).toBe(before[i])) is visible in jsdom, and it goes red on the real defect. A
symptom needing a browser often has a cause that does not, and the cause is the better assertion
anyway because it names why.
4. A test that SKIPS asserts nothing, and reads like a pass. A browser case was written with
if (findings.length !== 1) t.Skip(...), to skip when the fixture did not produce the state it
needed. It reported PASS in the suite output and had never executed its assertions. Skips are for a
genuinely absent capability (no browser on the machine). They are never for a fixture that did not
produce the state you wanted. That is a setup bug, and skipping hides it behind the same green tick
as a real pass. Find the input that reaches the branch, or the test does not exist.
5. A -run filter that misses the test you care about still prints ok. go test -run "Supersed"
ran three tests in cmd/agni and passed all three, so the package reported ok. It never ran
TestCheckReportsAProjectsOwnSupersessions, because the filter is an unanchored regexp over the test
name and Supersessions carries Superses rather than Supersed. The zero-match case is the safe
one, since Go says ok ... [no tests to run] when a pattern matches nothing at all. A PARTIAL match
lies, because the passes it reports are real and belong to other tests. Run a new test by its exact
name once and read the --- PASS line carrying that name, before trusting any filtered run that
claims to include it.
An out-of-tree oracle, for a format someone else reads ¶
A round trip proves the two halves of ONE implementation agree. That is worth having, but it is not portability. Agni issue 580 sat in the gap between the two claims. It had four defects that the round trip could not see, and each was fatal to a reader not written alongside our writer.
Two oracles have been worth running, and both are free.
kicad-cli answers questions about what a KiCad file MEANS. Export the netlist, read the (net ...)
blocks, compare which pins share a net. The technique is to ask rather than reason. Build a fixture
that isolates exactly the question, and let the answer decide the code. Twice now the answer has been
the opposite of the obvious one. A vector bus crossing a sheet boundary pairs its members by bit
POSITION, so a child’s bit 0 joins the parent’s A0 whatever the parent’s range is spelled. A group
bus pairs by member NAME, so two buses of equal width with different member names join nothing at
all. Guessing either would have shorted unrelated signals while moving the net count the flattering
way. readers/kicad/testdata/*.oracle are those answers, committed beside their fixtures with the
regenerate command in each header, so the expected result is not one we chose.
GNU Electric answers whether a file we WROTE is one another tool can read. Its EDIF reader is an independent implementation with source bundled in the jar. Its batch mode asserts before reaching the importer, so drive it through BeanShell, which runs inside the initialized database:
curl -O https://ftp.gnu.org/gnu/electric/electric-9.08.1.jar
curl -o bsh.jar https://repo1.maven.org/maven2/org/apache-extras/beanshell/bsh/2.0b6/bsh-2.0b6.jar
java -Dagni.edif=<file>.edn -classpath electric-9.08.1.jar:bsh.jar \
com.sun.electric.Electric -batch -s check.bsh
where check.bsh reaches the importer by reflection on EDIF$EDIFPreferences.doInput and prints the
cell, node and arc counts. Passing -s removes the script from the argument list, and so avoids the
batch-mode assertion. Electric prints a bogus 0 errors summary after printing every Error, line,
so count the lines rather than trusting the total.
Neither belongs in the gate, since one needs a 19MB corpus and the other a 23MB Java download. The
gate holds a test over the emitted text instead, readers/formats/e2e_edif_conformance_test.go. It
asserts each PROPERTY the out-of-tree oracle established. The oracle says which properties are the
right ones; the committed test keeps them true.
Read a foreign tool’s diagnostics as evidence about the FILE and not about the standard. Electric takes a cell rename’s display string as the cell’s name where ours takes the identifier, and reads exactly one token as a design’s name where the grammar allows a rename. The first is a real disagreement worth conceding, since a name it rejects is a file nobody can load. The second costs nothing. Loading the same file both ways gave identical counts, and conceding it would have meant discarding the design’s name.
Text you cannot match literally ¶
| Source | What moves | Match with |
|---|---|---|
prototext |
an unstable extra space after a colon, inserted ON PURPOSE to discourage byte-comparison | : +, or parse the proto |
protojson |
one or two spaces after a colon, so two runs over one message differ | parsed values, or strings.Fields joined by a single space |
| symbol text out of a doc-IR | flattened subscripts, so VCCA arrives as V CCA (~850 occurrences in one corpus) |
a pattern tolerating the injected space |
html/template output |
quotes, angle brackets and ampersands become entities, so "gateway" renders as "gateway" |
a fragment carrying none of them, or unescape before comparing |
One before/after table was built with a one-space prototext pattern. It read ZERO for every
“before” and was nearly shipped. The number itself was not the tell. The tell was an internal
contradiction, a file showing 0 typed pins and 2 supply pins at once. Distrust any count whose parts
do not add up. The injected space has bitten three times in unrelated places: a prose sweep, the
derive pin path where it would have produced pin ids no symbol library could match, and in-document
search.
Under EmitUnpopulated, a newly added field appears as its zero value in every existing
consumer’s output. Adding one to a response message is additive on the wire and visible in the JSON, so
“no data changed” and “byte-identical output” are different claims and only the first survives.
When the escaping row fails, the failure points the wrong way. Say a test asserts that a rendered
page carries a sentence. It fails when the sentence contains a quote, and it fails by reporting the
sentence absent. That reads as the feature not working, rather than as the assertion being
unmatchable. So the debugging pass goes to the renderer, which is correct, instead of to the test.
Assert on a fragment with no quotes in it, or unescape first. go test printing FAIL for a build
failure belongs to the same family. In both, the mechanism reports the wrong thing as broken.