Skip to content

Commit 8015834

Browse files
committed
ci(coverage): include docker-e2e in the coverage map
The host `coverage` job ran with `--all-features`, which enabled the docker-e2e feature, but the job never built the per-ecosystem Docker images — every docker_e2e_<eco> test would panic on `assert_image` and the job failed (or, if it ever passed, only the surviving in-process tests contributed). The Docker tests exercise the real socket-patch binary inside a Linux container, and that subprocess's coverage wasn't captured at all. Changes: * Each `docker_e2e_<eco>.rs` now reads SOCKET_PATCH_COV_BIN + SOCKET_PATCH_COV_PROFRAW_DIR. When both are set, the docker run mounts an llvm-cov-instrumented socket-patch binary over the image's baked-in /usr/local/bin/socket-patch and points LLVM_PROFILE_FILE into a host-visible volume. Empty Vec when unset → tests behave exactly as before for local dev and the existing e2e-docker matrix. * `coverage` job: drops `--all-features` for an explicit feature list (cargo,golang,maven,composer,nuget) that excludes docker-e2e. Produces `coverage-host.lcov`. * New `coverage-docker` matrix job: per ecosystem, builds the base + ecosystem Docker images, eval-sources `cargo llvm-cov show-env` to build an instrumented `target/debug/socket-patch`, sets the SOCKET_PATCH_COV_* hooks, runs `cargo llvm-cov --no-report --test docker_e2e_<eco>`, and emits a per-ecosystem lcov artifact. * New `coverage-merge` job: gathers `coverage-host` + all 8 `coverage-docker-*` artifacts and unions them via `lcov --add-tracefile` into a single `coverage-lcov` artifact. Same artifact name as before so downstream consumers keep working. Result: lines hit by ANY test (host in-process, host harness, or in-container binary execution) show up in the final coverage map.
1 parent f98e89e commit 8015834

9 files changed

Lines changed: 442 additions & 109 deletions

File tree

.github/workflows/ci.yml

Lines changed: 176 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,16 @@ jobs:
138138
# tests twice. The output filename matches the `*.lcov`
139139
# gitignore pattern so a stray local run can't accidentally
140140
# commit a 600 KB report.
141+
#
142+
# Explicit feature list (instead of --all-features) excludes the
143+
# docker-e2e feature — those tests need Docker images this job
144+
# doesn't build. The coverage-docker matrix covers them
145+
# separately, and coverage-merge stitches everything together.
141146
run: |
142-
cargo llvm-cov --workspace --all-features --no-report
143-
cargo llvm-cov report --lcov --output-path coverage.lcov
147+
cargo llvm-cov --workspace \
148+
--features cargo,golang,maven,composer,nuget \
149+
--no-report
150+
cargo llvm-cov report --lcov --output-path coverage-host.lcov
144151
cargo llvm-cov report --summary-only | tee coverage-summary.txt
145152
146153
- name: Publish coverage summary to job summary
@@ -149,16 +156,180 @@ jobs:
149156
# need to crack open the artifact for a quick look.
150157
run: |
151158
{
152-
echo "## Coverage summary"
159+
echo "## Host coverage summary"
160+
echo ""
161+
echo "(In-process tests only. See coverage-merge for the"
162+
echo "full picture including docker-e2e binary coverage.)"
153163
echo ""
154164
echo '```'
155165
cat coverage-summary.txt
156166
echo '```'
167+
} >> "$GITHUB_STEP_SUMMARY"
168+
169+
- name: Upload host LCOV artifact
170+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
171+
with:
172+
name: coverage-host
173+
path: coverage-host.lcov
174+
if-no-files-found: error
175+
retention-days: 30
176+
177+
coverage-docker:
178+
# Per-ecosystem coverage for the Docker-driven e2e suite. Mirrors
179+
# the e2e-docker matrix but builds an instrumented socket-patch
180+
# binary and mounts it into the container along with a host-
181+
# visible profraw directory, so the in-container code paths
182+
# contribute to the lcov merge.
183+
#
184+
# Hooks: docker_e2e_<eco>.rs reads SOCKET_PATCH_COV_BIN +
185+
# SOCKET_PATCH_COV_PROFRAW_DIR. Both unset is the no-op default
186+
# (used by the e2e-docker matrix above).
187+
runs-on: ubuntu-latest
188+
permissions:
189+
contents: read
190+
strategy:
191+
fail-fast: false
192+
matrix:
193+
ecosystem: [npm, pypi, gem, cargo, golang, maven, composer, nuget]
194+
steps:
195+
- name: Checkout
196+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
197+
with:
198+
persist-credentials: false
199+
200+
- name: Set up Docker Buildx
201+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
202+
203+
- name: Install Rust
204+
uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable
205+
with:
206+
components: llvm-tools-preview
207+
208+
- name: Install cargo-llvm-cov
209+
uses: taiki-e/install-action@65851e10cd6c377f11a60e600abc07cb08643468 # v2.79.3
210+
with:
211+
tool: cargo-llvm-cov@0.8.7
212+
213+
- name: Cache cargo
214+
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
215+
with:
216+
path: |
217+
~/.cargo/registry
218+
~/.cargo/git
219+
target
220+
key: ubuntu-latest-cargo-coverage-docker-${{ hashFiles('**/Cargo.lock') }}
221+
restore-keys: ubuntu-latest-cargo-coverage-docker-
222+
223+
- name: Build base image
224+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
225+
with:
226+
context: .
227+
file: tests/docker/Dockerfile.base
228+
tags: socket-patch-test-base:latest
229+
load: true
230+
cache-from: type=gha,scope=test-base
231+
cache-to: type=gha,scope=test-base,mode=max
232+
233+
- name: Build ${{ matrix.ecosystem }} image
234+
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
235+
with:
236+
context: .
237+
file: tests/docker/Dockerfile.${{ matrix.ecosystem }}
238+
tags: socket-patch-test-${{ matrix.ecosystem }}:latest
239+
load: true
240+
cache-from: type=gha,scope=test-${{ matrix.ecosystem }}
241+
cache-to: type=gha,scope=test-${{ matrix.ecosystem }},mode=max
242+
243+
- name: Build instrumented socket-patch binary
244+
# Source `cargo llvm-cov show-env` into the current shell so this
245+
# `cargo build` picks up RUSTC_WRAPPER=cargo-llvm-cov and the
246+
# same RUSTFLAGS that the subsequent `cargo llvm-cov` test step
247+
# will use. The bin we build ends up byte-compatible with the
248+
# test binaries — same source hashes → unified coverage map at
249+
# report time. Env stays scoped to this step (intentional;
250+
# cargo llvm-cov manages its own env in the test step).
251+
run: |
252+
eval "$(cargo llvm-cov show-env --export-prefix 2>/dev/null)"
253+
cargo build --bin socket-patch --features cargo,golang,maven,composer,nuget
254+
255+
- name: Configure docker-e2e coverage hooks
256+
run: |
257+
echo "SOCKET_PATCH_COV_BIN=$PWD/target/debug/socket-patch" >> "$GITHUB_ENV"
258+
# Profraw files from the in-container binary land here.
259+
# cargo-llvm-cov scans target/ for *.profraw at report time.
260+
echo "SOCKET_PATCH_COV_PROFRAW_DIR=$PWD/target" >> "$GITHUB_ENV"
261+
262+
- name: Run ${{ matrix.ecosystem }} Docker e2e test with coverage
263+
run: |
264+
cargo llvm-cov \
265+
--features docker-e2e,cargo,golang,maven,composer,nuget \
266+
--no-report \
267+
--test docker_e2e_${{ matrix.ecosystem }}
268+
269+
- name: Generate per-ecosystem lcov
270+
run: |
271+
cargo llvm-cov report \
272+
--lcov \
273+
--output-path coverage-docker-${{ matrix.ecosystem }}.lcov
274+
275+
- name: Upload per-ecosystem LCOV artifact
276+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
277+
with:
278+
name: coverage-docker-${{ matrix.ecosystem }}
279+
path: coverage-docker-${{ matrix.ecosystem }}.lcov
280+
if-no-files-found: error
281+
retention-days: 30
282+
283+
coverage-merge:
284+
# Merge the host coverage and per-ecosystem docker coverage into a
285+
# single lcov.info. lcov(1) handles the union — same files are
286+
# summed line-by-line so a line covered by ANY test counts.
287+
needs: [coverage, coverage-docker]
288+
runs-on: ubuntu-latest
289+
permissions:
290+
contents: read
291+
steps:
292+
- name: Install lcov
293+
run: sudo apt-get update && sudo apt-get install -y lcov
294+
295+
- name: Download all coverage artifacts
296+
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
297+
with:
298+
path: coverage-artifacts
299+
pattern: coverage-*
300+
301+
- name: Merge LCOV files
302+
# `--add-tracefile` is repeated per input. lcov sums hit counts
303+
# for identical source/line keys, so files covered by both host
304+
# and docker tests report the higher (union) count.
305+
# `find` (not bash globstar) for portability across runners.
306+
run: |
307+
set -e
308+
ARGS=()
309+
while IFS= read -r f; do
310+
ARGS+=(--add-tracefile "$f")
311+
done < <(find coverage-artifacts -name '*.lcov' -type f)
312+
if [ ${#ARGS[@]} -eq 0 ]; then
313+
echo "No lcov files found to merge" >&2
314+
exit 1
315+
fi
316+
lcov "${ARGS[@]}" --output-file coverage.lcov
317+
318+
- name: Render summary
319+
# `lcov --summary` prints a per-file rollup we tee into the job
320+
# summary, same shape as cargo-llvm-cov's own.
321+
run: |
322+
{
323+
echo "## Coverage (host + docker-e2e merged)"
324+
echo ""
325+
echo '```'
326+
lcov --summary coverage.lcov 2>&1 | tail -20
327+
echo '```'
157328
echo ""
158-
echo "Full LCOV report uploaded as the \`coverage-lcov\` artifact."
329+
echo "Full merged LCOV uploaded as the \`coverage-lcov\` artifact."
159330
} >> "$GITHUB_STEP_SUMMARY"
160331
161-
- name: Upload LCOV artifact
332+
- name: Upload merged LCOV artifact
162333
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
163334
with:
164335
name: coverage-lcov

crates/socket-patch-cli/tests/docker_e2e_cargo.rs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,26 @@ const PATCHED_RS: &[u8] = b"// SOCKET-PATCH-E2E-MARKER\n\
2424
#[macro_export]\n\
2525
macro_rules! cfg_if {\n ($($t:tt)*) => {};\n}\n";
2626

27+
/// See docker_e2e_npm.rs::cov_docker_args for the coverage hook
28+
/// semantics. The CI coverage-docker job sets the env vars; locally
29+
/// they're unset and this returns an empty Vec.
30+
fn cov_docker_args() -> Vec<String> {
31+
let Ok(bin) = std::env::var("SOCKET_PATCH_COV_BIN") else {
32+
return Vec::new();
33+
};
34+
let Ok(dir) = std::env::var("SOCKET_PATCH_COV_PROFRAW_DIR") else {
35+
return Vec::new();
36+
};
37+
vec![
38+
"-v".into(),
39+
format!("{bin}:/usr/local/bin/socket-patch:ro"),
40+
"-v".into(),
41+
format!("{dir}:/coverage"),
42+
"-e".into(),
43+
"LLVM_PROFILE_FILE=/coverage/docker-e2e-%p-%14m.profraw".into(),
44+
]
45+
}
46+
2747
fn git_sha256(content: &[u8]) -> String {
2848
let header = format!("blob {}\0", content.len());
2949
let mut hasher = Sha256::new();
@@ -168,19 +188,21 @@ async fn cargo_fetch_full_apply_chain() {
168188
let server = make_mock_server(&after_hash).await;
169189
let api_url = format!("http://host.docker.internal:{}", server.address().port());
170190
assert_image();
171-
let out = Command::new("docker")
172-
.args([
173-
"run",
174-
"--rm",
175-
"--add-host=host.docker.internal:host-gateway",
176-
"-i",
177-
"socket-patch-test-cargo:latest",
178-
"bash",
179-
"-c",
180-
&local_script(&api_url),
181-
])
182-
.output()
183-
.expect("docker run");
191+
let mut cmd = Command::new("docker");
192+
cmd.args([
193+
"run",
194+
"--rm",
195+
"--add-host=host.docker.internal:host-gateway",
196+
"-i",
197+
])
198+
.args(cov_docker_args())
199+
.args([
200+
"socket-patch-test-cargo:latest",
201+
"bash",
202+
"-c",
203+
&local_script(&api_url),
204+
]);
205+
let out = cmd.output().expect("docker run");
184206
let stdout = String::from_utf8_lossy(&out.stdout);
185207
let stderr = String::from_utf8_lossy(&out.stderr);
186208
assert!(

crates/socket-patch-cli/tests/docker_e2e_composer.rs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,26 @@ const PATCHED_PHP: &[u8] = b"<?php\n\
2828
namespace Monolog;\n\
2929
class Logger {\n public const VERSION = '3.5.0-patched';\n}\n";
3030

31+
/// See docker_e2e_npm.rs::cov_docker_args for the coverage hook
32+
/// semantics. The CI coverage-docker job sets the env vars; locally
33+
/// they're unset and this returns an empty Vec.
34+
fn cov_docker_args() -> Vec<String> {
35+
let Ok(bin) = std::env::var("SOCKET_PATCH_COV_BIN") else {
36+
return Vec::new();
37+
};
38+
let Ok(dir) = std::env::var("SOCKET_PATCH_COV_PROFRAW_DIR") else {
39+
return Vec::new();
40+
};
41+
vec![
42+
"-v".into(),
43+
format!("{bin}:/usr/local/bin/socket-patch:ro"),
44+
"-v".into(),
45+
format!("{dir}:/coverage"),
46+
"-e".into(),
47+
"LLVM_PROFILE_FILE=/coverage/docker-e2e-%p-%14m.profraw".into(),
48+
]
49+
}
50+
3151
fn git_sha256(content: &[u8]) -> String {
3252
let header = format!("blob {}\0", content.len());
3353
let mut hasher = Sha256::new();
@@ -190,19 +210,16 @@ fn assert_image() {
190210
}
191211

192212
fn run_container(script: &str) -> std::process::Output {
193-
Command::new("docker")
194-
.args([
195-
"run",
196-
"--rm",
197-
"--add-host=host.docker.internal:host-gateway",
198-
"-i",
199-
"socket-patch-test-composer:latest",
200-
"bash",
201-
"-c",
202-
script,
203-
])
204-
.output()
205-
.expect("docker run")
213+
let mut cmd = Command::new("docker");
214+
cmd.args([
215+
"run",
216+
"--rm",
217+
"--add-host=host.docker.internal:host-gateway",
218+
"-i",
219+
])
220+
.args(cov_docker_args())
221+
.args(["socket-patch-test-composer:latest", "bash", "-c", script]);
222+
cmd.output().expect("docker run")
206223
}
207224

208225
#[tokio::test]

crates/socket-patch-cli/tests/docker_e2e_gem.rs

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,26 @@ const PATCHED_RB: &[u8] = b"# SOCKET-PATCH-E2E-MARKER\n\
2727
# colorize.rb replaced by socket-patch e2e fixture\n\
2828
module Colorize\n VERSION = '1.1.0-patched'\nend\n";
2929

30+
/// See docker_e2e_npm.rs::cov_docker_args for the coverage hook
31+
/// semantics. The CI coverage-docker job sets the env vars; locally
32+
/// they're unset and this returns an empty Vec.
33+
fn cov_docker_args() -> Vec<String> {
34+
let Ok(bin) = std::env::var("SOCKET_PATCH_COV_BIN") else {
35+
return Vec::new();
36+
};
37+
let Ok(dir) = std::env::var("SOCKET_PATCH_COV_PROFRAW_DIR") else {
38+
return Vec::new();
39+
};
40+
vec![
41+
"-v".into(),
42+
format!("{bin}:/usr/local/bin/socket-patch:ro"),
43+
"-v".into(),
44+
format!("{dir}:/coverage"),
45+
"-e".into(),
46+
"LLVM_PROFILE_FILE=/coverage/docker-e2e-%p-%14m.profraw".into(),
47+
]
48+
}
49+
3050
fn git_sha256(content: &[u8]) -> String {
3151
let header = format!("blob {}\0", content.len());
3252
let mut hasher = Sha256::new();
@@ -189,19 +209,16 @@ fn assert_image() {
189209
}
190210

191211
fn run_container(script: &str) -> std::process::Output {
192-
Command::new("docker")
193-
.args([
194-
"run",
195-
"--rm",
196-
"--add-host=host.docker.internal:host-gateway",
197-
"-i",
198-
"socket-patch-test-gem:latest",
199-
"bash",
200-
"-c",
201-
script,
202-
])
203-
.output()
204-
.expect("docker run")
212+
let mut cmd = Command::new("docker");
213+
cmd.args([
214+
"run",
215+
"--rm",
216+
"--add-host=host.docker.internal:host-gateway",
217+
"-i",
218+
])
219+
.args(cov_docker_args())
220+
.args(["socket-patch-test-gem:latest", "bash", "-c", script]);
221+
cmd.output().expect("docker run")
205222
}
206223

207224
#[tokio::test]

0 commit comments

Comments
 (0)