Skip to content

Commit ea3fcec

Browse files
feat(restheart-mongo): real Java line coverage via JaCoCo overlay
Replaces the prior API-route-surface "coverage" (counting fired routes / curated route table) with actual JaCoCo line coverage of the RESTHeart 9.x JVM under traffic. Architecture: - `Dockerfile.coverage` is a multi-stage build: stage 1 (alpine) fetches JaCoCo 0.8.13 (jacocoagent.jar + jacococli.jar), stage 2 layers them into the upstream restheart image (which is distroless — no shell, no curl, so jars must be pulled in a builder stage and COPY'd over). - `docker-compose.coverage.yml` is an OVERLAY: applied via `-f docker-compose.yml -f docker-compose.coverage.yml`. It sets JAVA_TOOL_OPTIONS=-javaagent:.../jacocoagent.jar=output=tcpserver,... so JaCoCo attaches at JVM start and listens on port 6300. The base `Dockerfile` and `docker-compose.yml` are untouched, so keploy/integrations and keploy/enterprise CI lanes consume the base compose and pay zero JaCoCo cost (the agent rewrites bytecode at class-load, adding ~5-10% per-call overhead that would slow record/replay). - `flow.sh::restheart_report_coverage` shells into a one-off coverage container to dump execution data via JaCoCo TCP and render an XML report against /opt/restheart/restheart.jar. When called against the base image (no overlay) it prints "INFO: ... uninstrumented" and exits 0 so enterprise lanes' `flow.sh coverage || true` informational calls keep working. Also fixes a pre-existing config bug in the base docker-compose.yml's RHO env var: the override syntax uses ';' as a key->value separator (the upstream image's default RHO uses ';'); the prior YAML-folded version used ',' which RESTHeart parsed as part of the connection-string value, leading RESTHeart to ignore the override and bind /http-listener/host to its localhost default — making the HTTP listener unreachable from the host port mapping. The base compose now uses ';' AND explicitly overrides /http-listener/host -> "0.0.0.0". Removed: - `restheart_list_routes` (curated route table denominator). - `restheart_list_recorded_routes` (keploy-tests / fired-routes reader). - The legacy route-surface `restheart_report_coverage` body. - `list-routes` subcommand. Validated locally: helper produced `coverage=52.3` to GITHUB_OUTPUT against a clean stack (1663/3182 lines covered in restheart.jar; INSTRUCTION coverage 50.8%). Signed-off-by: Akash Kumar <meakash7902@gmail.com>
1 parent edc9f0a commit ea3fcec

7 files changed

Lines changed: 196 additions & 191 deletions

File tree

.github/workflows/restheart-mongo.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -194,4 +194,4 @@ jobs:
194194
195195
Threshold: PR may not drop coverage by more than **${{ env.COVERAGE_THRESHOLD }}pp**. Override per-repo via the `RESTHEART_COVERAGE_THRESHOLD` actions variable.
196196
197-
Coverage measures the RESTHeart 9.x REST surface (`/{db}/{coll}` CRUD + `_aggrs/{name}` + `_size` + `_meta` + `_indexes` + `_streams/{name}` + `/graphql` + `/graphql/{appname}` + `/{db}/{coll}.files` + `/acl` + `/users` + `/tokens` + sessions/transactions + `/ic` + `/csv` + metrics + OAuth) that `flow.sh::restheart_record_traffic` exercises against the running backend. Reports are attached as artifacts on each job ("coverage-build" / "coverage-release").
197+
Coverage is **Java line coverage** (JaCoCo 0.8.13) of the RESTHeart 9.x JVM under traffic — the bytecode `flow.sh::restheart_record_traffic` actually executes (REST CRUD + GraphQL + ACL + users + sessions/transactions + metrics + …). Instrumentation lives in a separate `Dockerfile.coverage` + `docker-compose.coverage.yml` overlay; the base `docker-compose.yml` consumed by keploy/integrations and keploy/enterprise CI lanes runs uninstrumented and pays zero JaCoCo cost. JaCoCo execution dumps + XML reports are attached as artifacts on each job (`coverage-build` / `coverage-release`).
Lines changed: 33 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,72 +1,43 @@
11
#!/usr/bin/env bash
22
#
3-
# run-and-measure.sh — bring restheart-mongo up via the sample's
4-
# compose, run flow.sh bootstrap + record-traffic with the
5-
# per-call audit log enabled, run flow.sh coverage, and emit
6-
# `coverage=PCT` onto $GITHUB_OUTPUT for the downstream
7-
# coverage-gate job.
3+
# run-and-measure.sh — bring restheart-mongo up under the
4+
# coverage overlay (JaCoCo agent attached via JAVA_TOOL_OPTIONS),
5+
# run flow.sh bootstrap + record-traffic, dump JaCoCo execution
6+
# data over the agent's TCP server, render a Java line-coverage
7+
# report, and emit `coverage=PCT` onto $GITHUB_OUTPUT for the
8+
# downstream coverage-gate job.
89
#
9-
# Called from .github/workflows/restheart-mongo.yml's
10-
# build-coverage and release-coverage jobs (one per ref under
11-
# comparison). Both jobs source the same script so the
12-
# measurement is identical across refs — any drift in the
13-
# numerator definition would otherwise produce a misleading
14-
# delta.
10+
# Coverage isolation contract:
11+
# * Base `Dockerfile` and `docker-compose.yml` are untouched.
12+
# * The overlay `Dockerfile.coverage` + `docker-compose.coverage.yml`
13+
# attach JaCoCo and expose its TCP server. ONLY this script
14+
# applies the overlay; keploy/integrations and keploy/enterprise
15+
# CI lanes consume the base compose and pay zero JVM-instrument
16+
# cost (jacocoagent adds ~5-10% per-call overhead).
1517
#
16-
# Inputs (all from the workflow env):
17-
# RESTHEART_FIRED_ROUTES_FILE — per-call audit log path; passed
18-
# through to flow.sh so its
19-
# record-traffic loop logs each
20-
# (METHOD, URL) pair, and so its
21-
# coverage subcommand uses that
22-
# file as the standalone
23-
# numerator.
24-
# RESTHEART_PHASE — label spliced into the project
25-
# name so build vs. release runs
26-
# don't collide on volume names
27-
# (compose project naming inside
28-
# the GH runner is per-job
29-
# anyway, but RESTHEART_PHASE
30-
# shows up in the test fixtures
31-
# and is useful for diffing logs).
32-
# GITHUB_OUTPUT — standard GH Actions sink for
33-
# step outputs.
18+
# Inputs (from the workflow env):
19+
# RESTHEART_PHASE — label for log diffing.
20+
# GITHUB_OUTPUT — standard GH Actions sink for step outputs.
3421
set -Eeuo pipefail
3522

36-
# Compose-substituted variables. Defaults match the sample's
37-
# docker-compose.yml so a local invocation of this script (no
38-
# overrides) reproduces what CI runs.
3923
export RESTHEART_APP_CONTAINER="${RESTHEART_APP_CONTAINER:-restheart_app}"
4024
export RESTHEART_MONGO_CONTAINER="${RESTHEART_MONGO_CONTAINER:-restheart_mongo}"
4125
export RESTHEART_APP_PORT="${RESTHEART_APP_PORT:-8080}"
4226
export RESTHEART_MONGO_IP="${RESTHEART_MONGO_IP:-172.36.0.10}"
4327
export RESTHEART_NETWORK_SUBNET="${RESTHEART_NETWORK_SUBNET:-172.36.0.0/24}"
44-
45-
# RESTHeart 9.x ships with admin/secret as the default
46-
# bootstrapped principal. flow.sh reads this header for every
47-
# call, so exporting it here keeps the standalone CI run aligned
48-
# with the keploy lanes (which pass the same value through).
4928
export RESTHEART_ADMIN_AUTH="${RESTHEART_ADMIN_AUTH:-Basic YWRtaW46c2VjcmV0}"
5029

51-
: "${RESTHEART_FIRED_ROUTES_FILE:?RESTHEART_FIRED_ROUTES_FILE must be set by the workflow}"
30+
mkdir -p coverage
31+
chmod 777 coverage
32+
sudo rm -rf coverage/jacoco.exec coverage/report.xml coverage/coverage_report.txt 2>/dev/null \
33+
|| rm -rf coverage/jacoco.exec coverage/report.xml coverage/coverage_report.txt 2>/dev/null \
34+
|| true
5235

53-
# Reset audit log for this run; otherwise a prior run's entries
54-
# would inflate the numerator on a re-trigger.
55-
: >"$RESTHEART_FIRED_ROUTES_FILE"
36+
COMPOSE=(docker compose -f docker-compose.yml -f docker-compose.coverage.yml)
5637

57-
# Single-phase bootstrap: RESTHeart embeds its own admin
58-
# principal at first boot, so there's no separate "seed admin
59-
# user" stage the way doccano needs. compose up → wait for app
60-
# port → flow.sh bootstrap (PUTs the db + record-traffic's
61-
# collections) → flow.sh record-traffic → flow.sh coverage.
62-
docker compose up -d
38+
"${COMPOSE[@]}" up -d --build
6339

64-
# Wait for the backend to start serving. Per the sample's
65-
# restheart_wait_for_app, both 200 AND 401 are success signals
66-
# — RESTHeart returns 401 on `/` until you authenticate, but
67-
# 401 still proves the HTTP listener and the auth filter are
68-
# both up. Anything before that (000 / connection refused) is
69-
# pre-listen.
40+
# Both 200 and 401 are success signals.
7041
for i in $(seq 1 120); do
7142
code=$(curl -sS -o /dev/null -w '%{http_code}' \
7243
"http://127.0.0.1:${RESTHEART_APP_PORT}/" 2>/dev/null || echo "")
@@ -80,33 +51,28 @@ if [ "$code" != "200" ] && [ "$code" != "401" ]; then
8051
docker logs "${RESTHEART_APP_CONTAINER}" --tail 200 2>&1 || true
8152
echo "----- mongo container logs -----"
8253
docker logs "${RESTHEART_MONGO_CONTAINER}" --tail 100 2>&1 || true
83-
echo "----- docker compose ps -----"
84-
docker compose ps || true
85-
docker compose down -v --remove-orphans || true
54+
"${COMPOSE[@]}" down -v --remove-orphans || true
8655
exit 1
8756
fi
8857

8958
bash flow.sh bootstrap 240
90-
91-
# Drive traffic. flow.sh::restheart_record_traffic gates on
92-
# restheart_wait_for_app internally, so this won't fire curls
93-
# at a half-booted backend.
9459
bash flow.sh record-traffic
9560

96-
# Coverage report — uses RESTHEART_FIRED_ROUTES_FILE as numerator
97-
# since no keploy/test-set-* tree exists in the standalone case.
61+
# JaCoCo TCP-dump + report (no JVM stop needed).
9862
COVERAGE_REPORT_FILE="$PWD/coverage_report.txt" bash flow.sh coverage
9963

100-
# Pull the percentage out of the report's `Covered N/M (XX.X%)`
101-
# line. Anchored on the parenthesised form so a future change to
102-
# the report's prose doesn't break the parse.
64+
if [ ! -f coverage_report.txt ]; then
65+
echo "::error::flow.sh coverage produced no coverage_report.txt"
66+
exit 1
67+
fi
68+
10369
pct=$(grep -oE '\([0-9]+\.[0-9]+%\)' coverage_report.txt | head -1 | tr -d '()%')
10470
if [ -z "$pct" ]; then
10571
echo "::error::Could not parse coverage percentage from coverage_report.txt"
10672
cat coverage_report.txt || true
10773
exit 1
10874
fi
10975
echo "coverage=${pct}" >>"$GITHUB_OUTPUT"
110-
echo "coverage: ${pct}% (audit log: $RESTHEART_FIRED_ROUTES_FILE)"
76+
echo "coverage: ${pct}% (Java line coverage via JaCoCo)"
11177

112-
docker compose down -v --remove-orphans
78+
"${COMPOSE[@]}" down -v --remove-orphans

restheart-mongo/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
coverage/
2+
coverage_report.txt
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Coverage overlay image for restheart-mongo.
2+
#
3+
# Adds the JaCoCo agent (jacocoagent.jar) and CLI (jacococli.jar)
4+
# alongside the upstream restheart 9.2.1 image. The agent is
5+
# attached at JVM start via JAVA_TOOL_OPTIONS (set in
6+
# docker-compose.coverage.yml) so we don't have to rewrite the
7+
# upstream entrypoint, which is `java -jar restheart.jar` with
8+
# specific JVM flags.
9+
#
10+
# The agent runs in `tcpserver` mode so the workflow can dump
11+
# coverage data on demand without restarting the JVM —
12+
# important for distroless-style upstream images that don't
13+
# ship a shell.
14+
#
15+
# IMPORTANT: this image is only consumed by docker-compose.coverage.yml.
16+
# The base Dockerfile and docker-compose.yml stay uninstrumented so
17+
# enterprise's keploy compat lane pays no JVM-instrumentation cost
18+
# (jacocoagent adds ~5-10% per-call overhead through bytecode
19+
# rewriting, which would slow record/replay measurably).
20+
21+
# Stage 1: pull JaCoCo zip in an alpine builder. The upstream
22+
# restheart image is distroless (no shell, no curl/unzip), so we
23+
# can't fetch JaCoCo from inside it.
24+
FROM alpine:3.19 AS jacoco-fetch
25+
ARG JACOCO_VERSION=0.8.13
26+
RUN apk add --no-cache curl ca-certificates unzip \
27+
&& curl -fsSL "https://repo1.maven.org/maven2/org/jacoco/jacoco/${JACOCO_VERSION}/jacoco-${JACOCO_VERSION}.zip" -o /tmp/jacoco.zip \
28+
&& mkdir -p /tmp/jacoco \
29+
&& unzip -j /tmp/jacoco.zip lib/jacocoagent.jar lib/jacococli.jar -d /tmp/jacoco
30+
31+
# Stage 2: layer JaCoCo into the upstream image. We can't `RUN`
32+
# anything because the base image has no shell — only COPY and
33+
# WORKDIR work. COPY --chown sets ownership at copy time so the
34+
# distroless user (uid 65532) can read the agent.
35+
FROM softinstigate/restheart:9.2.1
36+
COPY --from=jacoco-fetch --chown=65532:65532 /tmp/jacoco/jacocoagent.jar /opt/jacoco/jacocoagent.jar
37+
COPY --from=jacoco-fetch --chown=65532:65532 /tmp/jacoco/jacococli.jar /opt/jacoco/jacococli.jar
38+
39+
# Pre-create /coverage as an empty WORKDIR so docker has a
40+
# mountpoint for the bind-mount in docker-compose.coverage.yml.
41+
# WORKDIR doesn't require a shell.
42+
WORKDIR /coverage
43+
WORKDIR /opt/restheart
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Coverage overlay — applied with:
2+
#
3+
# docker compose -f docker-compose.yml -f docker-compose.coverage.yml up -d --build
4+
#
5+
# Used ONLY by the standalone .github/workflows/restheart-mongo.yml
6+
# CI workflow. Keploy CI lanes (enterprise, integrations) ignore
7+
# this file and run the base compose unchanged, so they pay zero
8+
# JaCoCo-instrumentation cost.
9+
services:
10+
restheart:
11+
build:
12+
context: .
13+
dockerfile: Dockerfile.coverage
14+
image: ${RESTHEART_COVERAGE_IMAGE:-restheart-mongo:local-coverage}
15+
environment:
16+
# Attach the JaCoCo agent in TCP server mode. The upstream
17+
# entrypoint is `java ... -jar restheart.jar`; JAVA_TOOL_OPTIONS
18+
# is read by the JVM and prepended to all java args, so the
19+
# `-javaagent` flag arms before restheart.jar starts loading
20+
# classes.
21+
#
22+
# output=tcpserver: the agent listens on port 6300 inside the
23+
# container and dumps coverage data over TCP on demand. No
24+
# need to stop the JVM to read coverage — the workflow
25+
# connects to 6300, dumps, and the report is generated
26+
# post-hoc by jacococli.
27+
JAVA_TOOL_OPTIONS: "-javaagent:/opt/jacoco/jacocoagent.jar=output=tcpserver,address=0.0.0.0,port=6300,sessionid=keploy,append=false"
28+
ports:
29+
- "${RESTHEART_JACOCO_PORT:-6300}:6300"
30+
volumes:
31+
- ./coverage:/coverage

restheart-mongo/docker-compose.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,14 @@ services:
1212
ports:
1313
- "${RESTHEART_APP_PORT:-8080}:8080"
1414
environment:
15-
RHO: >
16-
/mclient/connection-string->"mongodb://${RESTHEART_MONGO_IP:-172.36.0.10}:27017",
17-
/core/log-level->"INFO"
15+
# RHO is RESTHeart's runtime config-override syntax:
16+
# key->value pairs separated by ';'
17+
# We override the default mongo URL (which the upstream image
18+
# points at host.docker.internal — irrelevant in compose) AND
19+
# explicitly bind /http-listener/host to 0.0.0.0; without that
20+
# second override the upstream image binds to localhost and is
21+
# unreachable from the host port mapping.
22+
RHO: '/mclient/connection-string->"mongodb://${RESTHEART_MONGO_IP:-172.36.0.10}:27017";/http-listener/host->"0.0.0.0";/core/log-level->"INFO"'
1823
depends_on:
1924
mongo:
2025
condition: service_healthy

0 commit comments

Comments
 (0)