Coverage
unit-coverage is the second rung of the unit ladder: does the test run the code? This page explains why the floor is a strict 100% rather than a comfortable 85, and how the same floor is measured over a pull request's changed lines.
Why a 100% floor
A coverage floor below 100 hides a permanent, unexamined gap. At fail_under = 85, fifteen percent of the code is uncovered and nobody has said which fifteen or why — the number stays green while the uncovered region drifts to wherever tests are hardest to write, which is exactly where the bugs are. The floor rots silently, because a regression from 91 to 87 still passes.
The standard inverts this: the floor is 100% of what you didn't explicitly exempt. Code that genuinely shouldn't count — a version-conditional import, a defensive branch — is excluded by a reason-required, line-scoped exemption rather than by slack in the number. The result is that every uncovered line is either covered, or named with a reason, and the whole omission surface is auditable in one file.
What it enforces
The floor applies to the unit suite only — test files are excluded from the denominator, and exempted files/lines are lifted from it. The exact keys and defaults are in the configuration reference.
- Python — the suite runs under
coverage.pywith branch coverage on; the combined line + branch total meetsfail_under. A source with no branching constructs measures zero branches, which is vacuously full branch coverage — the total reduces to the line percent.pytestcollects only the scanned path, with the standard suite tiers undertests/ignored, so the integration tier stays out of the unit lane the same way it stays out for TypeScript and Rust. - TypeScript — the suite runs under
vitestv8 coverage; four independent metrics (lines, branches, functions, statements) each meet their floor, because line coverage can read 100% while a branch lags. - Rust — the suite runs under
cargo llvm-cov --lib --bins, so the floor measures the same unit-only slice the other languages measure (the integration tier undertests/stays out of the number) across both the library and the binary targets — a binary's colocated tests are units too. The default floors lines only; three finer metrics are opt-in:regionsis a Rust-only sub-line metric with a harsher bar and no cross-language analog,functionsmirrors TypeScript's, andbranchinstruments on a nightly toolchain, which the crate pins in its ownrust-toolchain.toml(withllvm-tools-preview) — the coverage run reads that pin, in CI and locally alike (the config reference has the exact keys). Keeping them opt-in keeps Rust's default floor line-shaped like Python's — the parity call, with the asymmetry named here.
Where each measurement runs
Each coverage run anchors where the consumer's own test run anchors, so the check measures the suite under the same configuration your own runs use:
- TypeScript — vitest runs at the scanned path and resolves its configuration with its own upward search, so the package-root
vitest.config.*that governs your ownvitest rungoverns the check's run the same way: environment, setup files (resolved beside the config file), aliases, and plugins all apply. The check owns the measurement itself: its flags set the coverage scope (the scanned path's sources are the denominator, and the package's suite tiers undertests/stay out of the run), its floors come from yourtesting-conventions.tomlconfig, and the run clears the config file's own global coverage thresholds — the check's floors decide, and your config file is left untouched (athresholds.autoUpdatenever rewrites it during a check run). - Python — coverage.py runs at the scanned path, and pytest resolves its rootdir and configuration with its own upward search — so a package-root
[tool.pytest.ini_options]/pytest.iniand theconftest.pyfiles below it apply to the check's run exactly as to your ownpytestrun. The measurement itself is owned by the check — the scanned path's sources are the denominator (--source), branch coverage on, test files and exempted paths omitted, floors from yourtesting-conventions.tomlconfig — so a.coveragerc/[tool.coverage]table is deliberately not consulted: the check holds the same keys it overrides in a vitest config, and the denominator is the scanned path's sources, addressing the same paths every other check uses. Scoping to the scanned path keeps an editable path dependency's source tree out of the number — a normal install lands insite-packagesand is ignored either way, but an editable install resolves through a.pthto a tree that is not undersite-packages, which coverage.py's default would otherwise pick up. (# pragma: no coveris coverage.py's built-in default and applies without configuration.)pytest's collection is scoped the same way: atests/directory directly beneath the scanned path is ignored, so a package whose scanned path is its package root never collects its owntests/integration/ortests/e2e/into the unit run. - Rust —
cargo llvm-cov --lib --binsanchors at the crate root by construction: cargo resolves the manifest upward from the scanned path, and.cargo/config.toml/rust-toolchain.tomldiscovery is cargo's and rustup's own, exactly as in your owncargo test.
The changed-line floor
On pull requests, the same configured floor is also measured over only the lines the <base>...HEAD diff added or modified. The thresholds stay the single source of truth; the diff-scoped job only changes what they're measured over. So a one-line change below the floor fails however small the diff is, an added file's new lines are subjects (brand-new code must be covered too), and a change touching no measured line passes vacuously.
Because the diff is judged against the configured floor rather than an implicit 100%, the two coincide only at a 100 floor — one more reason the strict default earns its keep: with it, every changed line is exercised, on every pull request.
What coverage can't tell you
A covered line is a line that executed — nothing more. A test that calls the function and asserts nothing covers every line it touches. That gap is the next rung's job: mutation breaks the covered line and requires a test to notice.