pytest-cov-container
Collect code coverage from Python applications running inside Docker containers during integration tests. Works alongside pytest-cov to combine container coverage with your local test coverage.
Built for AWS SAM local testing: by default it collects from the Lambda containers sam local starts for this checkout.
How It Works
- Before tests — moves your
<build_dir>/run.shaside to_orig_run.sh, then injects a coverage wrapper,.coveragerc, and a shimrun.shthat exec's the wrapper. The wrapper invokes your unmodified_orig_run.shundercoverage. Your production entrypoint is the test entrypoint by construction — no duplicate command string to drift. - During tests — your containerized app runs under
coveragevia the injected wrapper, which forwardsSIGTERMto your app on container shutdown so coverage data is saved cleanly. - After tests — signals each owned container's coverage process to save, waits for its save sentinel, copies the data files out and drops them next to pytest-cov's data file. pytest-cov's own combine merges them with the host data, so its report and
--cov-fail-underinclude container coverage.
Installation
pip install pytest-cov-container
Requires Python 3.11+ and, for the default container selection, SAM CLI 1.165+ (the first release that labels its Lambda containers).
Configuration
Add to your pyproject.toml:
[tool.pytest-cov-container]
[tool.pytest-cov-container.path_mapping]
"src/api" = "/var/task"
[tool.pytest-cov-container.python]
build_dir = ".aws-sam/build/ApiFunction"
| Key | Description |
|---|---|
image_pattern |
Glob pattern, or list of patterns, matched against the image the container was created from (then its tags) |
label |
Docker label filter, key or key=value (default: sam.cli.container.type=lambda, which SAM CLI 1.165+ sets on every Lambda container; "" switches the filter off) |
mount_prefix |
Ownership: only containers that bind-mount a directory under <rootdir>/<mount_prefix>/ (default: the parent of build_dir, i.e. the SAM build root; "" switches the check off). Tells concurrent checkouts apart, which image and labels cannot: they are the same in every checkout. sam local mounts each function's build dir. |
worker_env |
Ownership: only containers whose env holds <worker_env>=<worker id> (gw0, gw1, … under xdist, else main). Pass container_env() into the containers (see below). |
required |
Fail the run when a collection pass gets no data (default: false; also --cov-container-required) |
path_mapping |
Maps host source paths to container paths; applied when each collected file is handed to pytest-cov. [tool.coverage.paths] works too. |
language |
Language driver to use (default: "python") |
enabled |
Set to false to disable (default: true) |
[tool.pytest-cov-container.python]:
| Key | Description |
|---|---|
build_dir |
SAM build output directory where coverage files are injected (default: .aws-sam/build/ApiFunction) |
wrapper |
false: your application starts coverage itself and runs the save protocol; only .coveragerc is injected and run.sh is left alone (default: true) |
source_dir |
Measure only this host directory's *.py files (minus tests/, __pycache__/ and dot-dirs), at their paths under container_root. Keeps vendored dependencies in the build dir unmeasured. Default: every *.py. |
container_root |
Where build_dir is mounted in the container (default: /var/task) |
branch |
Branch coverage in the container. Default: the host's setting, which it must match to combine. |
relative_files |
coverage's relative_files in the container (default: true; set false to record absolute container paths for [paths] remapping) |
entrypoint |
Override (discouraged). Replace convention-discovered <build_dir>/run.sh with this command via sh -c. Drift between this string and prod run.sh is the bug class this plugin's default path eliminates. Omit the field to use convention discovery. Setting to the empty string raises a load-time error. |
Default Path: Convention Discovery
The plugin reads your existing <build_dir>/run.sh (whatever sam build produced) and arranges for coverage to wrap it. Requirements:
<build_dir>/run.shmust exist aftersam build.- Your build_dir must include the
coveragepackage as an installed dependency (the plugin checks for acoverage*.pthfile under anysite-packagesdirectory belowbuild_dir). This is what enables subprocess coverage attach.
If either precondition fails, inject() raises with a migration hint.
Usage
Run your tests with --cov as usual:
pytest --cov=src/api tests/
The plugin activates automatically when:
- pytest-cov is active (
--covflag present) [tool.pytest-cov-container]is configured inpyproject.toml
Disable it for a run with:
pytest --cov=src/api --no-cov-container tests/
Mid-Session Collection
If you need to collect coverage before containers stop (e.g., in a session-scoped fixture teardown), use the public API:
from pytest_cov_container import collect_container_coverage
@pytest.fixture(scope="session")
def sam_api():
proc = start_sam(...)
yield SAM_URL
collect_container_coverage()
proc.terminate()
This saves and collects the running containers this process owns and returns the number of data files collected. With required, collecting nothing raises. Containers still around after the tests are collected automatically before pytest-cov reports.
The wrapper's SIGUSR1 save flushes the wrapper process only. A long-running server started by _orig_run.sh saves when it gets SIGTERM (container stop), so for mid-session collection from a server, set wrapper = false and run the save protocol in the application.
Save Protocol
The process that owns the coverage.Coverage object follows three rules:
- After
cov.start(), write its pid toPID_FILE(/tmp/.cov_container.pid). - On
SIGUSR1, callcov.save(), then createDONE_FILE(/tmp/.cov_container.done). - Read the injected
.coveragerc(path inCOVERAGE_PROCESS_START), which writes data files as/tmp/.coverage.container*.
With wrapper = false, the application does this itself, for example:
import os
import signal
from pathlib import Path
def maybe_start_coverage() -> None:
rcfile = os.environ.get("COVERAGE_PROCESS_START")
if not rcfile:
return # production: no coverage
import coverage
cov = coverage.Coverage(config_file=rcfile)
cov.start()
Path("/tmp/.cov_container.pid").write_text(str(os.getpid()))
def save(*_):
cov.save()
Path("/tmp/.cov_container.done").touch()
signal.signal(signal.SIGUSR1, save)
pytest-xdist and Concurrent Checkouts
Image and label say what a container is, not whose it is. Two xdist workers, or two checkouts of the project, run identical images. Set the ownership keys and pass the marker into the containers:
[tool.pytest-cov-container]
worker_env = "MY_TEST_WORKER" # label and mount_prefix keep their SAM defaults
import pytest_cov_container
env_vars = {"Parameters": pytest_cov_container.container_env(project_root)}
# write env_vars to env.json, then: sam local start-api --env-vars env.json
container_env() returns the worker marker and, while the plugin is active, COVERAGE_PROCESS_START. sam local --env-vars only overrides variables the template declares, so declare both in template.yaml (empty values are fine) or sam drops them silently.
Under xdist the controller injects before any worker starts, each worker collects only its own containers, and pytest-cov's controller combines everything once.
owned_containers(rootpath, all_workers=False) lists the same containers for other uses, such as a reaper that removes leaked containers without touching a concurrent session's.
Pluggable Drivers
Language support is pluggable via entry points. The built-in Python driver handles:
- Moving your
<build_dir>/run.shaside to_orig_run.sh(mode preserved, idempotent across re-runs) - Writing
.coveragercwithparallel = true,sigterm = trueand the host'sbranchsetting - Writing
_cov_wrapper.pythat starts coverage, runs the save protocol, splitsSIGUSR1/SIGTERMhandling (save-only vs save+forward), forwardsSIGTERMto the child process so the runtime grace period saves cleanly, and invokes your unmodified_orig_run.sh - Writing a shim
run.shthat exec's the wrapper - Extracting
.coverage.container*files from/tmpin containers
All injected files are written atomically.
To add a driver for another language, register an entry point:
[project.entry-points."pytest_cov_container.drivers"]
node = "my_package.drivers.node:NodeDriver"
Drivers must implement the LanguageDriver protocol from pytest_cov_container.models.
Development
# Run tests
hatch test
# Run across all Python versions
hatch test --all
# Format and lint
hatch fmt
# Type check
hatch run types:check
# Security scan
hatch run security:scan
# Cut a release: bump + commit + tag + push in one shot. Pass a version
# literal or a hatch segment (`patch`, `minor`, `major`, `rc`, etc.).
# Omit the arg to bump the minor version.
hatch run release # 0.2.0 → 0.3.0
hatch run release patch # 0.2.0 → 0.2.1
hatch run release 0.3.5 # explicit
License
pytest-cov-container is distributed under the terms of the MIT license.
Release files for pytest-cov-container 0.4.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytest_cov_container-0.4.1.tar.gz | 105.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_cov_container-0.4.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 133.4 kB
Release files / pytest_cov_container-0.4.1.tar.gz
| Download URL | pytest_cov_container-0.4.1.tar.gz |
|---|---|
| Size | 105.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
22386b37b8f8235def11342b6e6f313b780d8b9877934299c5077f971acedd7d
|
|
BLAKE2b-256 checksum How to use checksums |
662172fb4b9421375e9cd3bd4367cf9254db178b387327ecfadfaa60490e8362
|
| 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 24, 2026.
Transparency logRelease files / pytest_cov_container-0.4.1-py3-none-any.whl
| Download URL | pytest_cov_container-0.4.1-py3-none-any.whl |
|---|---|
| Size | 28.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
437454bc1ae5298157ac58cdb7733be790c32c69893730fd64dbb6e2d53147e0
|
|
BLAKE2b-256 checksum How to use checksums |
221064d06c4df8891482095a49c75252b0707281c3f9366d0266df8af7af8127
|
| 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 24, 2026.
Transparency log