Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions examples/pytest/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,38 @@ sh_test(
":coverage_setup_test",
],
)

# ─── Subprocess coverage regression ────────────────────────────────────
#
# Verifies that coverage data is captured from Python subprocesses
# (e.g. pytest-xdist workers, subprocess.Popen). The test calls
# foo.subtract() in a subprocess; the LCOV should include DA hits for
# both add() (in-process) and subtract() (subprocess).

py_test(
name = "subprocess_coverage_setup_test",
srcs = ["subprocess_coverage_test.py"],
data = [":coverage_manifest"],
env = {
"COVERAGE_MANIFEST": "$(rootpath :coverage_manifest)",
"COVERAGE_OUTPUT_FILE": "subprocess_coverage_setup_test.lcov",
},
imports = ["../.."],
package_collisions = "warning",
pytest_main = True,
tags = ["manual"],
deps = [
":lib",
"@pypi//coverage",
"@pypi//pytest",
],
)

sh_test(
name = "subprocess_coverage_lcov_test",
srcs = ["run_subprocess_coverage_check.sh"],
data = [
":coverage_manifest",
":subprocess_coverage_setup_test",
],
)
40 changes: 40 additions & 0 deletions examples/pytest/run_subprocess_coverage_check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Verifies that subprocess coverage data is captured in the LCOV output.
# The subprocess_coverage_test.py test calls foo.subtract() (line 5) in
# a subprocess. If subprocess coverage propagation works, the LCOV
# should contain a DA:5,<nonzero> record.

set -euo pipefail

LAUNCHER="$TEST_SRCDIR/_main/examples/pytest/subprocess_coverage_setup_test"
MANIFEST="$TEST_SRCDIR/_main/examples/pytest/coverage_manifest.txt"

[[ -x "$LAUNCHER" ]] || { echo "launcher not found: $LAUNCHER" >&2; exit 1; }
[[ -f "$MANIFEST" ]] || { echo "manifest not found: $MANIFEST" >&2; exit 1; }

LCOV="$(mktemp -d)/coverage.lcov"

COVERAGE_MANIFEST="$MANIFEST" \
COVERAGE_OUTPUT_FILE="$LCOV" \
"$LAUNCHER"

[[ -s "$LCOV" ]] || { echo "LCOV file empty or missing: $LCOV" >&2; exit 1; }

# foo.py line 2 (add body) should be covered in-process.
grep -qE '^DA:2,[1-9]' "$LCOV" || {
echo "Expected DA:2,<nonzero> (add function) not found in LCOV." >&2
echo "LCOV contents:" >&2
cat "$LCOV" >&2
exit 1
}

# foo.py line 5 (subtract body) should be covered via subprocess.
grep -qE '^DA:5,[1-9]' "$LCOV" || {
echo "Expected DA:5,<nonzero> (subtract function, called in subprocess) not found." >&2
echo "This means subprocess coverage data was not captured." >&2
echo "LCOV contents:" >&2
cat "$LCOV" >&2
exit 1
}

echo "OK: subprocess coverage captured — both add() and subtract() have DA hits."
28 changes: 28 additions & 0 deletions examples/pytest/subprocess_coverage_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Regression test: coverage data from Python subprocesses is captured.

The test calls foo.subtract() in a subprocess. If subprocess coverage
propagation works, the LCOV report will include a DA hit for line 5
of foo.py (the subtract function body).
"""

import os
import subprocess
import sys

from examples.pytest.foo import add


def test_add_in_process():
assert add(1, 1) == 2


def test_subtract_in_subprocess():
env = {**os.environ, "PYTHONPATH": os.pathsep.join(sys.path)}
result = subprocess.run(
[sys.executable, "-c", "from examples.pytest.foo import subtract; print(subtract(3, 1))"],
capture_output=True,
text=True,
env=env,
)
assert result.returncode == 0, result.stderr
assert result.stdout.strip() == "2"
18 changes: 15 additions & 3 deletions py/private/pytest_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,22 @@
# The lines are files that matched the --instrumentation_filter flag
with open(os.getenv("COVERAGE_MANIFEST"), "r") as mf:
manifest_entries = mf.read().splitlines()
cov = coverage.Coverage(include = manifest_entries)
cov = coverage.Coverage(include = manifest_entries, data_suffix = True, config_file = False)
# coveragepy incorrectly converts our entries by following symlinks
# record a mapping of their conversion so we can undo it later in reporting the coverage
coveragepy_absfile_mapping = {coverage.files.abs_file(mfe): mfe for mfe in manifest_entries}
cov.start()
# Propagate coverage config to subprocesses (e.g. pytest-xdist
# workers). We write a .pth file so that every Python subprocess
# sharing this venv calls coverage.process_startup() at interpreter
# init. Combined with COVERAGE_PROCESS_CONFIG, the subprocess
# automatically starts its own Coverage instance and writes a
# per-process data file that we combine() during teardown.
import sysconfig
_subprocess_pth = os.path.join(sysconfig.get_path("purelib"), "_coverage_subprocess.pth")
with open(_subprocess_pth, "w") as _f:
_f.write("import coverage; coverage.process_startup()\n")
os.environ["COVERAGE_PROCESS_CONFIG"] = cov.config.serialize()
except ModuleNotFoundError as e:
print("WARNING: python coverage setup failed. Do you need to include the 'coverage' package as a dependency of py_pytest_main?", e)
pass
Expand Down Expand Up @@ -128,13 +139,14 @@
print("Ran pytest.main with " + str(args), file=sys.stderr)
elif cov:
cov.stop()
cov.save()
cov.combine()
# https://bazel.build/configure/coverage
coverage_output_file = os.getenv("COVERAGE_OUTPUT_FILE")

unfixed_dat = coverage_output_file + ".tmp"
cov.lcov_report(outfile = unfixed_dat)
cov.save()


with open(unfixed_dat, "r") as unfixed:
with open(coverage_output_file, "w") as output_file:
for line in unfixed:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies = [
"colorama~=0.4.0",
"click",
"pytest",
"coverage",
"coverage>=7.10.3",
"cowsay",
"pkg",
"snakesay",
Expand Down
2 changes: 1 addition & 1 deletion requirements.in
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ django~=4.2.7
colorama~=0.4.0
click
pytest
coverage
coverage>=7.10.3
cowsay
snakesay
ftfy==6.2.0
Expand Down
Loading