Documentation
Supported languages and test suites
Check the detail available from each runner and its measurement limits.
Supercov supports JavaScript, TypeScript, Rust, Python, Ruby, Go, Java, and Kotlin today. Start with the same test command the repository already uses; Supercov detects supported runners inside that command.
npx supercov -- npm test
npx supercov -- npx playwright test
supercov -- cargo test
uvx --from supercov-cli supercov -- pytest
supercov -- rspec
npx supercov -- go test ./...
npx supercov -- mvn testLanguage support
| Language | Status | Start with |
|---|---|---|
| JavaScript | Available | npx supercov -- npm test |
| TypeScript | Available | npx supercov -- npm test |
| Rust | Available | supercov -- cargo test |
| Python | Available | uvx --from supercov-cli supercov -- pytest |
| Ruby | Available | supercov -- rspec |
| Go | Available | npx supercov -- go test ./... |
| Java | Available | npx supercov -- mvn test |
| Kotlin | Available | npx supercov -- ./gradlew test |
| Zig | Coming soon | — |
| PHP | Coming soon | — |
| C | Coming soon | — |
The npm-distributed CLI requires Node.js 22 or newer for every language.
What exact and aggregate mean
Exact attribution means Supercov knows which test, attempt, retry, and
runner produced the coverage. Queries such as test, passed, and failed
can use that identity.
Aggregate coverage means Supercov knows the source executed but cannot
truthfully assign it to one test. Whole-run gaps and file queries still
work; per-test questions are limited.
Supercov reports the level it actually observed. It does not guess.
JavaScript and TypeScript
| Runner | Attribution |
|---|---|
| Playwright | Exact per test, worker, retry, outcome, action, and assertion phase |
| Vitest | Exact per test, with setup execution kept separate; Browser Mode included |
| Jest | Exact per test, including parameterized tests, with the user’s own configuration, setup files and reporters kept |
node:test | Exact per test |
| Mocha | Exact per test and retry, --parallel included, with setup execution kept separate |
| AVA | Exact per test, concurrent tests included, in worker threads or child processes |
| node-tap 15 and newer | Exact per subtest, nested and parallel (t.jobs) subtests included |
| Other Node-based runners | Aggregate when their processes remain visible to Supercov |
| Browser component runners without an adapter | Aggregate structural coverage |
One command may launch several runners. Supercov combines their evidence into one run and preserves runner identity wherever the runner exposes it.
Vitest Browser Mode
Vitest Browser Mode runs the test file in a real browser. Supercov measures it per test like any other Vitest run: lines, branches, MC/DC and assertion coverage. Combined Node/browser projects are configured after Vitest resolves inline definitions, config-file references, globs and project selection. Evidence travels over Vitest’s own browser command channel; the compatibility suite exercises Chromium through the Playwright provider.
Component code is instrumented the same way as any other source, with one
addition that matters most here. A JSX tree is a single statement, so an
expression rendered inside it – aria-label={label(state)}, a child
{formatted(value)} – is measured on its own rather than counted as covered
because the component rendered once. This is also what lets a UI assertion be
explained precisely: toHaveAccessibleName names the attribute and
toHaveTextContent names the child, and each is credited separately.
Expressions that cannot independently fail to evaluate do not become
obligations. {value} is reached exactly when the tree is, and
onClick={() => save()} is measured where the handler is called rather than
where it is created.
React and React Native
React components can use Testing Library with Vitest/jsdom, Vitest Browser Mode, or Babel/Jest. React Native and Expo component tests run through their existing Jest presets, including native mocks. This is JavaScript component-test coverage: it does not measure Hermes, native modules, simulator/device execution, Detox or Maestro. React SSR hydration is exercised in jsdom and Chromium; this does not establish Next.js Server Components, streaming SSR or server actions.
Assertion coverage treats a JSX expression as a statement: it asks whether a test would notice the displayed value, accessible name, disabled state or error message becoming undefined. A button being rendered does not establish that its disabled state was checked. Keep your existing runner and matchers.
The React verification example shows a fully executed checkout whose weak tests accept four UI regressions, then adds four precise assertions and independently checks the broken copies. The agent-authored maps credit only those four UI expressions.
Builds and source formats
JavaScript and TypeScript projects may use Vite, Next, Turbopack, Webpack,
esbuild, SWC, tsc, or no build step. ESM, CommonJS, JavaScript, JSX,
TypeScript, and TSX are supported.
Supercov instruments an isolated copy. It does not ask you to add an import, reporter, plugin, or alternate build output.
If tests import compiled output such as dist/ or launch a script that uses it,
Supercov runs the project’s build inside the isolated copy before testing.
Keep using your normal test and build commands. Instrumentation does not
require changing the project’s TypeScript settings.
Browsers, servers, and child processes
Playwright support includes Chromium, Firefox, and WebKit, along with pages, frames, popups, workers, request contexts, WebSockets, and test-launched child processes where the runner exposes their identity.
Browsers a suite launches itself are covered too. A fixture that calls
chromium.launchPersistentContext, or launch/connect and hands out its own
contexts and pages in place of Playwright’s page fixture, is adopted by each
test’s collector: its pages are read before the fixture closes them, and a
context kept for the whole worker follows the current test’s identity. Actions
on such pages are not recorded as separate phases, so their evidence is
attributed to the test and its assertions rather than to individual clicks.
Node child processes inherit coverage automatically. Long-running servers get a short drain window after the test command finishes so buffered evidence can arrive. Work without a reliable test identity is kept as background coverage instead of being assigned to an arbitrary test.
A child a test stops in teardown keeps the coverage it produced. Buffered
evidence is written when a terminating signal arrives—SIGTERM, SIGINT,
SIGHUP—not only when the process exits on its own, so killing a gateway
after the request it served does not lose the request. The program’s own
signal handling is untouched: a process with no handler still dies from the
signal exactly as it would unmeasured, and one with its own handler keeps
it. SIGKILL cannot be caught by anything and is the one stop that loses
whatever was still buffered.
A program that hands its process over to another with exec keeps what it
ran: a cache front end that restores a key and then process.execves the
runner’s Node, or a Ruby script that calls exec, writes its evidence before
the new program starts. Python and Rust write as they run, so an exec
loses nothing there either.
Rust
| Runner | Attribution | Current requirement |
|---|---|---|
| Cargo’s standard libtest runner | Exact test, attempt, and passing-assertion identity | Rust 1.95; run with supercov -- cargo test |
| rustdoc doctests | Exact doctest identity; every doctest runs in a process of its own | Rust 1.95; part of supercov -- cargo test |
| cargo-nextest | Exact test, attempt, retry, and binary identity | cargo-nextest 0.9.138 or newer (tested with 0.9.140 and 0.9.148) |
Supercov preserves Cargo’s test selection, scheduling, fail-fast behavior, environment and exit status. Doctests run with their own identities; nextest retries remain separate attempts.
Measured source follows the modules rustc compiles, including #[path] and
literal include! calls. Undeclared .rs files are not treated as application
modules. Statements, functions, branches, boolean decisions, loops and error
propagation are measured. Const contexts and macro expansions remain visible
with explicit measurement limitations.
Test code is not measured, as cargo-llvm-cov and cargo-tarpaulin leave it out:
files under a tests, examples or benches directory, files named
tests.rs, *_tests.rs or *-tests.rs, everything a test target or a
#[cfg(test)] module declares, and items marked #[cfg(test)], #[test] or
#[bench]. Its assertions still mark which evidence a passing assertion
witnessed.
Neither is code the compiler never builds: a #[cfg(windows)] function on
Linux, an item or statement behind a feature the build leaves off, or one
behind a build script’s --cfg that is not set. The target settles unix,
windows and target_* before the run; every other predicate is settled by
the build itself, as each crate’s probes record what cfg! says of its gated
code. Code no build of the run compiled is left out and the run names it
(N item(s) behind a #[cfg] no build compiled are outside the denominator);
code of a crate no test reached stays counted, and the run says that too.
Use the repository’s normal flags after the wrapped command:
supercov -- cargo test --workspace
supercov -- cargo nextest run --workspace
cross is not supported yet. Unsupported command shapes fail with an
explanation instead of silently falling back to plausible but inaccurate
attribution.
Rust compiled to wasm in JavaScript tests
A crate wasm-bindgen exposes to JavaScript (a cdylib that depends on
wasm-bindgen) is measured when JavaScript tests load it: a Node suite
(Vitest, Jest, node:test, Mocha) through the nodejs or bundler glue, or a
Playwright suite whose page loads the web glue. Build the wasm in the test
command, as the suite already does when it tests the crate it ships
(wasm-pack build && vitest, or cargo build --target wasm32-unknown-unknown
and wasm-bindgen), so the module the tests load is built from Supercov’s
copy:
supercov -- npm test
supercov -- npx playwright test
The JavaScript runtime remembers each wasm instance, in the test process and
in each page, and reads what the module ran at every test’s end, so each test
is credited exactly with its own lines and decisions. The crate’s coverage is
a Rust run beside the JavaScript run. What the copy built (target/, and the
generated pkg/ with its instrumented wasm) stays in the copy.
A Rust Worker under browser tests
A Playwright suite that starts a Cloudflare Worker written in Rust with
wrangler dev, built by worker-build, measures the Worker:
supercov -- npx playwright test
The crate is rewritten with probes in the copy the suite runs in, and
wrangler dev builds it there as usual. The copy’s wrangler configuration
(wrangler.toml, wrangler.json or wrangler.jsonc) points main at a
small entry of Supercov’s that remembers the Worker’s wasm instances and then
is your entry, and gives [dev] an inspector port unless you set one; your
own configuration and command are not changed. While the suite runs,
Supercov reads what the Worker ran through that inspector every 5 ms, and
each read is credited to the browser test that was running then, if only one
was, as for a Go server. Several Workers
in one wrangler dev (-c a/wrangler.toml -c b/wrangler.toml) are each
measured: wrangler’s inspector shows one of them, so Supercov reads each
through workerd’s own inspector, whose address a small preload in wrangler’s
Node process reports. The Worker’s
coverage is a Rust run of its own, published as the run or beside the browser
run. Requests made before the first test, such as the readiness check
webServer waits on, are the run’s and no test’s.
The build runs with --cap-lints=warn, so a crate that denies warnings still
builds with the probes in it.
A Cloudflare Worker in Rust that cargo test starts
Integration tests that start wrangler dev themselves – to test a Cloudflare
Worker written in Rust in workerd, Cloudflare’s own runtime – measure the
Worker too, Durable Objects included:
supercov -- cargo test
supercov -- cargo nextest run
The Worker crate is instrumented with the rest of the workspace, and wrangler dev builds it in the copy as usual. The tests find a wrangler of
Supercov’s first on PATH: for wrangler dev it picks an inspector port,
starts a reader of the Worker through it, and then runs your wrangler with
the test’s own arguments, process and signals. What the Worker ran is
credited to the test that started it, exactly, as its own coverage. This
needs a wrangler on PATH when the run starts; without one, nothing is
put in front of it.
A Worker crate’s code is what worker-build compiles: for wasm, as a release
unless its [build] command passes --dev, never as a test, with the
features the command and the crate’s defaults enable. Code a
#[cfg(target_arch = "wasm32")], a feature or debug_assertions keeps out
of that build is not counted, and wasm-only code is, whatever target
cargo test builds the rest for. The crate counts whether or not
cargo test compiles it (cargo test -p host does not) and whether or not
a test started its Worker, so every run of the workspace has one
denominator. Where no test of a run started a Worker, its files are not
measured, rather than read as code the tests missed: they leave the run’s
totals, the file view, files and gaps say why, scope lists them as left
out, the run’s measurement reads incomplete, the run names them after the
tests’ output, and runs check refuses to judge a floor over a run that left
them unmeasured.
wrangler dev --remote runs the Worker on Cloudflare’s edge, and wrangler
opens no inspector into it there, so nothing can read what it ran. A test
that starts it that way runs as usual; the Worker’s files then say they ran
remotely and could not be read. Start the Worker with wrangler dev to
measure it.
Code in a Durable Object (#[durable_object] and its impl DurableObject)
is measured like the rest of the Worker and credited to the test whose
requests reached it. So is a Worker that runs beside another in one
wrangler dev (-c plane/wrangler.toml -c runners/wrangler.toml), reached
only through the first’s service bindings: wrangler’s inspector shows one
Worker of a session, and Supercov reads each through workerd’s own, whose
address a small preload in wrangler’s Node process reports. A test that
sets its own NODE_OPTIONS keeps them; the preload is added after. A workspace with several Worker crates counts each crate
once, and runs of different tests – each starting different Workers, or
none – merge with supercov merge. A crate the Worker is built with, such
as a core library the native tests call too, is measured inside the Worker:
its probes run in the Worker’s wasm and are read with the Worker’s, into the
same file as its native tests, each test credited what it reached either
way. Tests that run a Worker only when its build/ exists, as a test
kit that will not start an unbuilt Worker does, run it: a build/ you made
beforehand and keep out of git is in the copy, and wrangler dev builds the
crate with probes over it there.
What a project needs:
wrangleronPATHin the job that runs Supercov, before the run starts, such asnode_modules/.binof a project that installs it.- The
wasm32-unknown-unknowntarget andworker-build, which the Worker’s[build]command runs. - The Worker crates inside the source Supercov measures, as workspace members or under the project root.
The project needs no cfg flag or build step of its own for Supercov.
What a test harness can rely on
- Any configuration, anywhere. A test may write its own wrangler
configuration, per test, outside the crate, with a
mainof its own: a module that loads the built Worker and adds stand-ins or test controls. Supercov leaves every configuration as it was written. When a test runswrangler dev, Supercov’swranglerhands it a copy of the configuration it was given (-c, or the one wrangler finds from its directory), beside the original as.supercov-wrangler-<pid>-<n>.toml(or.json), whosemainis a small entry that remembers the Worker’s wasm instances and then loads yourmain. The copy is removed once thatwranglerexits. A Worker is read when wasm built from the crate’s measured source runs in it, whatever the configuration’smainimports. - The measured build. The configuration’s
[build]builds the crate with worker-build (anycwd), or itsmainimports a build made in the run’s copy. Abuild/made before the run, without probes, has nothing to read. Supercov builds nothing itself: as a JavaScript suite builds the wasm it loads, build the Worker in the measured command or in the tests. - Why a Worker was not measured. Where tests started
wrangler devand no measured build ran in it, the Worker’s files name eachwrangler dev -c <path>the tests started, and why a configuration was left as it was (nomain, say), in the file view,scopeand the run’s output. --inspector-port. A test may pass its own: Supercov reads the Worker through that port.- Knowing the run is measured.
SUPERCOV_RUN_IDis set in every process a measured command starts, holding the run’s id. The otherSUPERCOV_variables a test may see are Supercov’s own wiring and change without notice. - Output.
cargo test -- --nocaptureshows each test’s output as it is written, as plaincargo testdoes.
The Worker built with probes stays in the copy: its build/ output is never
returned to the project, nor are wrangler’s .wrangler/ state and the
configuration copies above. Writing them does not make the run stale.
Rust tests compiled to wasm
cargo test --target <wasm target> runs each test through the target runner
Cargo is configured with, and so does Supercov, one test per process:
supercov -- cargo test --target wasm32-unknown-unknown
supercov -- cargo test --target wasm32-wasip1
wasm32-unknown-unknownwithwasm-bindgen-test-runner(whatwasm-pack test --nodesets up) runs each test module in Node. Supercov gives Node a hook that writes what the module ran, which lives in its memory, to the run as the test’s process ends.wasm32-wasip1withwasmtimeorwasmeras the runner: Supercov opens the test’s evidence directory to the module (--dir) and names it in the module’s environment (--env), and the module writes what it runs to a file there as it runs.
Each test is credited exactly, assertions included, as for a native test.
wasm-pack test works the same way. Supercov runs wasm-pack once to learn
the cargo test it would run (the runner, the browser driver), then runs
that command test by test:
supercov -- wasm-pack test --node
supercov -- wasm-pack test --headless --chrome
- In headless Chrome, Supercov stands between the runner and
chromedriver, and reads what each test’s module ran from the page before the runner closes it. - Tests run in Firefox, Safari or Edge are run and reported, but not measured yet; the run says so.
wasm-pack testis measured on Linux and macOS. On Windows, run thecargo test --target wasm32-unknown-unknownit runs.- Deno is not supported.
wasm-bindgen-test-runner’s Deno mode (WASM_BINDGEN_USE_DENO=1) does not run on Deno 2.
Python
| Runner | Attribution | Current requirement |
|---|---|---|
| pytest | Exact test, worker, retry, and setup/call/teardown phase identity | CPython 3.9 or newer; run with uvx --from supercov-cli supercov -- pytest or python -m pytest |
| pytest-xdist | Exact per worker | Workers inherit the run through the environment |
| pytest-rerunfailures | Exact per attempt; flaky tests are reported as such | |
python -m unittest | Exact test and setUp/test/tearDown phase identity | Serial in-process; skips and expected failures are recorded; subtest failures roll up to the parent test |
Your project runs in place with its own interpreter and virtual environment. Supercov adds its runner hooks through the process environment and places a probe for every obligation into each measured module as it is imported – nothing on disk changes, and tracebacks keep their line numbers. You do not need to rewrite tests or configure a different build.
Coverage includes statements, functions, boolean decisions, loops,
comprehensions, short-circuit operators, match cases and exception paths.
Child interpreters, threads and thread pools can retain the calling test’s
identity. The report also distinguishes execution before a passing assertion
from later execution. These phase records alone do not prove which values the
assertion checks; assertion coverage measures which executed
statements the pytest and unittest tests would catch breaking.
Interpreters launched with -I, -E or -S ignore the required startup hook
and are not measured. Code compiled from strings at runtime has no source
obligations. A measured module imported before Supercov starts, or compiled
past the import system, runs without probes; the run names it as a limitation
rather than reporting it uncovered. What a test executed survives the process
being killed – os._exit, SIGTERM or SIGKILL – but a corrupt or exhausted
evidence channel fails the run rather than reporting partial data as complete.
uvx --from supercov-cli supercov -- pytest
uvx --from supercov-cli supercov -- python -m pytest -n 4
uvx --from supercov-cli supercov -- uv run pytest
uvx --from supercov-cli supercov -- python -m unittestA Python server under browser tests
A Playwright suite that starts a Python server, through webServer, a fixture
or a test, measures the server when you run it under Supercov:
uvx --from supercov-cli supercov -- npx playwright test
Each request runs as the test that sent it. Supercov’s Playwright runtime names the test in a header on the page’s own requests and the request fixture’s, and in a cookie on the app’s host; the server reads it in the standard library’s HTTP server, in any WSGI server running Flask or Django (gunicorn, waitress, their own development servers) and in any ASGI server running Starlette, FastAPI or Django (uvicorn, hypercorn). A request that names no test, such as a webhook from a mock, is credited by time as it is for a Node server; see the coverage model. A Python process a test starts belongs to that test.
The server’s coverage is a Python run of its own. In a project with no
JavaScript source that run is the run. In one with a Node server or front end
too, the browser run is published as usual and the server’s run beside it.
runs latest is the browser run, and its summary names the server’s run
(companions in JSON); the server’s run names the browser run it took its
tests from. The pull-request comment Action reports each server’s run in a
section of its own. Each run’s figures stand alone, and uvx --from supercov-cli supercov merge
merges each language’s runs separately.
A server the suite runs in its own process works without Playwright: a
pytest-django live_server, Django’s LiveServerTestCase, or a Flask app in
a thread that a Selenium or pytest-playwright suite drives. Its requests name
no test, and each is credited to the test that was running in that process
when it came, and said to be.
The server runs in Supercov’s copy of the project. A virtual environment
inside the project is used as it is, and a package installed editable from the
checkout is measured too. SUPERCOV_SOURCE_ROOTS applies to both languages:
when you set it, list the Python directories as well.
Ruby
| Runner | Attribution | Current requirement |
|---|---|---|
| RSpec | Exact example and before/example/after phase identity | Ruby 3.4 or newer for full measurement; run with supercov -- rspec or bundle exec rspec |
| Minitest (including Minitest::Spec and ActiveSupport::TestCase) | Exact test and setup/test/teardown identity; skips recorded | ruby -Itest ..., rake test, rails test |
| test-unit | Exact test and setup/test/teardown identity; omissions and pendings recorded | ruby -Itest ..., rake test |
| parallel_tests, Rails process workers | Exact per worker process | Workers inherit the run through RUBYOPT; verified on a Rails app with bootsnap, Zeitwerk and two forked workers |
Thread-parallel Minitest (parallelize_me!, parallelize(with: :threads)) | Probe observations exact per test; line, method and simple-branch observations made while phases overlapped go to the run, declared | |
| Cucumber | Exact scenario identity (features/x.feature:LINE), hook steps as setup/teardown | cucumber, bundle exec cucumber |
Every test is credited with every line it runs: each statement of an
instrumented file carries a probe. Code Supercov cannot instrument – a file
it could not compile with its probes, or a Ractor.new block, where no probe
can run – falls back to Ruby’s one-shot line coverage, which reports a line
the first time it executes and never again. There a test is credited with the
lines it was first to reach; what it is not credited with is not evidence it
did not run the code. The run says so, per-test numbers there read “at least”,
and tests affected reports a test whose own record cannot settle the
question as undetermined, so --names includes it in the set to run.
Your project runs in place with its own interpreter and bundle. Supercov loads
through RUBYOPT; application files on disk and their backtrace line numbers
stay unchanged. RSpec, Minitest and test-unit assertions can identify execution
before a passing assertion. That timing evidence alone does not show which
values the assertion checks.
Ruby 3.3 and newer support statement, method, branch and MC/DC measurement, including loops, iterator blocks, short-circuit operators, pattern matching, optional calls and exception paths.
Some constructs have narrower coverage. Code written inside a Ractor.new
block keeps line coverage but may lack other observations. Code a non-main
Ractor calls elsewhere runs as it would without Supercov, but what it runs
there is not recorded: the run says so, and a line only a Ractor reached reads
as not executed. A project whose code or tests mention Ractor gets probes a
Ractor can call, which cost more than the usual ones (about a fifth more in a
tight loop). Certain nested-return expressions
limit normal-completion measurement. Constant predicates are folded as Ruby
folds them, so unreachable alternatives do not become obligations.
If a file cannot be instrumented safely, Supercov loads it unchanged and reports
the remaining limits. To apply that fallback to a known incompatible file, set
SUPERCOV_RUBY_SKIP_PROBES to a comma-separated list of path fragments. This
reduces measurement; it is not a way to claim that skipped obligations are covered.
A Spring preloader started before the run has no coverage hook. Restart it within the measured command. JRuby and TruffleRuby are not supported.
Stop test-owned Ruby servers with SIGTERM or wait for normal exit so they can
report their evidence. SIGKILL and exit! can lose observations since the last
test boundary. A process that does not report leaves a measurement limit; its
missing evidence is not counted as uncovered application code.
supercov -- rspec
supercov -- bundle exec rspec
supercov -- ruby -Itest test/shapes_test.rb
supercov -- bin/rails testA Ruby server under browser tests
A Playwright suite that starts a Ruby server, a Rails, Sinatra or other Rack
app under Puma, WEBrick or another server, measures it the way it measures a
Python server: each request runs as the
test that sent it, a request that names no test is credited by time, a Ruby
process a test starts belongs to that test, and the server’s coverage is a Ruby
run of its own, published as the run or beside the browser run. Gems installed
into the project (bundle config set path vendor/bundle) are used from the
copy the suite runs in.
A Rails system test runs its app in Capybara’s Puma, in the test process: each request is credited to the test that was running when it came, and said to be.
supercov -- npx playwright test
supercov -- bin/rails test:systemCrediting a test with what it reached
Most runners attribute exactly at no cost: Supercov runs each Rust test in its own process, Python’s workers are processes, and JavaScript keeps a coverage map per test. Two cases cannot, because probes are a store into one array the whole process shares and two tests running at once cannot both be credited with what they reached:
| Left as written | --exact-attribution | |
|---|---|---|
A Go test calling t.Parallel() | counted run-wide | announced after it resumes, with -parallel=1 |
| A JUnit suite with parallel execution enabled | counted run-wide | run in order |
npx supercov --exact-attribution -- go test ./...
By default your command runs as you wrote it, and the run says what it could
not credit. --exact-attribution buys that back by running the suite in order,
which costs whatever its parallelism was worth — on sixteen I/O-bound JUnit
tests across fifteen cores, 3.6x. Either way the coverage totals are the same;
what changes is whether a line can be traced to the test that ran it.
npx supercov runs latest reports which you got, under Attribution.
Go
| Runner | Attribution | Current requirement |
|---|---|---|
go test | Exact per test | Go 1.22 or newer |
A test that calls t.Parallel() | Aggregate: its coverage counts run-wide | — |
An Example with an Output comment, and a Fuzz target’s seed corpus | Aggregate: measured, but named by no test | — |
Supercov instruments an isolated copy of the module and runs your own command against it. Your tree is not touched, and your test sources are not rewritten beyond one deferred line per test that binds it to its evidence.
go test builds one binary per package, so each test package records its own
evidence and Supercov merges them. It also caches packages that passed, and a
cached package does not run — so Supercov adds -count=1 to GOFLAGS unless
your command or GOFLAGS already says otherwise, and tells you it did. Through
GOFLAGS it reaches go test however your command starts it, make test
included.
A test that calls t.Parallel() runs alongside others. Probes are a store into
one array shared by the process, so nothing can say which of two concurrent
tests reached a line. That coverage is reported against the run rather than
assigned to a test by guesswork: the lines count as covered, and no test claims
them. It counts only when the run passed, for the same reason a failing test’s
coverage never counts — a failed run cannot say which of it came from the test
that failed.
An Example with an Output comment and a Fuzz target’s seed corpus are
measured the same way: go test runs both, and what they reach is real
coverage no test can claim.
Such a test is still recorded as having run, with its outcome, and credited
with no coverage. npx supercov runs <id> test <name> says that rather than
reporting zeroes, coverage percentages leave it out, and tests affected lists
it as undetermined: nothing can say a change missed it, so --names
includes it in the set to run. A package where every test calls t.Parallel()
is published like any other.
A file Supercov cannot parse is a hole, not the end of the run. It is named on the line that reports the result and recorded in the run, and a run is refused only when nothing is left to measure.
Supercov also writes evidence as the suite runs, not only at the end. Go offers
no way to run code on os.Exit, and a TestMain need not reach the m.Run()
call Supercov wraps — goleak.VerifyTestMain(m) runs the suite and exits
itself. Without periodic writes such a run recorded nothing at all.
A repository with several modules works either way it is laid out. A directory
with a go.mod of its own that no go.work names is a different module, and
go test ./... walks past it, so Supercov leaves it alone. A go.work
workspace has no module at its root, so each module it names gets a runtime of
its own.
npx supercov -- go test ./...
npx supercov -- go test -run TestParser ./internal/...
npx supercov -- go test ./core/... ./app/...A Go server under browser tests
A Playwright suite that starts a Go server with go run measures it: the copy
the suite runs in has its Go rewritten with probes, as for go test, and the
server is built from that copy. Go has no per-request context Supercov can
carry a test’s name in to every probe, so the server sweeps what ran every
5 ms, and each sweep is credited to the browser test that was running then,
if only one was, and said to be. Work done while two tests ran at once, as
with parallel workers, is the run’s and no test’s; the run says how many tests
that left without their server work, and --workers=1 gives each test its
own. The server’s coverage is a
Go run of its own, published as the run or beside the browser run, as for a
Python server.
A server binary built before the run is not the one Supercov instruments:
start it with go run, or build it in the test command.
A Java or Kotlin server under browser tests
A Playwright suite whose server is built by its own command (./gradlew bootRun, mvn spring-boot:run, or javac and java) measures it the way it
measures a Go server: the copy’s Java and
Kotlin are rewritten with probes, the runtime goes in each module’s main
source set, and what the server ran is swept every 5 ms and credited by time
to the browser test running then, if only one was. A jar built before the run
is not the one Supercov instruments.
npx supercov -- npx playwright testA server in a container
A Playwright suite whose server runs in a container it builds measures it in
any of these languages: Node, Python, Ruby, Go, Java or Kotlin, whichever
client starts it: docker compose up with build: in the compose file,
docker build and docker run in webServer, testcontainers building from
a Dockerfile in a test, or a Docker SDK. The suite’s DOCKER_HOST points at
a relay of Supercov’s that passes every request to your Docker daemon and
gives each container whose image holds the copy’s code the run’s settings,
with the project’s .supercov mounted at the same path, so the server
writes its coverage where the run reads it. Each request or window is
credited to its test as for a server started directly. When the command
ends, the run stops the containers it measured and the rest of their compose
projects; it never removes them.
An image holds the copy’s code when it was built during the run, when an earlier run recorded it as built from the same source (a build that finds every layer cached returns it unchanged), or when it is a compose service whose build context is in the copy: Supercov builds that service from the copy before its container starts, so an image left over from an earlier run is never used. An image built from an earlier copy, by a command outside the run, is left as it is, and the run says to build it in the test command.
- Images pulled from a registry (
image:with nobuild:,postgres:16) run code nobody instrumented, and are left as they are. - testcontainers’ reaper, Ryuk, is pointed at the daemon’s own socket
(
TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE) unless you set one. - A Python or Ruby image may keep the code at a path of its own
(
COPY backend/ /srv/): the runtime matches the image’s files to the copy’s by their paths’ common ending, and only when several of them match. - A Go image needs Supercov’s runtime package,
_supercov_runtime/at the module root: copy the whole module (COPY . .), not chosen directories. - A daemon reached over TCP or SSH (
DOCKER_HOST=tcp://...), and Windows, are not covered: there the containers run as written.
npx supercov -- npx playwright testJava and Kotlin
| Runner | Attribution | Current requirement |
|---|---|---|
| JUnit 5 (Jupiter) | Exact per test | JDK 17 or newer, Maven or Gradle |
| JUnit 4 (through Vintage) | Exact per test | — |
| Kotest | Exact per test, under the names Kotest itself reports | — |
| Spock | Exact per feature, under the names Spock itself reports | — |
| TestNG | Exact per test, each data-provider invocation its own | — |
| JUnit 4 alone | Exact per test, through Vintage — see below | Maven or Gradle |
Multi-module builds are measured module by module: each compiles its own source
set and forks its own JVM, so each gets a runtime and records evidence of its
own, and the run merges them. A build that forks several JVMs to run tests in
parallel — Gradle’s maxParallelForks, surefire’s forkCount — is measured
the same way: each JVM writes evidence of its own and the run merges every
one.
Attribution comes from the framework’s own lifecycle rather than from rewritten test sources: a JUnit Platform listener sees every engine built on the platform, which is what covers Kotest and Spock, whose tests are not annotated methods any rewriter could find. TestNG is not a platform engine and has a listener of its own. Tests keep the names their framework chose, so a coverage report and a test report name the same thing.
JUnit 4 on its own is not a platform engine and does not run on one. Maven and
Gradle choose a test provider from what is on the classpath, so putting the
platform there makes the build pick a provider that finds no engine and fail.
Supercov adds junit-vintage-engine to the copy — the platform’s own way of
running exactly those JUnit 4 tests through the lifecycle it listens to — and
measures them; your own build still runs JUnit 4 as it did. In a Gradle build
the copy also switches each JUnit 4 module’s test tasks to useJUnitPlatform(),
leaving any task the build already put on the platform or on TestNG as it was.
Supercov instruments an isolated copy and leaves your build file alone. In the
copy it adds a test-scoped junit-platform-launcher, because the listener is
compiled from the project’s test sources and neither Maven nor Gradle puts that
API on the compile classpath; it disables JUnit’s parallel execution, keeping
whatever else your junit-platform.properties set; and it stops the copy
failing its build on warnings, because the copy holds instrumented code your
project never wrote a style policy for. Warnings are still reported.
If tests do run concurrently anyway, Supercov says so and stops attributing rather than reporting numbers nobody can trust: statements and branches still count run-wide, and condition coverage is dropped, because concurrent evaluations corrupt the state it is computed from.
Some conditions are read by the compiler as well as evaluated at runtime, and
those Supercov leaves exactly as written. x instanceof String s, a record
deconstruction pattern, and Kotlin’s x is String or x != null all narrow a
type for the code beneath them; wrapping such a condition to observe its
operands would take the narrowing away and the code would stop compiling. An
if is still measured — which way it went is recorded from inside its arms
instead — but it carries no condition vectors, so it contributes no MC/DC
obligation. A loop is a harder case: it has one arm, and no place to record an
exit a break would not also reach, so a loop whose condition narrows a type
carries no branch obligation at all rather than one no test could close. The
same goes for a loop over a constant, while (true), which can only go one
way. A Kotlin contract { } has to stay the first statement of its function,
so the probe that records the function being entered is written after it.
Every one of these is named in the run: ask for npx supercov runs latest limitations and each appears with its file, its line, and why it was left
alone. A source file the parser cannot read is declared there too, so a hole in
the denominator stays visible after the build log is gone.
npx supercov -- mvn test
npx supercov -- ./gradlew test
npx supercov -- ./mvnw verifyContainers, VMs, and remote execution
Supercov can collect from supported processes launched through a container, VM, or remote executor when it can see the launch boundary, carry the instrumented workspace into that environment, and receive evidence back.
Mounted workspaces and local child-process launchers are the most direct path. If an executor hides how code is launched or cannot return evidence, Supercov reports the missing boundary rather than claiming unseen code was measured.
If your runner is not listed
For a Node-based runner, try the complete command and inspect the result:
npx supercov -- npm test
npx supercov runs latest runners
npx supercov runs latest scope
Aggregate coverage may already be useful even without exact per-test identity. If a supported runner appears incomplete, see Troubleshooting and include the exact command and runner output when opening an issue.