Measuring coverage¶
Coverage is measured by running coverage.py around voci — no plugin to install, no voci flag to pass:
$ coverage run -m voci
$ coverage report -m
Name Stmts Miss Cover Missing
-----------------------------------------------------
ledger/__init__.py 2 0 100%
ledger/store.py 84 3 96% 61, 118-119
-----------------------------------------------------
TOTAL 86 3 97%
coverage html and coverage xml read the same data file. Everything a project already has in
[tool.coverage.run] — source, branch, omit, exclusions, plugins — applies unchanged:
coverage.py is reading its own configuration, not voci's.
Use the module form, coverage run -m voci, rather than coverage run voci. It finds voci
without the console script being on PATH, which containers and --user installs are not always
arranged to guarantee.
Isolated tests¶
A voci suite is one process running many tests at once, so a single coverage run measures all
of them: the event loop's thread, and the worker threads sync def tests run on.
@voci.isolated tests each run in a subprocess instead. voci puts those subprocesses under the
same measurement as the run that spawned them, with the settings the parent is measuring under,
and merges back what each one recorded as it exits — so a run produces one data file to report on,
whatever mix of tests it contained.
@voci.isolated
async def test_install_rebinds_the_signal_handler() -> None:
"""Runs in its own subprocess, and is measured there: every line `install` executes
counts towards the report the same way an in-process test's would."""
reloader.install()
assert signal.getsignal(signal.SIGHUP) is not signal.SIG_DFL
Carrying a configuration into a subprocess needs coverage.py 7.10 or newer. Below that, voci says so on stderr, once per isolated test whose lines are missing from the report.
In CI¶
Keeping --fail-under on the report step keeps two different failures apart: a suite that fails
and a suite that covers too little are separate problems, and one exit code for both hides the
first behind the second.