testrisk
Find the code most likely to need better tests. testrisk reads coverage.py data, maps uncovered lines onto functions, and ranks them by test risk — not another coverage percentage.
Why testrisk?
- Rank gaps, do not just list them — uncovered branches, cyclomatic-ish complexity, git-changed lines, and missing tests all feed one score
- Human by default, JSON when you need it —
--promptturns the same evidence into a concise agent task - Optional changed-line gate —
--fail-under-changed 95for CI; the default command still just advises - Small install — one runtime dependency: coverage (see
pyproject.toml)
testrisk 1.0.0
Coverage 91.7%
Branch 84.2%
Highest-value test gaps
1. demo/services.py::ServiceManager.start
Missing: 15-18
Branches: 2 uncovered
Complexity: 3
Risk: HIGH
Changed code
94.1% covered
2 uncovered executable lines
Suggested next target:
tests/test_services.py
Installation
uvx (recommended — zero install)
cd /path/to/your/repo
uvx testrisk
Persistent install on your PATH:
uv tool install testrisk
testrisk --doctor
via pipx (isolated CLI)
pipx install testrisk
via pip
pip install testrisk
If testrisk is not on your PATH
python -m testrisk --version
python -m testrisk --doctor
from source
git clone https://github.com/karlhillx/testrisk.git
cd testrisk
uv sync
uv run testrisk --version
uv run python -m testrisk --version
Quick start
Generate coverage first (pytest-cov or coverage.py), then rank:
pytest --cov --cov-report=json
testrisk
testrisk looks for coverage.json, then coverage.xml, then .coverage, walking up from . to the nearest pyproject.toml / .git when --repo is omitted.
testrisk --changed # only gaps on git-changed lines
testrisk --top 10 # default
testrisk --file src/foo.py # one file (repeatable)
testrisk --json # machine-readable report
testrisk --prompt # evidence-based agent task
testrisk --fail-under-changed 95 # exit 1 if changed-line coverage is low
testrisk --doctor # coverage file, git, and Python
--quiet prints only the suggested next test file.
--prompt
Write focused unit tests for these uncovered behaviors.
Do not modify production code unless required to expose a testable seam.
Target:
src/foo.py::parse_config
Uncovered:
44-48, 57
Untested branches:
line 46: false branch
line 57: exception/exit branch
Complexity: 6
Risk: MEDIUM
Existing tests:
tests/test_foo.py
How ranking works
Each function or method with uncovered statements or branches gets a score:
| Signal | Weight |
|---|---|
| Uncovered executable lines | × 1 |
| Uncovered branches | × 2 |
| Complexity above 1 | × 0.75 |
| Changed uncovered lines | × 3 |
| No related test file | + 4 |
Risk is HIGH / MEDIUM / LOW from that score plus a few hard rules (for example: five or more changed uncovered lines, or complexity ≥ 10 with two uncovered branches).
Related tests are existing test_*.py / *_test.py files that mention the function or module stem. If none exist, the suggestion is tests/test_<stem>.py.
--base defaults to origin/main, then main, then origin/master, then master, then HEAD. Changed lines are git diff against the merge-base of that ref (plus your working tree).
Exit codes
| Code | Meaning |
|---|---|
0 |
Success (or changed-line coverage meets --fail-under-changed) |
1 |
Runtime failure (no coverage data, changed-line coverage below the floor) |
2 |
Usage error |
130 |
Interrupted with Ctrl-C |
Use as a library
testrisk ships type hints (py.typed) and a small public API:
from testrisk import GapError, analyze
try:
report = analyze(".", changed_only=True, top=5)
except GapError as exc:
raise SystemExit(exc) from exc
for gap in report.gaps:
print(gap.qualname, gap.risk, gap.score)
analyze(...) returns a GapReport. Optional kwargs: coverage, changed_only, base, files, top. You can also pass an Options instance. Only names in testrisk.__all__ are public; import the CLI via python -m testrisk or the testrisk console script.
Requirements
- Python 3.12+ (
requires-pythoninpyproject.toml) - OS Linux, macOS, and Windows
- coverage 7.x (installed automatically with
testrisk) - git (optional; needed for
--changed, the changed-code section, and--fail-under-changed)
Local development
uv sync
uv run pytest
uv run pytest --cov=testrisk --cov-report=xml tests/
uv run ruff check testrisk tests
uv run ty check
Environment variables
| Variable | Description |
|---|---|
NO_COLOR |
Disable color when --color auto |
FORCE_COLOR |
Enable color when --color auto even if stdout is not a TTY |
TESTRISK_DEBUG |
Print a traceback on unexpected errors (same as --verbose for crashes) |
testrisk does not run your test suite. It only reads coverage artifacts, source files, and git diffs.
Troubleshooting
"No coverage data found"
Run tests with coverage and write a report in the repo root:
pytest --cov --cov-report=json
# or
coverage run -m pytest && coverage json
Then pass --coverage PATH if the file is not in the usual place.
Changed-code section is missing
The checkout is not a git repo, or git is not on PATH. --fail-under-changed needs git.
uvx: command not found
Install uv (curl -LsSf https://astral.sh/uv/install.sh | sh or brew install uv), then retry uvx testrisk.
License
MIT License - see LICENSE for details.
Contributing
See CONTRIBUTING.md. User-facing changes should be noted in CHANGELOG.md. Security reports: SECURITY.md.
Links
Metadata
Release files for testrisk 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| testrisk-1.0.0.tar.gz | 29.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| testrisk-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 56.2 kB
Release files / testrisk-1.0.0.tar.gz
| Download URL | testrisk-1.0.0.tar.gz |
|---|---|
| Size | 29.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
26b307bc0d3a39de97182e08ce6c59343724a20c194f055e756b29e39b826e5f
|
|
BLAKE2b-256 checksum How to use checksums |
13e720296e5fe1bafbf22590ac3f7299d9585dd0ea6d122b01a111a9f3c5bcf8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency logRelease files / testrisk-1.0.0-py3-none-any.whl
| Download URL | testrisk-1.0.0-py3-none-any.whl |
|---|---|
| Size | 27.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c65e0c7c42dfeb9f4cb8474d55a5561a7773e66aac9afc383ad5a0b067a923a2
|
|
BLAKE2b-256 checksum How to use checksums |
9b16e3d27e6350c60e03d3b652d63e9ed8f712abda170cb8777c17ac29fe4e45
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 12, 2026.
Transparency log