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 |
| Jest | Exact per test, including parameterized tests, with the user’s own configuration, setup files and reporters kept; passing expect occurrences are identified for assertion maps |
node:test | Exact per test |
| AVA and Mocha | Aggregate structural coverage |
| 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.
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.
For assertion maps, Node, Vitest, Jest and Playwright’s Node-side assertions supply supported passing-occurrence evidence. Custom assertion wrappers and browser-side checks can have additional observation limits. See Assertion evidence before interpreting a missing occurrence.
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.
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 0.9.140 |
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.
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.
Python
| Runner | Attribution | Current requirement |
|---|---|---|
| pytest | Exact test, worker, retry, and setup/call/teardown phase identity | CPython 3.12 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 monitoring and runner hooks through the process environment; 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.
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. Completed observations can survive a hard kill, 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 unittestRuby
| 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 |
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.4 and newer support statement, method, branch and MC/DC measurement, including loops, iterator blocks, short-circuit operators, pattern matching, optional calls and exception paths. Ruby 3.3 supplies Ruby’s own line, method and branch coverage; obligations requiring additional instrumentation are reported as measurement limits.
Some constructs have narrower coverage. Code in a non-main Ractor keeps line coverage but may lack other observations. 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 testGo
| 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 unless your command
already says otherwise, and tells you it did.
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, so what they reach is real
coverage, but neither takes a *testing.T for a result to be named by.
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.
For assertion maps, t.Error, t.Errorf, t.Fatal, t.Fatalf and testify’s
assert and require are inventoried. A Go test states its claim with an if
and reports the violation, so the report is the site.
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/...Java 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 |
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.
For a Maven module, 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.
A Gradle module is left alone and told about rather than broken.
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 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.
For assertion maps, forms spelled assertSomething, assertThat or fail are
inventoried, which covers JUnit, TestNG, AssertJ, Hamcrest and kotlin.test.
Kotest’s infix matchers are not.
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.