Skip to content

fix(ci): wire enable_coverage into pytest-local pytest invocation#9559

Open
nv-tusharma wants to merge 6 commits into
mainfrom
fix/pytest-local-coverage-wiring
Open

fix(ci): wire enable_coverage into pytest-local pytest invocation#9559
nv-tusharma wants to merge 6 commits into
mainfrom
fix/pytest-local-coverage-wiring

Conversation

@nv-tusharma
Copy link
Copy Markdown
Contributor

@nv-tusharma nv-tusharma commented May 14, 2026

Overview:

Fix Python coverage being reported as 0% in nightly CI. Two related gaps caused the same symptom:

  1. The pytest-local composite action declared an enable_coverage input and gated its upload-artifact step on it, but never threaded --cov flags into the pytest command. Nightly jobs uploaded artifacts containing only JUnit XML, so the downstream coverage-report job's coverage combine had nothing to merge and printed 0%.
  2. When max_vram_gib is set (vllm/sglang single-GPU jobs with run_gpu_parallel_tests=true), tests/conftest.py hands off to the VRAM-aware orchestrator in tests/utils/pytest_parallel_gpu.py. Even after fixing Update README.md #1, profiled GPU tests still ran without coverage because the orchestrator builds child pytest argv from a hardcoded whitelist that did not include --cov*.

Details:

.github/actions/pytest-local/action.yml

In the normal-mode else branch, when inputs.enable_coverage == 'true':

  • Build COVERAGE_FLAGS with --cov=components/src/dynamo --cov=lib/bindings/python/src/dynamo --cov-report=. The empty --cov-report= tells pytest-cov to write only the binary .coverage data file — the nightly coverage-report job generates the human-readable reports after coverage combine.
  • Export COVERAGE_FILE="${TEST_RESULTS_DIR}/.coverage" so the data file (and any per-worker .coverage.* shards produced by parallel=True in .coveragerc) lands in the uploaded artifact directory.
  • Extend PYTHONPATH with ${CONTAINER_WORKSPACE}/components/src:${CONTAINER_WORKSPACE}/lib/bindings/python/src so coverage's source resolution matches the [paths] mapping in .coveragerc.
  • Inject ${COVERAGE_FLAGS} into PYTEST_CMD just before the -m marker selector.

This mirrors the pre-existing pattern in .github/actions/pytest/action.yml (lines 138–145).

tests/conftest.py

In the orchestrator's child-argv builder (pytest_runtestloop), forward --cov / --cov-report values when the _cov plugin is loaded in the parent. Without this, child sessions ran without pytest-cov loaded and contributed no .coverage data to the nightly merge.

if config.pluginmanager.get_plugin("_cov") is not None:
    for src in config.getoption("cov_source", default=None) or []:
        extra_args.append(f"--cov={src}")
    for rpt in config.getoption("cov_report", default=None) or []:
        extra_args.append(f"--cov-report={rpt}")
tests/utils/pytest_parallel_gpu.py

Give each child a unique COVERAGE_FILE (suffixed with worker id + safe test name) so its session-end combine doesn't clobber siblings. pytest-cov writes per-process suffixed shards during the run and combines them into the unsuffixed path at session end; with a shared path, the last child wins. Unique paths let every child's combined data survive, and the parent's own session-end coverage combine sweeps the per-child files into the merged .coverage (which the nightly coverage-report job then merges across all jobs).

What's intentionally not touched

.coveragerc, pyproject.toml, and the coverage-report job in nightly-ci.yml are all correct as-is — they just had nothing to merge before this fix. The dind-as-sidecar branch in .github/actions/pytest/action.yml has the same latent gap as pytest-local, but shared-test.yml doesn't use it, so this PR is kept tightly scoped.

Where should the reviewer start?

  1. .github/actions/pytest-local/action.yml lines 217–226: the coverage block and ${COVERAGE_FLAGS} interpolation in PYTEST_CMD.
  2. tests/conftest.py around line 266: the new pytest-cov forwarding block in the orchestrator argv builder.
  3. tests/utils/pytest_parallel_gpu.py around line 532: the per-child COVERAGE_FILE suffix.

Test plan:

Validated locally inside the actual CI test image (-vllm-runtime-cuda12-test):

  • CPU/unprofiled path: ran the patched action's exact PYTEST_CMD against test_vllm_unit.py. .coverage data file written (77 KiB), coverage report produced real per-file numbers with TOTAL = 25 %.
  • Orchestrator path: probe pytest plugin intercepted run_parallel and captured EXTRA_PYTEST_ARGS — confirmed --cov=components/src/dynamo, --cov=lib/bindings/python/src/dynamo, and --cov-report= are forwarded to children, plus per-child COVERAGE_FILE block is intact.

Validated end-to-end on this PR's pre-merge CI (via a temporary enable_coverage: true flip on vllm-test, now reverted):

  • CPU stage .coverage artifact contains real data — 201 files, 4727 bytes of bitmap, coverage report shows TOTAL = 37 % (17120 statements, 10752 missed).
  • Sequential GPU stage (226 tests in-process) also produced a valid .coverage file via the same wiring.
  • Parallel GPU stage (orchestrator: 14 profiled e2e tests spawned as subprocesses) produced a valid .coverage file. The merged data is roughly equal to the parent's collection-time imports — see "Known limitation" below.

Known limitation (follow-up needed for full e2e coverage):

The orchestrator forwarding fix is mechanically correct but contributes little measurable coverage for the current test mix because the GPU-parallel pool is entirely e2e tests (test_serve_deployment[*], test_router_e2e_*). These tests spawn dynamo binaries as separate subprocesses and black-box them over HTTP — the dynamo source executes inside the spawned binary, which pytest-cov in the test process cannot trace.

This was confirmed empirically on this PR: the sequential GPU stage ran 226 tests in-process with --cov correctly wired, and its .coverage was bytewise-identical to the CPU stage's. The test execution adds essentially nothing on top of pytest's collection-time imports, because the test bodies don't exercise dynamo code in the pytest process.

For meaningful coverage of dynamo internals from e2e tests, a follow-up will need to add COVERAGE_PROCESS_START + a sitecustomize hook so spawned Python subprocesses (the dynamo binaries themselves) also record coverage. That's dmitry's "Option B" from review — broader scope, separate PR.

This PR is the right floor: unit-test coverage (which is what the CPU stages produce) now flows through end-to-end, and the orchestrator wiring is ready for the day someone adds in-process profiled tests.

🤖 Generated with Claude Code


Open in Devin Review

Summary by CodeRabbit

  • Chores
    • Enhanced test coverage collection configuration to enable more comprehensive code quality verification during development.

Review Change Stack

The pytest-local composite action declared an `enable_coverage` input and
gated its upload-artifact step on it, but never threaded the corresponding
`--cov` flags into the pytest command. As a result, nightly-ci jobs that
opted into coverage uploaded artifacts containing only JUnit XML -- no
`.coverage*` data files -- and the downstream `coverage-report` job's
`coverage combine` step had nothing to merge, producing 0% coverage.

Mirror the pattern from `.github/actions/pytest/action.yml`:

- Build COVERAGE_FLAGS with the two `--cov` targets and an empty
  `--cov-report=` so pytest-cov writes only the binary `.coverage` data
  file (the merge job generates the human-readable reports).
- Export COVERAGE_FILE under TEST_RESULTS_DIR so the data file and any
  per-worker `.coverage.*` shards (from `parallel=True` in .coveragerc)
  land inside the uploaded artifact directory.
- Extend PYTHONPATH with the component and bindings src roots so
  coverage's source resolution lines up with `.coveragerc`'s `[paths]`
  mapping.
- Inject \${COVERAGE_FLAGS} into PYTEST_CMD before the \`-m\` selector,
  matching the order used by the older action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nv-tusharma nv-tusharma requested a review from a team as a code owner May 14, 2026 17:13
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Walkthrough

The .github/actions/pytest-local/action.yml composite GitHub Action is updated to conditionally enable Python code coverage collection. When enable_coverage is true, the action constructs coverage-related flags, sets environment variables for coverage output, configures the Python path, and updates the pytest command to include coverage instrumentation.

Changes

Conditional Coverage Collection

Layer / File(s) Summary
Pytest coverage configuration
.github/actions/pytest-local/action.yml
enable_coverage conditional block builds COVERAGE_FLAGS with pytest coverage directives, exports COVERAGE_FILE to test-results directory, extends PYTHONPATH with workspace bindings, and appends coverage flags to PYTEST_CMD invocation.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: wiring the enable_coverage input into the pytest-local action's pytest invocation, which directly addresses the 0% coverage reporting issue.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description check ✅ Passed The pull request description is comprehensive and well-structured, covering all required sections with detailed technical explanations and clear guidance for reviewers.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread .github/actions/pytest-local/action.yml
When `max_vram_gib` is set, tests/conftest.py hands off to the VRAM-aware
orchestrator at tests/utils/pytest_parallel_gpu.py, which spawns each
profiled test as a separate pytest subprocess. The child argv is built
from a hardcoded whitelist in conftest.py and did not include `--cov*`,
so child sessions ran without pytest-cov loaded and emitted no coverage
data. Result: in nightly vllm-test and sglang-test single-GPU jobs
(run_gpu_parallel_tests=true), profiled tests still contributed 0%
after the prior pytest-local action wiring fix.

Two contained changes:

- tests/conftest.py: forward --cov and --cov-report values to children
  when the _cov plugin is loaded in the parent.
- tests/utils/pytest_parallel_gpu.py: give each child a unique
  COVERAGE_FILE (suffixed with worker id + safe test name) so its
  session-end combine doesn't clobber siblings. pytest-cov sets
  data_suffix=True internally and combines per-process shards into
  the unsuffixed path at session end; with a shared path, the last
  child wins. Unique paths let every child's combined data survive,
  and the existing job-level merge in nightly-ci.yml's coverage-report
  job picks them all up alongside the parent's data.

Validated by reproducing the orchestrator path inside the CI test image
with the patched worktree mounted: forwarded EXTRA_PYTEST_ARGS contains
both `--cov=...` entries plus `--cov-report=`, and the per-child
COVERAGE_FILE block is intact.

Per review by dynamo-ops and dmitry-tokarev-nv on #9559.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nv-tusharma nv-tusharma requested a review from a team as a code owner May 18, 2026 18:52
…erge)

Flips enable_coverage:true on the PR pipeline's vllm-test job so we can
observe the patched pytest-local action + orchestrator forwarding
end-to-end against real GPU runners. vllm-test already runs with
run_gpu_parallel_tests=true and gpu_parallel_max_vram_gib=24, so it
exercises both the sequential pytest path (action wiring fix) and the
VRAM-aware orchestrator path (conftest + pytest_parallel_gpu fixes).

After CI completes:
- Open the coverage-python-vllm-* artifacts from the workflow run.
- Confirm each contains .coverage* files (parent and per-child shards).
- Confirm the parallel-GPU stage produces .coverage.w{N}.{test_name}*
  shards (proof the orchestrator forwarding works).

Revert this commit before merging.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants