Skip to content

Commit 22c76cc

Browse files
committed
Share one docker test harness
Add tests/docker_common as the one home for the container plumbing every docker suite copied: cov_docker_args (with sbt's SOCKET_PATCH_DOCKER_BIN branch), skip_if_no_image / skip_or_require_image, run_container and assert_vex_agent_attested. Delete the copies in the 10 docker_e2e agent suites and docker_vendor_common, the plain_sha256/sha256_hex copies (common has sha256_hex), deno's SOCKET_PATCH_REQUIRE_DOCKER gate (set nowhere), the dead run_in_image and npm's unused Duration import. Every agent suite's skip now honours SOCKET_PATCH_DOCKER_E2E_REQUIRED like the sbt and vendor suites. CI sets it only on legs whose images it builds, so no CI leg changes outcome. Refs #1968 Assisted-by: Claude Code:claude-opus-5-5
1 parent b3943f4 commit 22c76cc

19 files changed

Lines changed: 260 additions & 1113 deletions
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//! Container plumbing shared by every `docker_e2e_*` suite. Include it with
2+
//! `#[path = "docker_common/mod.rs"] mod docker_common;` (suites that also
3+
//! include `docker_vendor_common` need both: that module builds on this one).
4+
//!
5+
//! `#![allow(dead_code)]`: each suite uses a different subset.
6+
7+
#![allow(dead_code)]
8+
9+
use std::process::{Command, Output};
10+
11+
/// Docker args that shadow the image's baked-in socket-patch. With
12+
/// `SOCKET_PATCH_DOCKER_BIN` set, that host binary is mounted (a local Linux
13+
/// build). Otherwise the CI coverage-docker job sets `SOCKET_PATCH_COV_BIN`
14+
/// (an llvm-cov-instrumented binary) and `SOCKET_PATCH_COV_PROFRAW_DIR` (where
15+
/// the in-container profraws land). Locally all are unset and this is empty.
16+
pub fn cov_docker_args() -> Vec<String> {
17+
if let Ok(bin) = std::env::var("SOCKET_PATCH_DOCKER_BIN") {
18+
return vec!["-v".into(), format!("{bin}:/usr/local/bin/socket-patch:ro")];
19+
}
20+
let (Ok(bin), Ok(dir)) = (
21+
std::env::var("SOCKET_PATCH_COV_BIN"),
22+
std::env::var("SOCKET_PATCH_COV_PROFRAW_DIR"),
23+
) else {
24+
return Vec::new();
25+
};
26+
vec![
27+
"-v".into(),
28+
format!("{bin}:/usr/local/bin/socket-patch:ro"),
29+
"-v".into(),
30+
format!("{dir}:/coverage"),
31+
"-e".into(),
32+
"LLVM_PROFILE_FILE=/coverage/docker-e2e-%p-%14m.profraw".into(),
33+
]
34+
}
35+
36+
/// Returns `true` when the test should skip: `docker` missing from PATH or
37+
/// `image` not built. Prints a skip notice, because Rust integration tests
38+
/// have no native "skipped" outcome and the test then reports `ok`. Build
39+
/// locally with `docker build -f tests/docker/Dockerfile.base -t
40+
/// socket-patch-test-base:latest .`, then `docker build -f
41+
/// tests/docker/Dockerfile.<eco> -t socket-patch-test-<eco>:latest .`.
42+
///
43+
/// With `SOCKET_PATCH_DOCKER_E2E_REQUIRED=1` (a CI job that built the image
44+
/// itself) a missing docker or image fails the test instead, so a tag
45+
/// mismatch cannot pass the job with zero assertions.
46+
#[must_use]
47+
pub fn skip_if_no_image(image: &str) -> bool {
48+
skip_or_require_image(
49+
image,
50+
std::env::var("SOCKET_PATCH_DOCKER_E2E_REQUIRED").is_ok_and(|v| v == "1"),
51+
)
52+
}
53+
54+
/// [`skip_if_no_image`] with the required switch passed in.
55+
#[must_use]
56+
pub fn skip_or_require_image(image: &str, required: bool) -> bool {
57+
let reason = match Command::new("docker")
58+
.args(["image", "inspect", image])
59+
.output()
60+
{
61+
Err(_) => "`docker` not on PATH".to_string(),
62+
Ok(out) if !out.status.success() => format!("docker image `{image}` not present"),
63+
Ok(_) => return false,
64+
};
65+
assert!(
66+
!required,
67+
"{reason}; SOCKET_PATCH_DOCKER_E2E_REQUIRED=1 forbids skipping"
68+
);
69+
eprintln!("skipping: {reason}");
70+
true
71+
}
72+
73+
/// Run `script` (bash) in a throwaway `image` container that can reach a
74+
/// host mock server at `host.docker.internal`.
75+
pub fn run_container(image: &str, script: &str) -> Output {
76+
Command::new("docker")
77+
.args([
78+
"run",
79+
"--rm",
80+
"--add-host=host.docker.internal:host-gateway",
81+
"-i",
82+
])
83+
.args(cov_docker_args())
84+
.args([image, "bash", "-c", script])
85+
.output()
86+
.expect("docker run")
87+
}
88+
89+
/// Host-side oracle over the VEX document the container printed between
90+
/// `===VEX DOC BEGIN===` and `===VEX DOC END===` (the agent suites keep the
91+
/// workspace inside the container, so the doc comes from captured stdout).
92+
/// Asserts exactly one `not_affected` statement for `ghsa` on the
93+
/// `subcomponent_purl`, with a PLAIN impact statement: no `(vendored)` or
94+
/// `(redirected)` marker, which is what distinguishes agent provenance.
95+
pub fn assert_vex_agent_attested(stdout: &str, subcomponent_purl: &str, ghsa: &str) {
96+
const BEGIN: &str = "===VEX DOC BEGIN===";
97+
const END: &str = "===VEX DOC END===";
98+
let start = stdout
99+
.find(BEGIN)
100+
.unwrap_or_else(|| panic!("VEX DOC BEGIN marker missing from stdout:\n{stdout}"))
101+
+ BEGIN.len();
102+
let stop = stdout[start..]
103+
.find(END)
104+
.unwrap_or_else(|| panic!("VEX DOC END marker missing from stdout:\n{stdout}"))
105+
+ start;
106+
let doc: serde_json::Value = serde_json::from_str(stdout[start..stop].trim())
107+
.expect("emitted VEX document must be valid JSON");
108+
let stmts = doc["statements"]
109+
.as_array()
110+
.expect("VEX document must have a statements array");
111+
assert_eq!(stmts.len(), 1, "exactly one VEX statement expected: {doc}");
112+
let st = &stmts[0];
113+
assert_eq!(st["vulnerability"]["name"], ghsa, "attested GHSA mismatch");
114+
assert_eq!(st["status"], "not_affected");
115+
assert_eq!(
116+
st["products"][0]["subcomponents"][0]["@id"], subcomponent_purl,
117+
"subcomponent must be the patched package purl"
118+
);
119+
let impact = st["impact_statement"]
120+
.as_str()
121+
.expect("statement must carry an impact_statement");
122+
assert!(
123+
impact.contains("Patched via Socket patch")
124+
&& !impact.contains("(vendored)")
125+
&& !impact.contains("(redirected)"),
126+
"agent-mode attestation must carry a PLAIN impact statement (no vendored/redirected marker): {impact}"
127+
);
128+
}

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

Lines changed: 7 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,16 @@
1010

1111
#[path = "common/mod.rs"]
1212
mod common;
13+
#[path = "docker_common/mod.rs"]
14+
mod docker_common;
1315
use common::git_sha256;
14-
15-
use std::process::Command;
16+
use docker_common::{assert_vex_agent_attested, run_container, skip_if_no_image};
1617

1718
use base64::Engine;
1819
use wiremock::matchers::{method, path, path_regex};
1920
use wiremock::{Mock, MockServer, ResponseTemplate};
2021

22+
const IMAGE: &str = "socket-patch-test-cargo:latest";
2123
const ORG: &str = "test-org";
2224
const PURL: &str = "pkg:cargo/cfg-if@1.0.0";
2325
const UUID: &str = "14141414-1414-4141-8141-141414141414";
@@ -31,26 +33,6 @@ const PATCHED_RS: &[u8] = b"// SOCKET-PATCH-E2E-MARKER\n\
3133
#[macro_export]\n\
3234
macro_rules! cfg_if {\n ($($t:tt)*) => {};\n}\n";
3335

34-
/// See docker_e2e_npm.rs::cov_docker_args for the coverage hook
35-
/// semantics. The CI coverage-docker job sets the env vars; locally
36-
/// they're unset and this returns an empty Vec.
37-
fn cov_docker_args() -> Vec<String> {
38-
let Ok(bin) = std::env::var("SOCKET_PATCH_COV_BIN") else {
39-
return Vec::new();
40-
};
41-
let Ok(dir) = std::env::var("SOCKET_PATCH_COV_PROFRAW_DIR") else {
42-
return Vec::new();
43-
};
44-
vec![
45-
"-v".into(),
46-
format!("{bin}:/usr/local/bin/socket-patch:ro"),
47-
"-v".into(),
48-
format!("{dir}:/coverage"),
49-
"-e".into(),
50-
"LLVM_PROFILE_FILE=/coverage/docker-e2e-%p-%14m.profraw".into(),
51-
]
52-
}
53-
5436
async fn make_mock_server(after_hash: &str) -> MockServer {
5537
let listener = std::net::TcpListener::bind("0.0.0.0:0").expect("bind wiremock");
5638
let server = MockServer::builder().listener(listener).start().await;
@@ -324,96 +306,15 @@ exit 0
324306
)
325307
}
326308

327-
/// Host-side oracle over the VEX document the container emitted between the
328-
/// `===VEX DOC BEGIN===` / `===VEX DOC END===` markers (these agent suites run
329-
/// the workspace inside the container with no bind mount, so the doc is parsed
330-
/// from captured stdout). Asserts exactly one statement attesting the agent
331-
/// patch: the fixture GHSA, `not_affected`, the installed-package subcomponent
332-
/// purl, and a PLAIN impact statement with NO `(vendored)`/`(redirected)`
333-
/// marker — the marker's absence is what distinguishes agent provenance.
334-
fn assert_vex_agent_attested(stdout: &str, subcomponent_purl: &str) {
335-
const BEGIN: &str = "===VEX DOC BEGIN===";
336-
const END: &str = "===VEX DOC END===";
337-
let start = stdout
338-
.find(BEGIN)
339-
.unwrap_or_else(|| panic!("VEX DOC BEGIN marker missing from stdout:\n{stdout}"))
340-
+ BEGIN.len();
341-
let stop = stdout[start..]
342-
.find(END)
343-
.unwrap_or_else(|| panic!("VEX DOC END marker missing from stdout:\n{stdout}"))
344-
+ start;
345-
let doc: serde_json::Value = serde_json::from_str(stdout[start..stop].trim())
346-
.expect("emitted VEX document must be valid JSON");
347-
let stmts = doc["statements"]
348-
.as_array()
349-
.expect("VEX document must have a statements array");
350-
assert_eq!(stmts.len(), 1, "exactly one VEX statement expected: {doc}");
351-
let st = &stmts[0];
352-
assert_eq!(st["vulnerability"]["name"], GHSA, "attested GHSA mismatch");
353-
assert_eq!(st["status"], "not_affected");
354-
assert_eq!(
355-
st["products"][0]["subcomponents"][0]["@id"], subcomponent_purl,
356-
"subcomponent must be the patched package purl"
357-
);
358-
let impact = st["impact_statement"]
359-
.as_str()
360-
.expect("statement must carry an impact_statement");
361-
assert!(
362-
impact.contains("Patched via Socket patch")
363-
&& !impact.contains("(vendored)")
364-
&& !impact.contains("(redirected)"),
365-
"agent-mode attestation must carry a PLAIN impact statement (no vendored/redirected marker): {impact}"
366-
);
367-
}
368-
369-
/// Returns `true` when the test should skip (docker missing, image
370-
/// missing). Prints a skip notice to stderr in that case so the test
371-
/// log shows *why* the test did nothing — the test still reports as
372-
/// `ok` because Rust integration tests have no native "skipped" outcome.
373-
///
374-
/// Local devs: build the image with
375-
/// `docker build -f tests/docker/Dockerfile.base -t socket-patch-test-base:latest .`
376-
/// then
377-
/// `docker build -f tests/docker/Dockerfile.cargo -t socket-patch-test-cargo:latest .`
378-
#[must_use]
379-
fn skip_if_no_image() -> bool {
380-
let Ok(out) = Command::new("docker")
381-
.args(["image", "inspect", "socket-patch-test-cargo:latest"])
382-
.output()
383-
else {
384-
eprintln!("skipping: `docker` not on PATH");
385-
return true;
386-
};
387-
if !out.status.success() {
388-
eprintln!("skipping: docker image `socket-patch-test-cargo:latest` not present");
389-
return true;
390-
}
391-
false
392-
}
393-
394309
#[tokio::test]
395310
async fn cargo_fetch_full_apply_chain() {
396311
let after_hash = git_sha256(PATCHED_RS);
397312
let server = make_mock_server(&after_hash).await;
398313
let api_url = format!("http://host.docker.internal:{}", server.address().port());
399-
if skip_if_no_image() {
314+
if skip_if_no_image(IMAGE) {
400315
return;
401316
}
402-
let mut cmd = Command::new("docker");
403-
cmd.args([
404-
"run",
405-
"--rm",
406-
"--add-host=host.docker.internal:host-gateway",
407-
"-i",
408-
])
409-
.args(cov_docker_args())
410-
.args([
411-
"socket-patch-test-cargo:latest",
412-
"bash",
413-
"-c",
414-
&local_script(&api_url, &after_hash),
415-
]);
416-
let out = cmd.output().expect("docker run");
317+
let out = run_container(IMAGE, &local_script(&api_url, &after_hash));
417318
let stdout = String::from_utf8_lossy(&out.stdout);
418319
let stderr = String::from_utf8_lossy(&out.stderr);
419320
assert!(
@@ -429,7 +330,7 @@ async fn cargo_fetch_full_apply_chain() {
429330
stderr.contains("===VEX VERIFIED==="),
430331
"agent-mode VEX leg did not run/pass (===VEX VERIFIED=== missing).\nstderr=\n{stderr}"
431332
);
432-
assert_vex_agent_attested(&stdout, PURL);
333+
assert_vex_agent_attested(&stdout, PURL, GHSA);
433334

434335
// The script gates on an exact git-blob-hash match; confirm the
435336
// expected hash actually appears in the log so a future edit that

0 commit comments

Comments
 (0)