Skip to main content

pytest-cov-container

PyPI - Version PyPI - Python Version security: bandit

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

  1. Before tests — moves your <build_dir>/run.sh aside to _orig_run.sh, then injects a coverage wrapper, .coveragerc, and a shim run.sh that exec's the wrapper. The wrapper invokes your unmodified _orig_run.sh under coverage. Your production entrypoint is the test entrypoint by construction — no duplicate command string to drift.
  2. During tests — your containerized app runs under coverage via the injected wrapper, which forwards SIGTERM to your app on container shutdown so coverage data is saved cleanly.
  3. 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-under include 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.sh must exist after sam build.
  • Your build_dir must include the coverage package as an installed dependency (the plugin checks for a coverage*.pth file under any site-packages directory below build_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 (--cov flag present)
  • [tool.pytest-cov-container] is configured in pyproject.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:

  1. After cov.start(), write its pid to PID_FILE (/tmp/.cov_container.pid).
  2. On SIGUSR1, call cov.save(), then create DONE_FILE (/tmp/.cov_container.done).
  3. Read the injected .coveragerc (path in COVERAGE_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.sh aside to _orig_run.sh (mode preserved, idempotent across re-runs)
  • Writing .coveragerc with parallel = true, sigterm = true and the host's branch setting
  • Writing _cov_wrapper.py that starts coverage, runs the save protocol, splits SIGUSR1/SIGTERM handling (save-only vs save+forward), forwards SIGTERM to the child process so the runtime grace period saves cleanly, and invokes your unmodified _orig_run.sh
  • Writing a shim run.sh that exec's the wrapper
  • Extracting .coverage.container* files from /tmp in 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)

Source distribution for pytest-cov-container 0.4.1
File Size Uploaded
pytest_cov_container-0.4.1.tar.gz 105.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-cov-container 0.4.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

0.5.0

2 release files

This release

0.4.1 This release

2 release files

0.3.0

2 release files

0.2.0

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page