Test coverage
See which tests reach your code, without a coverage run.
Repowise reads the call and import graph to show which tests reach each file and which tests a change needs. Ingest a coverage report and the measured numbers appear beside the inferred ones, each labelled.
Inferred answers name the tests. Percentages come from the reports you ingest.
At a glance
- 97.5%
- precision of the inferred tests to run
- 47 targets on the repowise repository, checked against per-test coverage.
- 95.7%
- precision of inferred test-to-file links
- 44 of 46 links confirmed by per-test coverage on the same repository.
- 8
- coverage report formats
- LCOV, Cobertura, Clover, JaCoCo, Go coverprofile, coverage.py, Apex and a normalized JSON.
- 40%
- untested-hotspot line
- A hotspot with at least four dependents and line coverage under it.
Sound familiar?
Coverage gives you a number. You needed a list.
- 01
The coverage job takes forty minutes, so nobody runs it locally.
- 02
Coverage is 78 percent and nobody knows which 22 percent matters.
- 03
A one-line change runs the whole suite in CI.
- 04
The agent says it ran the tests. Which ones?
What makes it different
What the index tells you about your tests.
- Inferred reach
Every file answered at index time.
Reached, unreached, mocked only or type only, recomputed on every index update.
- Measured coverage
Eight report formats, one command.
LCOV, Cobertura, Clover, JaCoCo, Go coverprofile, coverage.py, Salesforce Apex and a normalized JSON format.
- Untested hotspots
Risky files no test reaches.
Top-quartile churn, at least four dependents and under 40 percent line coverage, or, with no report ingested, no test reaching the file.
- Impacted tests
The tests a change needs.
From the per-test map when you have one and from the graph when you do not, each row labelled with how it was found.
- Patch coverage gate
Judge a pull request on its new lines.
repowise coverage check needs only git and a report, and returns exit codes CI understands.
- Test quality
Tests that check nothing.
Advisory markers flag assertion-free and mock-saturated tests in Python, JavaScript and TypeScript; oversized assertion blocks count against health.
In depth
Test answers from the graph, and from your reports.
Tests to run
A diff in, a run list out.
On httpx, a one-line change to httpx/_urlparse.py maps to one test through the per-test coverage map, test_url_invalid_port, and to 32 candidate test files through the call and import graph. The two lists are printed apart, and every candidate says how it was found, so a guess never passes for evidence. With no per-test map, the graph list becomes the run list.
- via coverage
- A test whose recorded lines touch the change.
- call-graph
- A test whose calls reach the changed file.
- import-graph
- A test that imports it, directly or through a chain.
- Runner arguments
- --format args prints what to pass to pytest, or :all.
$ repowise impacted-tests HEAD~1Tests covering the changed lines tests/models/test_url.py::test_url_invalid_port|run covers httpx/_urlparse.py1 test(s) directly cover the change (via recorded per-test coverage). Inferred tests (NOT coverage-backed) tests/models/test_whatwg.py call-graph tests/client/test_async_client.py import-graph tests/client/test_auth.py import-graph ... and 29 more32 candidate(s) for 1 file(s) with no coverage.Patch coverage in CI
Gate the new lines, not the whole project.
repowise coverage check reads the diff and one coverage report and answers a single question: of the changed lines the report marks executable, how many ran. It needs no index, so it drops into any CI job. Files missing from the report are listed, never counted as zero, and with an index each uncovered range names the test file to extend.
- Exit codes
- 0 pass, 1 below a gate, 2 could not run.
- Path-scoped gates
- A stricter bar for the API than for scripts.
- max_drop
- How far project coverage may fall from the base.
- suggest-gates
- Proposes gate YAML from your current coverage, writes nothing.
$ repowise coverage check origin/main...HEAD --report coverage/lcov.info --fail-under 80# exit 0: passed or nothing to judge# exit 1: below a gate# exit 2: could not run (no report, unknown revision) # .repowise/config.yamlcoverage: fail_under: 80 max_drop: 0.5 gates: - {name: api, paths: ["/services/api/"], fail_under: 85}With a coverage report
Health against coverage, file by file.
Once a report is ingested, every instrumented file is placed by its health score and its line coverage. The low-health, low-coverage corner is where a bug is most likely to land with no test to catch it, and each file opens to a line-level heatmap.

In plain words
Two kinds of answer, kept apart.
A coverage report says which lines ran. The graph says which tests can reach a file. Repowise keeps both. Measured answers come from a report you ingest and carry percentages. Inferred answers come from the graph, are file-level, and say whether tests reach the file and how. That makes the graph the right source for "is anything testing this" and a coverage report the right source for percentages.
How to read it
What each file can say
- Reached
- Tests call into the file or import it. The answer names the tests and how they reach it.
- Unreached
- No walk from any test reaches the file. On a hotspot, write the test before the refactor.
- Mocked only
- Every test that reaches the file replaces it with a double, so its own code may never run.
- Type only
- The file declares only types. There is nothing for a test to run, so it is never counted as a gap.
How it works
How an answer is produced.
- 01 / Index
Resolve imports and calls
The same dependency graph every other answer uses. - 02 / Walk
Start from every test file
Record which production files each test reaches, and whether by call, import or wiring such as a fixture. - 03 / Ingest
Add a report when you have one
File, line and branch coverage, plus a per-test map from LCOV, coverage.py or Apex. - 04 / Answer
Label every row
Measured or inferred, side by side and never averaged.
Where it shows up
Shows up where the work already happens.
CLI
Turn a diff into a run list for pytest or any runner, and gate a pull request on patch coverage.
repowise impacted-tests, repowise coverage check
Coding agents
Before it edits, the agent gets up to ten recommended tests for the files it is changing, each marked measured or inferred.
get_risk, get_change_risk
CI
The GitHub Action and GitLab template run the patch-coverage gate and can hand the impacted tests to your runner.
Dashboard
The Tests page shows coverage per file and the unreached list, with a trend once three complete ingests exist.
Questions
What people ask before they try it.
Can I see test coverage without running my test suite?
Yes, file by file. During indexing repowise resolves imports and calls across the repository, then walks that graph from every test file. Each production file comes out reached, unreached, mocked only or type only (with two more states for files the index has not seen and repositories with no tests), and a reached file names the tests and whether they call into it or import it. These answers are labelled inferred and are never turned into a percentage, because a call edge shows a test can reach a file, not which lines ran. For line and branch percentages, ingest a coverage report.
How accurate is the inferred test map?
Measured on the repowise repository against per-test coverage from coverage run --contexts=test: 95.7% of inferred "which tests reach this file" links were confirmed (44 of 46), and the inferred list of tests to run for a change was 97.5% precise across 47 targets. The method and raw counts are on the benchmarks page.
Which coverage formats can I ingest?
LCOV, Cobertura, Clover and JaCoCo XML, Go coverprofile, Salesforce Apex JSON, coverage.py's .coverage file and a normalized JSON format, repowise-coverage-v1, for any other runner. LCOV with test names, .coverage recorded with per-test contexts and the Apex run result also carry a per-test map: which test ran which lines. Run repowise coverage add with no path and it finds reports in the usual locations.
What is an untested hotspot?
A file that is a hotspot (top-quartile churn in its repository plus minimum activity), has at least four dependents and has line coverage under 40 percent. Without an ingested report, it fires only when nothing says a test touches the file: no test file paired by name and no test reaching it in the graph. It is the place to add a characterization test before you refactor.
How do I run only the tests a pull request needs?
repowise impacted-tests main...HEAD diffs the branch from its merge base and returns the tests whose recorded coverage touches the changed lines, then lists graph-inferred candidates in a separate table marked not coverage-backed. With --format args --runner pytest it prints runner arguments, or :all when any part of the answer is unknown, so CI falls back to the full suite instead of skipping tests.
Can I gate pull requests on coverage?
Yes. repowise coverage check gates a change on patch coverage: of the changed lines the report marks executable, what share did the tests run. It needs only git and a report, no index. It exits 0 when it passes, 1 when a gate fails and 2 when it cannot run. Gates can be repository-wide or scoped to paths, and max_drop limits how far project coverage may fall from the base branch.
Does an empty test list mean my change is untested?
No. An empty list means no test is needed only when the tool says so, as for a docs change. In every other case the answer carries run_all: true, the CLI tells you to run the full suite, and a file with no coverage data is reported as having no data, never as uncovered.
Last reviewed: October 2026
Get started
See which files no test reaches.
Index a public repository free and open its Tests page. Add a coverage report when you have one and the measured numbers appear beside the inferred ones.