Skip to content

fix: node coverage#2904

Open
mitchspano wants to merge 2 commits into
aspect-build:mainfrom
mitchspano:fix/node_coverage
Open

fix: node coverage#2904
mitchspano wants to merge 2 commits into
aspect-build:mainfrom
mitchspano:fix/node_coverage

Conversation

@mitchspano

Copy link
Copy Markdown

Fixes #2901: bazel coverage reported 0% (empty coverage.dat) for JS tests whenever coverage postprocessing ran as its own action (--experimental_split_coverage_postprocessing, which is typical with remote execution).

The V8 -> lcov conversion ran in the _lcov_merger action, whose runfiles contain only the merger and node, not the instrumented sources, so c8 emitted LF:0 for every file. The conversion now runs at the end of the test action, where the V8 data and sources coexist; the report is stashed in $COVERAGE_DIR and the merger simply publishes it, falling back to the old behavior when no stash exists. Adding the sources to the merger action was considered instead, but the V8 URLs still point at the test's sandbox paths and would need fragile rewriting.


Changes are visible to end-users: yes

  • Searched for relevant documentation and updated as needed: no docs cover the coverage merger; none needed
  • Breaking change (forces users to change their own code or config): no
  • Suggested release notes appear below: yes

fix: bazel coverage no longer reports 0% for JS tests when coverage postprocessing runs as a separate action (#2901)

Test plan

  • New test cases added: e2e/coverage/test.sh runs bazel coverage in both the default and split-postprocessing modes and asserts the lcov report attributes hits and misses to lib.js. Fails against pre-fix sources (coverage.dat is missing or empty), passes with the fix.

Disclaimer: This is my first contribution on this project. I've read the code of conduct and the contributing guidelines, but if I made a mistake in the creation of this PR, please let me know! I'm happy to change the target branch or anything else that is required.

@CLAassistant

CLAassistant commented Jul 8, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b0ab821194

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

# the report produced here.
# ==============================================================================
if [ "${COVERAGE_DIR:-}" ] && [ "${COVERAGE_OUTPUT_FILE:-}" ]; then
js_coverage_report=$(find "$JS_BINARY__RUNFILES" -path '*/js/private/coverage/coverage.js' 2>/dev/null | head -1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use a single-match search for the coverage tool

When a covered js_test has more than one runfile matching */js/private/coverage/coverage.js (for example, the application also has a file at that path in its data), this find | head -1 pipeline can fail even after finding a match: the launcher runs with set -o pipefail, so head closing the pipe may make find exit with SIGPIPE/141, and the assignment then exits the wrapper under set -e before it returns the test result or writes coverage. Use find ... -print -quit or a known merger runfile path instead.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another robots suggestion for this:

The clean fix: js_binary.bzl already knows the coverage entry point label and merges _lcov_merger runfiles (js_binary.bzl:547-548). Plumb the runfiles path in as a template substitution / env var populated only when hasattr(ctx.attr, "_lcov_merger") — i.e., only for js_test. That's O(1), deterministic, and automatically excludes nested js_binary launchers since their scripts are expanded per-target at analysis time.

Comment on lines +496 to +497
"$JS_BINARY__NODE_BINARY" "$js_coverage_report" ||
logf_error "coverage report generation failed"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail coverage when report generation fails

If the in-test c8 conversion fails here (bad manifest/source path, missing tool, or a tool crash), the launcher only logs the error and still exits with the test's original status. In split coverage postprocessing, the later merger is exactly the action that lacks the instrumented sources, so this can turn a coverage-generation failure back into a successful bazel coverage run with empty or stale coverage instead of surfacing the failure as the old merger path did.

Useful? React with 👍 / 👎.


# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did this need to change?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the aspect format (shfmt) applied this automatically. It's purely cosmetic. We can revert , but it will resurface the next time someone touches the file.

# ==============================================================================
# Generate the lcov coverage report here in the test action, where the V8
# coverage data (NODE_V8_COVERAGE) and the instrumented sources both exist and
# their paths match. The _lcov_merger runs in a separate action that does not

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

robot says:

The fix only works because js_binary.bzl:538-548 merges _lcov_merger runfiles into the test's runfiles, and that block is annotated "TODO: Remove once bazel<8 support is dropped." After this PR, removing it would silently resurrect the 0% bug (the find misses → fallback → merger without sources). The comment needs updating so nobody executes that TODO.

Which makes me think this might not be the right approach for this...?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: bazel coverage reports 0% for jest_test on rules_js 3.x (works on 1.x)

3 participants