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
141 changes: 141 additions & 0 deletions .agents/skills/test-coverage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
---
name: test-coverage
description: Measure C++ and Java test coverage of origin/main across every Linux platform (glibc/musl × x64/aarch64) and test JVM CI tests there (HotSpot, OpenJ9, GraalVM), one worktree and subagent per libc×arch, then merge everything into one report. Use when asked for full, merged or cross-platform coverage.
---

# Test coverage across Linux platforms

Each cell runs `:ddprof-lib:coverageReport -PenableCoverage` in a container through
`utils/run-containers-tests.sh --coverage-out`; `utils/merge-coverage.sh` merges the
cells. Read [doc/building/CodeCoverage.md](../../../doc/building/CodeCoverage.md)
first: the C++ numbers cover the `-O0 -DDEBUG` build only. macOS is not part of the
matrix. Optional arguments: `ref=<git ref>` (default `origin/main`) and
`tests=<pattern>` (passed as `--tests`, for a quick smoke run).

## 1. Preflight

Stop and report on the first failure:
- A container runtime answers: `podman info`, else `docker info`. Use it as `RT` for
every cell. Docker on macOS needs `colima start` first.
- The host has `java`, `python3` and `curl` on `PATH`, and a gcovr at least as new as the one in the base
images (checked below): an older gcovr rejects the cells' tracefiles ("Wrong format
version"). If the host's is older or missing, do not change the user's Python;
after step 2 create `python3 -m venv "$RUN/tools/venv"`, install
`gcovr==<base image version>` into it, and put `$RUN/tools/venv/bin` first on `PATH`
in step 4. Both are needed only for step 4, but check now.
- Free disk: about 3 GB per worktree plus the container images.
- Memory: `RT info --format '{{.Host.MemTotal}}'` (podman) or `'{{.MemTotal}}'`
(docker), in bytes. Gradle, the `-O0` compile and the test JVMs of one cell need
about 8 GB; with less, the Gradle daemon is killed ("Gradle build daemon
disappeared unexpectedly"). Set `SLOTS` = memory / 8 GB, at most 4. If it is 0,
stop and ask the user to give the VM more memory (`podman machine set --memory`,
`colima start --memory`).
- Other containers: list `RT ps`. Running containers share that memory; ask the
user before stopping any.
- Base images: for each `java-profiler-base:<libc>-<arch>` that exists, run
`RT run --rm --platform linux/<amd64|arm64> <image> gcovr --version`; note the
newest version for the host check above. If it fails, that agent's first cell must
pass `--rebuild-base`; the script then rebuilds the JDK images on top of it.

The non-native architecture runs emulated (QEMU or Rosetta) and takes several times
longer than the ~30 minutes of a native cell. Tell the user the slot count and that
estimate before starting.

## 2. Worktrees

```bash
git fetch origin main
COMMIT=$(git rev-parse <ref>)
RUN=$(pwd)/build/test-coverage/$(git rev-parse --short=12 $COMMIT)-$(date -u +%Y%m%dT%H%M%SZ)
for c in glibc-x64 glibc-aarch64 musl-x64 musl-aarch64; do
git worktree add --detach "$RUN/wt-$c" $COMMIT
done
```

On macOS, check that `RUN` starts with `$HOME/` and stop if it does not: colima and
podman machine share `$HOME` with their VM by default, and a cell directory the VM
cannot see makes every cell `blocked`.
Check that the worktree's `utils/run-containers-tests.sh` accepts `--coverage-out`
(`grep -q -- --coverage-out`); if it does not, the ref predates coverage mode — stop.

## 3. Subagents

Run at most `SLOTS` subagents at a time, in the background (general-purpose): launch
the first `SLOTS` in one message, and launch the next one when an agent sends its
final per-cell report (interim "waiting" notifications do not free a slot). Start
with the emulated architecture's agents: they take longest.
Cells per agent: the JVMs CI tests on each platform (`test_workflow.yml` matrices).
CI's `8-orcl` and `8-ibm` (glibc x64) have no container image and are not covered;
say so in the report. Keep this table in sync when the CI matrices change.

| Agent | JDKs |
|---|---|
| glibc-x64 | 8 8-j9 11 11-j9 17 17-j9 17-graal 21 21-graal 25 25-graal |
| glibc-aarch64 | 8-j9 11-j9 17 17-j9 17-graal 21 21-graal 25 25-graal |
| musl-x64 | 8 11 17 21 25 |
| musl-aarch64 | 8 11 17 21 25 |

Give each agent this brief, filled in:

> Run java-profiler coverage cells for libc=`<libc>`, arch=`<arch>`, one at a time,
> in worktree `<RUN>/wt-<libc>-<arch>`. JDKs, in order: `<list>`. Runtime: `<RT>`.
> For each JDK `<jdk>`, let `<cell>` be `<RUN>/cells/<libc>-<arch>-<jdk>` and `<log>`
> be `<RUN>/logs/<libc>-<arch>-<jdk>.log` (absolute paths; `mkdir -p <RUN>/logs`).
> Before every start, retries included, run `rm -rf <cell> <cell>.exit` so the wait
> below cannot end on an earlier attempt's result. Start the cell detached so no tool
> timeout kills it:
> `cd <wt> && nohup bash -c './utils/run-containers-tests.sh --container=<RT>
> --libc=<libc> --arch=<arch> --jdk=<jdk> --coverage-out=<cell> [--tests=<pattern>];
> echo $? > <cell>.exit' > <log> 2>&1 &`.
> Then wait until `<cell>.exit` exists (Monitor with an until-loop, or a background
> `while [ ! -f <cell>.exit ]; do sleep 60; done`, re-armed if it times out). Do not
> start the next cell before the current one finishes.
> Classify each cell from the exit code, `<cell>/cell.properties` and the log:
> `passed` (exit 0); `failed` (a task failed on its own terms: compile error, test
> failures, gtest failure — name the task and tests); `blocked` (no
> `cell.properties`: image build, download or container failure; or Gradle was
> killed: "Gradle build daemon disappeared unexpectedly", exit 137, `Killed`; or exit
> 70: the export to `<cell>` failed). If
> the log says a coverage tool is missing (`checkCoverageTools`), the cached base
> image predates it: rerun that cell once with `--rebuild-base`. Retry any other
> `blocked` cell once. Do not edit code, change tests or retry `failed` cells. Never
> print more than 30 log lines. Never put the word "gradlew" in a shell command, not
> even in a grep pattern: a hook blocks it — search logs for `What went wrong` or
> `FAILED` instead.
> Report one line per cell: jdk, status, gradle_exit, failing task/tests or the
> blocking error, log path.

While they run, answer the user from their notifications only; if asked for
progress, list which `$RUN/cells/*.exit` files exist.

## 4. Merge

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


```bash
WT=$(ls -d "$RUN"/wt-* | head -1) # any of the run's worktrees
cells=(); for p in "$RUN"/cells/*/cell.properties; do cells+=("${p%/cell.properties}"); done
"$WT/utils/merge-coverage.sh" --source-root="$WT" --out="$RUN/merged" "${cells[@]}"
```

If gcovr rejects a tracefile, report the gcovr error and the cell; do not drop the
cell silently.

## 5. Report

- The commit, and a per-cell table: libc, arch, jdk, `passed`/`failed`/`blocked`,
failing tests or blocking error.
- The merged totals from `$RUN/merged/summary.md`, and paths to
`merged/cpp/index.html` and `merged/java/html/index.html`.
- Which cells are missing from the merge or from one of its reports (the C++ and
Java columns of `summary.md`) and why, and that every `failed` or missing cell
lowers the numbers. Say so when `tests=` filtered the run. The C++ report is the
debug build only.

Leave `$RUN` in place. Offer to remove the worktrees
(`git worktree remove "$RUN"/wt-*`); `cells/`, `logs/` and `merged/` stay until the
user deletes them. On Linux with rootful Docker the container writes `cells/` as
root; remove them with `sudo` or from a container.
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ dependency-free leak-demo app; not published).
Configurations: `release`, `debug`, plus `asan`/`tsan`/`fuzzer` when the sanitizer runtime
is found for the selected compiler (inactive on macOS and musl), plus opt-in `coverage`
with `-PenableCoverage` (`:ddprof-lib:coverageReport`; the C++ report covers only the
`-O0 -DDEBUG` build — see [doc/building/CodeCoverage.md](doc/building/CodeCoverage.md)).
`-O0 -DDEBUG` build — see [doc/building/CodeCoverage.md](doc/building/CodeCoverage.md);
merged coverage across the CI-tested Linux platforms and JVMs: the `test-coverage` skill).

```bash
./.claude/commands/build-and-summarize buildRelease # release library + JAR
Expand Down
2 changes: 2 additions & 0 deletions ddprof-lib/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,8 @@ if (project.hasProperty("enableCoverage")) {
"--gcov-executable", gcovExecutable,
"--html", "--html-details",
"-o", "${outDir.absolutePath}/index.html",
// Tracefile for merging runs from several platforms (gcovr --add-tracefile).
"--json", "${outDir.absolutePath}/coverage.json",
"--print-summary",
// Safety net only: -fprofile-update=atomic (see configureCoverage) should keep
// counters consistent, but a stray negative count must not discard a
Expand Down
5 changes: 5 additions & 0 deletions ddprof-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ tasks.withType<Test>().named { it in coverageTaskNames }.configureEach {
environment("JACOCO_CHILD_DIR", childJacocoDir.get().asFile.absolutePath)
environment("JACOCO_INCLUDES", jacocoIncludes)
}
// -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 👍 / 👎.

}
}

// On musl the profiler-test plugin creates the test tasks as Exec tasks (running its own
Expand Down
30 changes: 21 additions & 9 deletions doc/building/CodeCoverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

| Report | Tool | Sources | Output |
|--------|------|---------|--------|
| C++ | gcovr | gtest binaries + Java integration tests | `ddprof-lib/build/reports/coverage/cpp/index.html` |
| C++ | gcovr | gtest binaries + Java integration tests | `ddprof-lib/build/reports/coverage/cpp/index.html`, tracefile `.../cpp/coverage.json` |
| Java | JaCoCo | Java integration tests (incl. child JVMs) | `ddprof-lib/build/reports/coverage/java/html/index.html`, `.../java/jacoco.xml` |

Coverage is opt-in. Pass `-PenableCoverage`, or none of the coverage tasks exist.
Expand Down Expand Up @@ -105,15 +105,27 @@ failing gtest stops the build before the report.

`utils/run-containers-tests.sh` base images include gcovr from PyPI, and `llvm` on
glibc for the unversioned `llvm-cov`. The script reuses cached images, so a base image
cached before these packages were added lacks them, and so does every JDK test image
built on it. Pass `--rebuild-base` once: it rebuilds the base image and the JDK image
on top of it.
cached before these packages were added lacks them. Pass `--rebuild-base` once per
libc and architecture. Each JDK image records the ID of the base image it was built
on, and the script rebuilds it when that base image changes.

The script's `--config` does not accept `coverage`. Open a shell in the container and
run the task there:
`--coverage-out=DIR` runs the report for one cell and copies the reports, the JaCoCo
`.exec` files, the classes they were recorded against and a `cell.properties` file to
`DIR` (see [utils/README.md](../../utils/README.md)). A failing gtest stops the build
before the report, so that cell exports no C++ tracefile.

```bash
./utils/run-containers-tests.sh --libc=glibc --jdk=21 --rebuild-base --shell
# inside the container:
./gradlew :ddprof-lib:coverageReport -PenableCoverage
./utils/run-containers-tests.sh --libc=musl --arch=aarch64 --jdk=21 --coverage-out=build/cov/musl-aarch64-21
```

## Merging Platforms

One run covers one libc, architecture and test JVM. Code that only runs on another
platform (aarch64 stack walking, musl paths, J9 support) shows as not covered.
`utils/merge-coverage.sh` merges `--coverage-out` directories built from one commit:
gcovr merges the `coverage.json` tracefiles, and the JaCoCo CLI merges the `.exec`
files and reports against one cell's classes. Every container builds with JDK 21, so
the class files match across cells; the script warns when they differ. The host's
gcovr must be at least as new as the containers': an older one rejects their
tracefiles with "Wrong format version". The
`test-coverage` skill runs the full Linux matrix and merges it.
5 changes: 3 additions & 2 deletions doc/building/TestingGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -335,8 +335,9 @@ The C++ report covers only the `-O0 -DDEBUG` build. Prerequisites and limits are
(ddtool) fails outside the CI network and aborts container commands. Run `./utils/run-containers-tests.sh --help` for the full flag list
(`--tests`, `--gtest`, `--gtest-task`, `--mount`, `--rebuild`, `--matrix`, ...).
Cached images are reused until `--rebuild-base` (or `--rebuild`) is passed. Use it
after the base image's package list changes, e.g. to pick up gcovr for coverage runs:
it rebuilds the base image and the JDK image built on it.
after the base image's package list changes, e.g. to pick up gcovr for coverage runs.
A JDK image is also rebuilt when it was built on another base image than the current
one (the `java-profiler.base-id` label), so every JDK picks up the rebuilt base.

```bash
# Matches the nightly configuration
Expand Down
13 changes: 12 additions & 1 deletion utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,12 +175,13 @@ Runs tests in containers across various OS/libc/JDK combinations, mirroring the
--arch=x64|aarch64|all[,..] (default: auto-detect)
--config=debug|release|asan|tsan|all[,..] (default: debug)
--container=podman|docker (default: podman)
--tests="TestPattern" (optional)
--tests="TestPattern" (optional; letters, digits and _ . * # , -)
--gtest (enable C++ gtests)
--gtest-task=Task (run one C++ gtest task)
--shell (drop to shell instead of running tests)
--mount (mount local repo instead of cloning)
--rebuild (force rebuild of container images)
--coverage-out=DIR (single cell: coverageReport, exported to DIR)
--matrix (preview a full matrix)
--run (execute an inferred matrix without prompting)
--fail-fast (stop matrix execution on first failure)
Expand All @@ -206,6 +207,16 @@ Examples:

Single-value commands run one container test configuration immediately. When any dimension expands to multiple cells, the script prints a compact status table first; interactive terminals ask for confirmation, while non-interactive runs require `--run` to execute. Matrix execution prints the status table again after all cells finish and writes summaries to `build/reports/container-matrix/summary.md` and `build/reports/container-matrix/summary.json`. Matrix gtest runs require a short `--gtest-task` name so the task follows each cell's configuration. Cells not run because of `--fail-fast` are reported as cancelled separately from unsupported cells that are skipped.

`--coverage-out=DIR` runs `:ddprof-lib:coverageReport -PenableCoverage` for one cell and replaces in `DIR` the reports (`reports/`, including the gcovr tracefile `reports/cpp/coverage.json`), the JaCoCo `.exec` files (`jacoco/`), the classes they were recorded against (`classes/`) and `cell.properties` (libc, arch, jdk, tests filter, commit, Gradle exit code). The export also runs when Gradle fails; the script still exits with Gradle's code, or with 70 when Gradle passed but the export failed. The script can run from a git worktree: it also mounts the main repository's git dir, read-only.

### `merge-coverage.sh`

Merges `--coverage-out` directories from several cells into one C++ report (gcovr `--add-tracefile`) and one Java report (JaCoCo CLI `merge` + `report`), plus `summary.md`. All cells and `--source-root` must be at the same commit. Needs `java`, `python3` and a gcovr at least as new as the containers' (an older one rejects their tracefiles); the JaCoCo CLI comes from `$JACOCO_CLI` or is downloaded from Maven Central with `curl` and checked against a pinned SHA-256. `--out` is replaced on each run, and `summary.md` marks which cells each report includes. The `test-coverage` skill drives the full Linux matrix and this merge.

```bash
./utils/merge-coverage.sh --source-root=. --out=build/coverage-merged cells/glibc-x64-21 cells/musl-aarch64-17
```

### `check-signal-reachability.py`

Static async-signal-safety check. Builds a call graph from the relocations of the debug object files (`objdump -dr`) and reports every function reachable from a signal handler or malloc hook that calls an unsafe symbol (heap allocation, locks, stdio, dynamic-loader calls, dynamic TLS, logging). The entry points, unsafe symbols and known findings are data lists at the top of the script. ELF objects only (Linux x64/aarch64); needs GNU `objdump` and `c++filt`.
Expand Down
Loading
Loading