Skip to content

PROF-16533: Merged test coverage across CI-tested Linux platforms and JVMs - #948

Open
zhengyu123 wants to merge 7 commits into
mainfrom
zgu/test-coverage-skill
Open

zhengyu123 wants to merge 7 commits into
mainfrom
zgu/test-coverage-skill

Conversation

@zhengyu123

@zhengyu123 zhengyu123 commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?:

Adds a merged C++ and Java coverage report across the Linux platforms and JVMs that CI tests.

  • utils/run-containers-tests.sh --coverage-out=DIR runs :ddprof-lib:coverageReport -PenableCoverage for one container cell. It exports the reports, the JaCoCo .exec files, the classes they were recorded against and a cell.properties file (libc, arch, jdk, tests filter, commit, Gradle exit code).
  • coverageReport also writes a gcovr JSON tracefile, reports/coverage/cpp/coverage.json.
  • utils/merge-coverage.sh merges cells built from one commit into one gcovr report and one JaCoCo report, plus summary.md.
  • The test-coverage agent skill runs the CI matrix (test_workflow.yml): one worktree and one subagent per libc×arch, capped by container memory, then the merge.

Other changes to run-containers-tests.sh:

Change Why
Clone mode works from a git worktree The worktree's .git names a gitdir in the main repository; the script mounts that git dir read-only
JDK images carry a java-profiler.base-id label and are rebuilt when the base image changes A JDK image cached on an older base image hid the rebuilt base's gcovr, even after --rebuild-base on another JDK
--tests accepts only letters, digits and _ . * # , -, in every mode The pattern is spliced unquoted into a host-side eval; a quote in it could run commands on the host

With -Ptests, the coverage test tasks no longer fail on an empty match (ddprof-test/build.gradle.kts). A pattern usually matches tests of only one of testCoverage and testSlowCoverage.

Motivation:

One coverageReport run covers one libc, one architecture and one test JVM. Code that only runs elsewhere (aarch64 stack walking, musl paths, OpenJ9 and GraalVM support) shows as not covered. A merged report shows what the CI-tested matrix actually executes.

Additional Notes:

  • Behavior change for all users: --tests now rejects patterns with other characters, e.g. spaces, $ or ;. Such patterns could not work reliably before.
  • One-time cost: cached JDK images carry no base-id label, so each is rebuilt once on first use.
  • Not covered: CI's 8-orcl and 8-ibm (no container image) and macOS. The C++ report covers the -O0 -DDEBUG build only (see doc/building/CodeCoverage.md).
  • Merge host: needs a gcovr at least as new as the containers'. gcovr 8.6 rejects the format-0.15 tracefiles written by the containers' gcovr 8.8.
  • Agentic review: sphinx reviewed the full range, with 31 findings and none Critical or High. /code-review then re-reviewed each fix commit; the last pass was clean. Declined findings:
Finding Reason
Move orchestration into the script's matrix mode The skill's per-platform subagents and memory slots are the requested design
Narrow safe.directory "*" in worktree mode The ownership failure does not reproduce under podman; "*" applies only inside the throwaway container
--rebuild-base should not force a JDK image rebuild Keeps the script's documented behavior
Pin gcovr in the base images Left as a follow-up; the skill checks the host's gcovr version against the images' instead
Gradle-level tests for coverage.json and the empty-match filter No Gradle test harness exists for this; the real container run below exercised both

How to test the change?:

  • Hermetic: bash utils/tests/test_coverage_scripts.sh has 103 tests and runs in CI with utils/tests/*.sh. Stub podman, gcovr, java and curl programs record their arguments. The container command itself runs on the host against a stub Gradle and checks exit codes, cell.properties and the export.
  • Real run: 14 native aarch64 cells ran with tests=JavaProfilerTest: 9 glibc (HotSpot 17/21/25, OpenJ9 8/11/17, GraalVM 17/21/25) and 5 musl (8–25). All passed and merged into one report.
    • Merged C++ lines: 68.1%. glibc cells alone: 66.6%; musl cells alone: 65.6%.
    • The run used commit 0825b2dcb; the review fixes after it are covered by the hermetic tests only.
    • The x64 cells did not run: this host's container VM has memory for one cell at a time, and x64 runs emulated there.

For Datadog employees:

  • If this PR touches code that signs or publishes builds or packages, or handles
    credentials of any kind, I've requested a security review (run the dd:platform-security-review
    skill, or file a request via the PSEC review form).
    bewaire also runs automatically on every PR.
  • This PR doesn't touch any of that.
  • JIRA: PROF-16533

🤖 Generated with Claude Code

zhengyu123 and others added 7 commits November 6, 2026 10:10
Run :ddprof-lib:coverageReport for each CI-tested libc/arch/JVM cell in a
container and merge the results into one C++ and one Java report.

- run-containers-tests.sh: --coverage-out=DIR runs the coverage report for
  one cell and exports reports, JaCoCo data, classes and cell.properties;
  clone mode works from a git worktree
- coverageReport also writes a gcovr JSON tracefile
- merge-coverage.sh merges cells built from one commit
- hermetic tests for both scripts

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Coverage test tasks do not fail on an empty -Ptests match, so a filter
  that matches only slow or only regular tests still reaches the reports
- run-containers-tests.sh labels each JDK image with its base image ID and
  rebuilds it when the base image changed
- test-coverage skill: memory preflight caps parallel cells at one per
  8 GB, checks base images for gcovr, and counts a killed Gradle daemon
  as blocked instead of failed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- --coverage-out replaces the previous export, records the -Ptests
  filter, exits 70 when Gradle passed but the export failed, rejects an
  empty directory and creates it only for an accepted run
- --rebuild-base again forces the JDK image rebuild
- merge-coverage.sh replaces --out, rejects cells without a commit, marks
  per cell whether it is in the C++ and Java reports, flags filtered runs
  and checks the downloaded JaCoCo CLI against a pinned SHA-256
- tests run the container command with a stub Gradle and cover the new
  merge and image-reuse branches
- test-coverage skill clears a cell before each attempt, checks RUN
  against colima's $HOME share, and reports cells missing from a report

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- --coverage-out clears the previous export on the host before the run,
  so a run that dies before the export leaves no stale data, and rejects
  --tests patterns that would break the spliced container command
- merge-coverage.sh refuses an --out that is not empty or an earlier
  merge, and marks a cell in the Java column only when it contributed
  .exec files to a report that was written
- test-coverage skill: the $HOME check covers podman machine too, and the
  merge snippet sets the worktree it uses

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- run-containers-tests.sh accepts only letters, digits and _ . * # , - in
  --tests, in every mode: the pattern is spliced unquoted into the
  host-side eval and the container shell, so other characters could run
  commands on the host or inject cell.properties keys
- the host-side clearing of a previous export is best effort; with a
  rootful runtime the in-container export clears root-owned files
- merge-coverage.sh marks its output directory, so a merge that failed
  midway can be rerun into the same --out, and links a report in
  summary.md only when it was written

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@zhengyu123
zhengyu123 requested a review from a team as a code owner November 6, 2026 20:34
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 6, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-06T20:41:41.458500Z 9a545ac PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@dd-octo-sts

dd-octo-sts Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

Scan-Build Report

User:runner@runnervm7aw5e
Working Directory:/home/runner/work/java-profiler/java-profiler/ddprof-lib/src/test/make
Command Line:make -j4 all
Clang Version:Ubuntu clang version 18.1.3 (1ubuntu1)
Date:Fri Nov 6 20:38:19 2026

Bug Summary

Bug TypeQuantityDisplay?
All Bugs2
Logic error
Dereference of null pointer1
Unused code
Dead initialization1

Reports

Bug Group Bug Type ▾ File Function/Method Line Path Length
Unused codeDead initializationreferenceChains.cppapplyOomRamp4971
Logic errorDereference of null pointerperfEvents_linux.cppunregisterThread72026

@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: 9a545ac893

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// -Ptests filters both coverage tasks, and a pattern usually matches tests of only one
// (slow or not); an empty match must not stop the build before the reports.
if (project.hasProperty("tests")) {
filter.isFailOnNoMatchingTests = false

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 when both filtered coverage tasks match no tests

When a -Ptests typo or stale pattern matches neither testCoverage nor testSlowCoverage, both tasks now succeed with zero tests, so the coverage run can exit successfully without Java test coverage. Gradle's filter normally fails when no tests match (TestFilter documentation); keep allowing an empty match in one task for fast/slow-only patterns, but fail if neither task matched.

Useful? React with 👍 / 👎.

Comment thread utils/merge-coverage.sh
Comment on lines +68 to +69
source_commit=$(git -C "$SOURCE_ROOT" rev-parse HEAD 2>/dev/null || true)
if [[ "$source_commit" != "$COMMIT" ]]; then

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 Reject a dirty source root before merging coverage

If --source-root has edits to the C++ or Java sources, HEAD still matches the commit recorded by the cells, so this check passes and reports are generated against modified sources rather than the snapshot the cells built. That can misattribute coverage when source lines or class contents changed; require the relevant source tree to be clean or report against the recorded commit snapshot.

Useful? React with 👍 / 👎.

@datadog-official datadog-official 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.

Bits Code Review: FAIL

Two of the most critical issues are a Git mount/clone path overlap that causes every worktree coverage cell to fail before testing, and an unescaped eval of the coverage output path that allows shell injection and silently redirects exports to the wrong location. A separate gcovr version mismatch between the preflight-checked host and the unpinned container image can abort coverage merging after the full matrix completes.

Open Bits AI session

🤖 Bits Code Review · Commit 9a545ac

GIT_COMMON_DIR=""
if [[ -f "$PROJECT_ROOT/.git" ]]; then
GIT_COMMON_DIR=$(git -C "$PROJECT_ROOT" rev-parse --path-format=absolute --git-common-dir)
CONTAINER_CMD="$CONTAINER_CMD -v \"$GIT_COMMON_DIR\":\"$GIT_COMMON_DIR\"${CONTAINER_VOLUME_RO_OPTIONS}"

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 Keep worktree Git mounts outside the clone destination

When the main checkout is under /workspace, such as /workspace/repo, this mount creates /workspace/repo/.git inside the container before cloning. Git then rejects /workspace because it is nonempty, so every worktree coverage cell fails before testing. The mounted metadata and clone destination must not overlap; relocating either requires updating the corresponding Git resolution or build/export paths.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

After all four agents send their final reports, merge every cell that has
`cell.properties`. A `failed` cell still holds the Java coverage of the tests that ran,
but a failed gtest stops the build before the C++ report, so such a cell has no C++
tracefile. `summary.md` marks, per cell, whether it is in each report.

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 Recheck gcovr compatibility after building base images

If a base image is missing or rebuilt, preflight does not check its eventual gcovr version, while the runner installs unpinned gcovr. A host that passes preflight can therefore be incompatible with newly generated tracefiles—for example, host 8.6 versus image 8.8—causing merging to abort after the matrix finishes. Recheck the resulting images and update the isolated host environment before merging.

Suggested change
tracefile. `summary.md` marks, per cell, whether it is in each report.
tracefile. `summary.md` marks, per cell, whether it is in each report.
Before merging, repeat step 1's gcovr version check on all resulting base images,
including images created or rebuilt during step 3. Compare the newest version with
`gcovr --version` on the host. If the host is older or missing, create or update
`$RUN/tools/venv` with `gcovr==<newest base image version>` and put
`$RUN/tools/venv/bin` first on `PATH` for the merge.

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

CONTAINER_CMD="$CONTAINER_CMD -e TEST_CONFIGURATION=$LIBC/${JDK_VERSION}-$CONFIG-$ARCH"
CONTAINER_CMD="$CONTAINER_CMD -e GRADLE_USER_HOME=/gradle-cache"
if [[ -n "$COVERAGE_OUT" ]]; then
CONTAINER_CMD="$CONTAINER_CMD -v \"$COVERAGE_OUT\":/coverage-out${CONTAINER_VOLUME_RW_OPTIONS}"

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 Shell-escape the coverage output path before eval

A literal --coverage-out='/tmp/coverage-$(printf EXECUTED >&2)' creates the requested directory, but the final host-side eval executes the substitution and mounts /tmp/coverage- instead. Double quotes in directory names can also break parsing. Shell-escape the complete mount argument so valid directory names remain literal and exports reach the requested location.

Suggested change
CONTAINER_CMD="$CONTAINER_CMD -v \"$COVERAGE_OUT\":/coverage-out${CONTAINER_VOLUME_RW_OPTIONS}"
printf -v CONTAINER_CMD '%s -v %q' "$CONTAINER_CMD" "${COVERAGE_OUT}:/coverage-out${CONTAINER_VOLUME_RW_OPTIONS}"

React: 👍 useful · 👎 wrong · 😕 unclear · 👀 right, but not acting on it

What each reaction means
  • 👍 Useful: correct, and you're fixing it or will.
  • 👎 Wrong: the finding is incorrect, for example the bug can't happen or the code was misread.
  • 😕 Unclear: you can't tell what it means or why it matters.
  • 👀 Right, not acting: correct, but intended, out of scope for this PR, a known risk, or too minor.

🤖 Bits Code Review · Open Bits AI session

@dd-octo-sts

dd-octo-sts Bot commented Nov 6, 2026

Copy link
Copy Markdown
Contributor

CI Test Results

Run: #42289533870 | Commit: 7af36f1 | Duration: 19m 58s (longest job)

✅ All 32 test jobs passed

Status Overview

JDK glibc-aarch64/debug glibc-amd64/debug musl-aarch64/debug musl-amd64/debug
8 - ✅ - -
8-ibm - ✅ - -
8-j9 ✅ ✅ - -
8-librca - - ✅ ✅
8-orcl - ✅ - -
11 - ✅ - -
11-j9 ✅ ✅ - -
11-librca - - ✅ ✅
17 ✅ ✅ - -
17-graal ✅ ✅ - -
17-j9 ✅ ✅ - -
17-librca - - ✅ ✅
21 ✅ ✅ - -
21-graal ✅ ✅ - -
21-librca - - ✅ ✅
25 ✅ ✅ - -
25-graal ✅ ✅ - -
25-librca - - ✅ ✅

Legend: ✅ passed | ❌ failed | ⚪ skipped | 🚫 cancelled

Summary: Total: 32 | Passed: 32 | Failed: 0


Updated: 2026-11-06 20:56:43 UTC

This branch has not been deployed

No deployments
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.

1 participant