Cumulative coverage and test gain
This page extends code coverage. It assumes you can
already bind, run and ingest a window for Go or LLVM, and read documents with
reflow coverage documents|gaps|lines|diff. To decide which gaps deserve a
test and when to stop, see
when to stop adding tests.
Before and after a new test
Section titled “Before and after a new test”Coverage is process-wide: nothing attributes lines to a single flow. To learn what one new test adds without running the existing suite again, run only the new test in its own window, then compare two cumulative documents built from the same frozen binary: one from the baseline windows’ raw files, and one from those files plus the new window’s.
- Record a baseline: run the existing tests in one or more run windows on one frozen instrumented binary. Keep every window’s cover directory unchanged after ingest, with its expected process count and apply keys. Build the baseline cumulative document from them and note its ID.
- Find candidates:
gaps --document <baseline>lists missed ranges. Read them withlines --with-sourceand pick reachable behaviour that no existing test asserts: an error path, an empty state, a boundary, a state transition. - Write the test. Record its purpose (the behaviour and risk) and its oracle (where the expected values come from) before you look at the delta.
- Run only the new test once, in its own window: a fresh, empty cover directory for each producer, bound to the same frozen binaries (do not rebuild or re-sign them), with the same exclusions. Keep that directory too.
- Build a new cumulative document from the baseline windows’ raw files plus
the new window’s, then compare:
diff --document <new cumulative> --base <baseline cumulative> --json. When the pair islikeForLike, the delta comes from the new test’s processes.
A diff between documents of different scenarios (for example one test’s window
against another’s) is a scenario comparison. It can even be likeForLike, but
it is never a test gain.
diff classifies the pair:
likeForLike: same format, producer, toolchain, binary, mode, instrumented set, exclusions, tree and coverable code.gainreports the hit delta per metric for the whole comparison, plus newly hit and newly missed ranges per file on the current page (pageCompleteis false while more pages follow). A net line delta of 0 can still hide lines that moved: checknewlyHitandnewlyMissed.descriptive: something other than tests changed (reasonssuch astree_changed,toolchain_changed,binary_changedorexclude_changed). Totals are shown,gainisnull, and the change must not be reported as test gain. To get a like-for-like pair, use the same instrumented binaries from the same commit on both sides; a rebuild can change the binary (binary_changed) even from the same source.- Documents from different formats or producers are refused
(
format_mismatch,producer_kind_mismatch).
Cumulative documents from saved windows
Section titled “Cumulative documents from saved windows”There is no command that merges stored documents. Each ingest makes one
document, and documents are never combined afterwards. Ingest does merge every
raw file in its cover directory, so you can assemble one cumulative document
per language from windows you already ran, with bind and ingest only and
without running anything again. It is not a separate feature: record how each
cumulative document was assembled.
Requirements
Section titled “Requirements”- One frozen binary. Every included window ran the exact same executable:
the same file and SHA-256, never a rebuild or re-sign. Bind that file;
ingest refuses
binary_changed. - The source it was built from. Run
bindandingestfrom a clean checkout of the commit the binary was built from. Go’sbindchecks the binary’s VCS stamp againstHEAD. LLVM binaries carry no stamp, so the source binding is whatever checkout you bind: choose the right commit. - The same scope as the documents you compare. Use the same
--collection, the same--excludeglobs and, for LLVM, the same--path-strip. - One fresh directory per language. Bind a new, empty directory outside
the worktree, then copy the raw files in (
bindrefuses a directory that is not empty). Go and LLVM files never share a directory or a document.
Process count and batch keys
Section titled “Process count and batch keys”--processesis the sum of each included window’s expected processes for that language, taken from what you launched (launch records, step states), never from the files you copied. Never lower it to match the files.- Pass every included window’s apply keys with
--batch: at most 64 keys, each with at most 1,000 runs (batch_too_largeotherwise). Each key must have runs in the collection you pass (unknown_batchotherwise). A flow that ran in several windows counts once indistinctFlows. - If a window is missing a file it should have, leave that whole window out (its files, its share of the count and its keys) and say so, or stop. Never keep its keys while dropping its missing process.
cd <clean checkout at the binary's commit>reflow coverage bind --binary <frozen Go binary> --cover-dir <new-dir> \ --collection <id> [--exclude <glob>]... --jsoncp -p <window-1-dir>/covmeta.* <new-dir>/cp -p <window-dir>/covcounters.* <new-dir>/ # every included windowreflow coverage ingest --cover-dir <new-dir> --processes <N> \ --batch <key-1> --batch <key-2> --collection <id> --json- Copy exactly one
covmeta.*file. For one binary it is the same file in every window; compare them withcmpfirst, and stop if they differ. - Copy the
covcounters.*files with their names unchanged. Do not copyreflow-start.*markers ortmp.*files. - Counter file names carry the process ID. If two windows reused the same
process ID, ingest refuses
duplicate_pid. Do not rename the files to get around it: split those windows into separate cumulative documents, or report the limitation. - The document is exit-only: copied files cannot be verified at process start.
LLVM (Swift and other languages)
Section titled “LLVM (Swift and other languages)”cd <clean checkout at the commit the binary was built from>reflow coverage bind --format llvm-cov --binary <frozen Mach-O or ELF> \ --cover-dir <new-dir> --path-strip <same prefix> --collection <id> --jsoncp -p <window-dir>/<name>.profraw <new-dir>/<window>-<name>.profraw # every profilereflow coverage ingest --format llvm-cov --cover-dir <new-dir> --processes <N> \ --batch <key-1> --batch <key-2> --collection <id> --json- Process IDs repeat across windows, so prefix each copied file name with its
window. Names may use letters, digits and
._%+-and must end in.profraw. Renaming changes only the name, never the contents. - Ingest refuses any other file, a count other than
N(profile_count_mismatch), and byte-identical profiles (profile_duplicate), even when they came from different processes. Copy each profile once; if two genuine profiles are identical, split the scope or report it. - Reflow does not check that a profile came from the bound binary;
llvm-covdrops what does not match. Copy only profiles written by launches of that exact executable.
Reading and labelling the result
Section titled “Reading and labelling the result”- Read it like any document:
gaps,files,lines, anddiff --document <new cumulative> --base <previous cumulative>. With one frozen binary, one checkout and the same excludes, the pair islikeForLike; anything else isdescriptiveand never a test gain. - Go and LLVM stay separate documents and separate diffs. Never blend them.
- Keep a record of each cumulative document: the source directories, the apply keys and the expected-process sums it was built from, and any window left out and why.