diff --git a/crates/socket-patch-cli/tests/apply/check_verifies_installed_tree.rs b/crates/socket-patch-cli/tests/apply/check_verifies_installed_tree.rs index da0f9a8d7..da78eeb33 100644 --- a/crates/socket-patch-cli/tests/apply/check_verifies_installed_tree.rs +++ b/crates/socket-patch-cli/tests/apply/check_verifies_installed_tree.rs @@ -12,6 +12,7 @@ use std::path::Path; use serde_json::{json, Value}; use crate::common; +use common::envelope::events; use common::{git_sha256, parse_json_envelope, run_with_env}; const PURL: &str = "pkg:npm/check-target@1.0.0"; @@ -66,10 +67,6 @@ fn project(installed: Option<&[u8]>) -> tempfile::TempDir { tmp } -fn events(env: &Value) -> Vec { - env["events"].as_array().cloned().unwrap_or_default() -} - /// The regression: an unpatched installed copy is drift (exit 1), in both /// output modes, and `--check` writes nothing. #[test] @@ -97,8 +94,8 @@ fn check_fails_on_an_unpatched_npm_package() { assert_eq!(code, 1, "stderr={stderr}"); let env = parse_json_envelope(stdout.trim()); assert_eq!(env["status"], "partialFailure", "{env}"); - let failed: Vec = events(&env) - .into_iter() + let failed: Vec<&Value> = events(&env) + .iter() .filter(|e| e["action"] == "failed") .collect(); assert_eq!(failed.len(), 1, "{env}"); diff --git a/crates/socket-patch-cli/tests/cli/envelope_helper_copies.rs b/crates/socket-patch-cli/tests/cli/envelope_helper_copies.rs new file mode 100644 index 000000000..a6390048d --- /dev/null +++ b/crates/socket-patch-cli/tests/cli/envelope_helper_copies.rs @@ -0,0 +1,268 @@ +//! One set of `--json` envelope readers for every CLI test target (#1089). +//! +//! `common/envelope.rs` owns parsing stdout and reading `events[]`, their +//! `errorCode`s and the `warnings[]` codes. The self-tests below run every +//! shape the per-file copies took through it, on the inputs where those +//! copies differed; the ratchet keeps new private copies out. + +use std::path::Path; + +use serde_json::{json, Value}; + +use crate::common::envelope::{ + all_codes, codes_in, event_codes, event_triples, events, find_event, parse_json_envelope, +}; + +/// Files that still define a private envelope reader. Each is changed by an +/// open PR, is a shared module whose includers are, or keeps a rule of its +/// own (a stricter or wider read). Migrate a file onto `common::envelope` +/// and delete its entry; a stale entry is not an error, so a PR that +/// migrates one never turns another red. +const PENDING_PRIVATE_READERS: &[&str] = &[ + "covgap_commands_rollback.rs", + "e2e_redirect_gradle_build.rs", + "e2e_redirect_pnpm_build.rs", + "e2e_socket_yml_policy.rs", + "e2e_vendor_composer_build.rs", + "e2e_vendor_npm_build.rs", + "e2e_vendor_pnpm_build.rs", + "e2e_vendor_pypi_build.rs", + "e2e_vendor_vlt_build.rs", + "e2e_vendor_yarn_berry_build.rs", + "hosted_memory_engine.rs", + "in_process_redirect.rs", + "in_process_rollback_hosted.rs", + "in_process_vendor.rs", + "in_process_vendor_bun_takeover.rs", + "in_process_vendor_npm_v1_takeover.rs", + "in_process_vendor_pnpm_takeover.rs", + "vendor_eject_bun_lockb.rs", + "vendor_jvm_cli.rs", + "e2e_vex_lockfile/cargo.rs", + "in_process_vendor_bun_takeover/vlt.rs", + "remove/covgap_commands_remove.rs", + "scan/scan_paths_e2e.rs", + "vlt_hosted_common/mod.rs", + "vlt_vendor_common/mod.rs", +]; + +/// The top-level helper names the per-file copies used. +const READER_NAMES: &[&str] = &[ + "events", + "find_event", + "parse_envelope", + "parse_json_envelope", + "event_codes", + "event_triples", + "warning_codes", + "codes", + "all_codes", + "codes_in", +]; + +/// Every `.rs` file under `tests/`, as (`/`-joined relative path, text). +fn test_sources() -> Vec<(String, String)> { + fn walk(dir: &Path, root: &Path, out: &mut Vec<(String, String)>) { + let mut entries: Vec<_> = std::fs::read_dir(dir) + .unwrap_or_else(|e| panic!("read {}: {e}", dir.display())) + .map(|e| e.unwrap().path()) + .collect(); + entries.sort(); + for path in entries { + if path.is_dir() { + walk(&path, root, out); + } else if path.extension().is_some_and(|e| e == "rs") { + let rel = path + .strip_prefix(root) + .unwrap() + .components() + .map(|c| c.as_os_str().to_string_lossy().into_owned()) + .collect::>() + .join("/"); + let text = std::fs::read_to_string(&path) + .unwrap_or_else(|e| panic!("read {}: {e}", path.display())); + out.push((rel, text)); + } + } + } + let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests"); + let mut out = Vec::new(); + walk(&root, &root, &mut out); + out +} + +/// Whether `text` defines a top-level envelope reader of its own. +fn defines_private_reader(text: &str) -> bool { + text.lines().any(|line| { + let line = line + .strip_prefix("pub(crate) ") + .or_else(|| line.strip_prefix("pub ")) + .unwrap_or(line); + line.strip_prefix("fn ").is_some_and(|rest| { + READER_NAMES.iter().any(|name| { + rest.strip_prefix(name) + .is_some_and(|after| after.starts_with('(') || after.starts_with('<')) + }) + }) + }) +} + +#[test] +fn no_new_private_envelope_readers() { + let unexpected: Vec = test_sources() + .into_iter() + .filter(|(rel, text)| { + !rel.starts_with("common/") + && !PENDING_PRIVATE_READERS.contains(&rel.as_str()) + && defines_private_reader(text) + }) + .map(|(rel, _)| rel) + .collect(); + assert!( + unexpected.is_empty(), + "these test files define their own --json envelope reader: {unexpected:?}. \ + Use `common::envelope` instead (`use common::envelope::..`, or \ + `#[path = \"common/envelope.rs\"] mod envelope;` in a file without \ + `mod common`). Do not add them to PENDING_PRIVATE_READERS." + ); +} + +#[test] +fn the_detector_sees_every_former_copy_shape() { + for copy in [ + "fn parse_envelope(stdout: &str) -> serde_json::Value {", + "fn events(envelope: &Value) -> &Vec {", + "fn events(stdout: &str) -> Vec<(String, String, String)> {", + "fn find_event<'a>(env: &'a Value, action: &str, code: Option<&str>) -> &'a Value {", + "fn find_event<'a>(", + "pub fn codes(env: &Value) -> Vec {", + "fn warning_codes(doc: &Value) -> Vec<&str> {", + "fn event_codes(v: &Value) -> Vec {", + ] { + assert!(defines_private_reader(copy), "missed: {copy}"); + } + for not_a_copy in [ + "use common::envelope::{events, find_event};", + " fn warning_codes(&self) -> Vec {", + "fn events_for_purl(env: &Value) -> Vec {", + "fn codes_of_lock(lock: &str) -> Vec {", + "let codes = codes_in(&env[\"warnings\"]);", + ] { + assert!( + !defines_private_reader(not_a_copy), + "false positive: {not_a_copy}" + ); + } +} + +fn sample() -> Value { + json!({ + "status": "partial_failure", + "events": [ + {"action": "vendored", "purl": "pkg:npm/a@1.0.0"}, + {"action": "failed", "purl": "pkg:npm/b@1.0.0", "errorCode": "vendor_lock_drift"}, + {"action": "skipped", "errorCode": "vendored"}, + ], + "warnings": [{"code": "w_top"}, {"code": 7}, {"message": "no code"}], + "redirect": {"warnings": [{"code": "w_redirect"}]}, + "vex": {"warnings": [{"code": "w_vex", "errorCode": "e_vex"}]}, + }) +} + +#[test] +fn parse_json_envelope_reads_padded_stdout_and_panics_on_text() { + // `vendor_group_commit_e2e` trimmed stdout before parsing; serde + // already skips the surrounding whitespace. + assert_eq!( + parse_json_envelope("\n {\"events\": []}\n")["events"], + json!([]) + ); + let err = std::panic::catch_unwind(|| parse_json_envelope("error: no manifest")).unwrap_err(); + let msg = err.downcast_ref::().unwrap(); + assert!(msg.contains("error: no manifest"), "{msg}"); +} + +#[test] +fn events_is_strict_and_event_codes_is_lenient() { + let env = sample(); + assert_eq!(events(&env).len(), 3); + assert_eq!(event_codes(&env), ["vendor_lock_drift", "vendored"]); + // The lenient copies (`mode_migration_bun`, `vlt_vendor_common`) read a + // nested `vendor` envelope or none at all as "no codes". + assert!(event_codes(&json!({"status": "error"})).is_empty()); + assert!(std::panic::catch_unwind(|| events(&json!({"status": "error"})).len()).is_err()); +} + +#[test] +fn find_event_matches_action_and_optional_code() { + let env = sample(); + assert_eq!( + find_event(&env, "vendored", None)["purl"], + "pkg:npm/a@1.0.0" + ); + assert_eq!( + find_event(&env, "failed", Some("vendor_lock_drift"))["purl"], + "pkg:npm/b@1.0.0" + ); + // The golang copy required a code and returned `None`; a miss now panics + // with the envelope. + let env2 = env.clone(); + let err = std::panic::catch_unwind(move || { + find_event(&env2, "failed", Some("other"))["purl"].to_string() + }) + .unwrap_err(); + let msg = err.downcast_ref::().unwrap(); + assert!( + msg.contains("vendor_lock_drift"), + "panic shows the envelope: {msg}" + ); +} + +#[test] +fn event_triples_fill_absent_fields_with_empty_strings() { + assert_eq!( + event_triples(&sample()), + [ + ("pkg:npm/a@1.0.0".into(), "vendored".into(), String::new()), + ( + "pkg:npm/b@1.0.0".into(), + "failed".into(), + "vendor_lock_drift".into() + ), + (String::new(), "skipped".into(), "vendored".into()), + ] + ); +} + +#[test] +fn codes_in_reads_any_warnings_array_and_skips_non_strings() { + let env = sample(); + assert_eq!(codes_in(&env["warnings"]), ["w_top"]); + assert_eq!(codes_in(&env["redirect"]["warnings"]), ["w_redirect"]); + // Missing arrays read as empty, as every former copy did. + assert!(codes_in(&env["nope"]["warnings"]).is_empty()); + // The sbt agent and gradle agent copies chained two arrays. + let both: Vec = [&env["warnings"], &env["vex"]["warnings"]] + .into_iter() + .flat_map(codes_in) + .collect(); + assert_eq!(both, ["w_top", "w_vex"]); +} + +#[test] +fn all_codes_collects_code_and_error_code_at_any_depth() { + let codes = all_codes(&sample()); + // `mode_migration_vlt` pushed `errorCode` before `code` per object; the + // callers only ask membership, so order is not part of the contract. + for want in [ + "vendor_lock_drift", + "vendored", + "w_top", + "w_redirect", + "w_vex", + "e_vex", + ] { + assert!(codes.iter().any(|c| c == want), "{want} in {codes:?}"); + } + assert_eq!(codes.len(), 6, "non-string codes are skipped: {codes:?}"); +} diff --git a/crates/socket-patch-cli/tests/cli/main.rs b/crates/socket-patch-cli/tests/cli/main.rs index f3bd4b28c..26fc7aac6 100644 --- a/crates/socket-patch-cli/tests/cli/main.rs +++ b/crates/socket-patch-cli/tests/cli/main.rs @@ -16,6 +16,7 @@ mod cli_dry_run_paths_e2e; mod covgap_api_client; mod covgap_commands_list; mod covgap_output; +mod envelope_helper_copies; mod interactive_prompts_e2e; mod output_modes_e2e; mod shared_helper_copies; diff --git a/crates/socket-patch-cli/tests/common/envelope.rs b/crates/socket-patch-cli/tests/common/envelope.rs new file mode 100644 index 000000000..a72b473f9 --- /dev/null +++ b/crates/socket-patch-cli/tests/common/envelope.rs @@ -0,0 +1,109 @@ +//! Readers for the `--json` envelope every command prints (#1089). +//! +//! Tests assert on stable codes, not sentences: parse stdout once with +//! [`parse_json_envelope`], then read `events[]`, their `errorCode`s and the +//! `warnings[]` codes through these helpers instead of a private copy. +//! +//! Files that don't need the rest of `common` pull this in on its own with +//! `#[path = "common/envelope.rs"] mod envelope;`. + +#![allow(dead_code)] + +use serde_json::Value; + +/// `stdout` parsed as JSON; panics with the raw output when it isn't. +pub fn parse_json_envelope(stdout: &str) -> Value { + serde_json::from_str(stdout) + .unwrap_or_else(|e| panic!("failed to parse JSON envelope: {e}\nstdout:\n{stdout}")) +} + +/// A top-level string field (`status`, …), or `None` when it is missing +/// or not a string. +pub fn json_string<'a>(env: &'a Value, key: &str) -> Option<&'a str> { + env.get(key).and_then(|v| v.as_str()) +} + +/// `env.error.code`: the envelope nests a top-level failure under `error` +/// (`{"error": {"code": "lock_held", "message": "..."}}`). +pub fn envelope_error_code(env: &Value) -> Option<&str> { + env.get("error")?.get("code")?.as_str() +} + +/// `env.error.message`, the companion of [`envelope_error_code`]. +pub fn envelope_error_message(env: &Value) -> Option<&str> { + env.get("error")?.get("message")?.as_str() +} + +/// The envelope's `events[]`; panics when the array is missing (every +/// command envelope serializes it, empty or not). +pub fn events(envelope: &Value) -> &[Value] { + envelope["events"] + .as_array() + .unwrap_or_else(|| panic!("no `events` array in:\n{envelope:#}")) +} + +/// The first `action` event (with `errorCode == code` when given); panics +/// with the envelope when there is none. +pub fn find_event<'a>(envelope: &'a Value, action: &str, error_code: Option<&str>) -> &'a Value { + events(envelope) + .iter() + .find(|e| e["action"] == action && error_code.is_none_or(|c| e["errorCode"] == c)) + .unwrap_or_else(|| { + panic!("expected a `{action}` event (errorCode={error_code:?}) in:\n{envelope:#}") + }) +} + +/// Each event as `(purl, action, errorCode)`, absent fields as `""`. +pub fn event_triples(envelope: &Value) -> Vec<(String, String, String)> { + events(envelope) + .iter() + .map(|e| { + let s = |k: &str| e[k].as_str().unwrap_or_default().to_string(); + (s("purl"), s("action"), s("errorCode")) + }) + .collect() +} + +/// The `errorCode` of every event that has one; empty when `events` is +/// missing. +pub fn event_codes(envelope: &Value) -> Vec { + envelope["events"] + .as_array() + .into_iter() + .flatten() + .filter_map(|e| e["errorCode"].as_str().map(str::to_string)) + .collect() +} + +/// The `code` of every entry of a warnings array (`&env["warnings"]`, +/// `&env["redirect"]["warnings"]`, …); empty when `list` isn't an array. +pub fn codes_in(list: &Value) -> Vec { + list.as_array() + .into_iter() + .flatten() + .filter_map(|w| w["code"].as_str().map(str::to_string)) + .collect() +} + +/// Every `code` and `errorCode` string anywhere in `doc`, depth first. +pub fn all_codes(doc: &Value) -> Vec { + fn walk(v: &Value, out: &mut Vec) { + match v { + Value::Object(map) => { + for (k, v) in map { + if k == "code" || k == "errorCode" { + if let Some(s) = v.as_str() { + out.push(s.to_string()); + } + } + walk(v, out); + } + } + Value::Array(items) => items.iter().for_each(|v| walk(v, out)), + _ => {} + } + } + let mut out = Vec::new(); + walk(doc, &mut out); + out +} diff --git a/crates/socket-patch-cli/tests/common/mod.rs b/crates/socket-patch-cli/tests/common/mod.rs index 1cc10c5b2..3eeb95327 100644 --- a/crates/socket-patch-cli/tests/common/mod.rs +++ b/crates/socket-patch-cli/tests/common/mod.rs @@ -38,6 +38,12 @@ pub mod hermetic; pub use hermetic::command as hermetic_command; +/// Readers for the `--json` envelope (parse, `events[]`, error and warning +/// codes). +pub mod envelope; + +pub use envelope::{envelope_error_code, envelope_error_message, json_string, parse_json_envelope}; + // ── Binary discovery + invocation ───────────────────────────────────── /// Absolute path to the built `socket-patch` binary that cargo @@ -393,36 +399,6 @@ pub fn write_blob(socket_dir: &Path, hash: &str, content: &[u8]) { std::fs::write(blobs.join(hash), content).expect("write blob"); } -/// Parse `--json` apply output, returning the top-level JSON object -/// or panicking with the raw text on parse failure. Most safety tests -/// want to assert on specific fields (`errorCode`, `status`, etc.). -pub fn parse_json_envelope(stdout: &str) -> serde_json::Value { - serde_json::from_str(stdout) - .unwrap_or_else(|e| panic!("failed to parse JSON envelope: {e}\nstdout:\n{stdout}")) -} - -/// Extract a stringified field from a parsed JSON envelope, or None -/// if the field is missing / not a string. Convenience for the -/// `status` checks the safety tests do repeatedly. -pub fn json_string<'a>(env: &'a serde_json::Value, key: &str) -> Option<&'a str> { - env.get(key).and_then(|v| v.as_str()) -} - -/// Extract `env.error.code` from a parsed envelope. The v3.0 -/// envelope shape nests the error under a top-level `error` object -/// (`{"error": {"code": "lock_held", "message": "..."}}`), not at -/// the top level. This helper centralises that lookup so individual -/// tests can stay terse. -pub fn envelope_error_code(env: &serde_json::Value) -> Option<&str> { - env.get("error")?.get("code")?.as_str() -} - -/// Extract `env.error.message` from a parsed envelope. Companion to -/// [`envelope_error_code`]. -pub fn envelope_error_message(env: &serde_json::Value) -> Option<&str> { - env.get("error")?.get("message")?.as_str() -} - /// Map a slice of `(env-var-name, env-var-value)` tuples into a /// HashMap for callers that want a stable container. pub fn env_map(pairs: &[(&str, &str)]) -> HashMap { diff --git a/crates/socket-patch-cli/tests/coverage_fix_scan_hosted_dryrun_vendored.rs b/crates/socket-patch-cli/tests/coverage_fix_scan_hosted_dryrun_vendored.rs index baf7da1f9..a8702ff68 100644 --- a/crates/socket-patch-cli/tests/coverage_fix_scan_hosted_dryrun_vendored.rs +++ b/crates/socket-patch-cli/tests/coverage_fix_scan_hosted_dryrun_vendored.rs @@ -10,8 +10,11 @@ //! shapes) produces the vendored lock + `.socket/vendor/state.json` entry; //! the hosted API is wiremock (`in_process_redirect_pnpm.rs` shapes). +#[path = "common/envelope.rs"] +mod envelope; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; +use envelope::codes_in; use std::path::Path; @@ -328,13 +331,6 @@ fn rewritten_files(doc: &Value) -> Vec<&str> { .unwrap_or_default() } -fn warning_codes(doc: &Value) -> Vec<&str> { - doc["redirect"]["warnings"] - .as_array() - .map(|w| w.iter().filter_map(|e| e["code"].as_str()).collect()) - .unwrap_or_default() -} - /// The dry-run preview must match the wet run's takeover outcome: the /// vendored purl counts as `redirected` (the wet run reverts its vendored /// state, then redirects), the `redirect_would_revert_vendored` warning @@ -363,15 +359,15 @@ async fn dry_run_over_vendored_project_previews_the_wet_takeover() { ); assert_eq!(doc["redirect"]["dryRun"], true, "envelope: {doc:#}"); - let codes = warning_codes(&doc); + let codes = codes_in(&doc["redirect"]["warnings"]); assert!( - codes.contains(&"redirect_would_revert_vendored"), + codes.iter().any(|c| c == "redirect_would_revert_vendored"), "the takeover plan must be announced: {doc:#}" ); // Previewing against the still-vendored lock would refuse it, // contradicting the takeover warning above. assert!( - !codes.contains(&"redirect_pnpm_entry_vendored"), + !codes.iter().any(|c| c == "redirect_pnpm_entry_vendored"), "the dry-run must not also tell the user to run `vendor --revert` \ for a purl this run just promised to revert itself: {doc:#}" ); @@ -480,13 +476,13 @@ async fn dry_run_refuses_unrevertable_vendored_state_like_the_wet_run() { let (code, doc) = scan_hosted_json(root, &server.uri(), /*dry_run=*/ true); assert_eq!(code, 0, "dry-run scan --mode hosted must succeed: {doc:#}"); - let codes = warning_codes(&doc); + let codes = codes_in(&doc["redirect"]["warnings"]); assert!( - codes.contains(&"redirect_vendored_revert_failed"), + codes.iter().any(|c| c == "redirect_vendored_revert_failed"), "the unrevertable state must be refused in the preview too: {doc:#}" ); assert!( - !codes.contains(&"redirect_would_revert_vendored"), + !codes.iter().any(|c| c == "redirect_would_revert_vendored"), "a refused purl must not also be promised a takeover: {doc:#}" ); assert!( @@ -659,7 +655,9 @@ async fn dry_run_package_lock_takeover_previews_the_npmrc_write() { assert_eq!(code, 0, "{doc:#}"); assert_eq!(tree_snapshot(root), vendored_tree, "dry run writes nothing"); assert!( - warning_codes(&doc).contains(&"redirect_would_revert_vendored"), + codes_in(&doc["redirect"]["warnings"]) + .iter() + .any(|c| c == "redirect_would_revert_vendored"), "{doc:#}" ); assert_eq!(doc["redirect"]["redirected"], 1, "{doc:#}"); diff --git a/crates/socket-patch-cli/tests/covgap_commands_scan_hosted.rs b/crates/socket-patch-cli/tests/covgap_commands_scan_hosted.rs index 6a9c07706..979207e57 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_scan_hosted.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_scan_hosted.rs @@ -24,6 +24,7 @@ use wiremock::{Mock, MockServer, ResponseTemplate}; #[path = "common/mod.rs"] mod common; +use common::envelope::codes_in; const ORG: &str = "test-org"; const NAME: &str = "covgap-hosted"; @@ -365,18 +366,6 @@ fn scan_hosted_json( (code, doc) } -/// The `code` of every warning in the redirect envelope. -fn warning_codes(doc: &Value) -> Vec { - doc["redirect"]["warnings"] - .as_array() - .map(|arr| { - arr.iter() - .filter_map(|w| w["code"].as_str().map(str::to_string)) - .collect() - }) - .unwrap_or_default() -} - /// The `detail` of the first warning carrying `code` (panics when absent). fn warning_detail<'a>(doc: &'a Value, code: &str) -> &'a str { doc["redirect"]["warnings"] @@ -1593,7 +1582,7 @@ async fn fifo_bun_lockb_refuses_before_spawning_bun_and_never_wedges() { ); let detail = warning_detail(&doc, "redirect_bun_lockb_invalid"); assert!(detail.contains("not a regular file"), "{extra:?}: {detail}"); - let codes = warning_codes(&doc); + let codes = codes_in(&doc["redirect"]["warnings"]); assert_eq!( codes .iter() @@ -2510,7 +2499,8 @@ async fn vlt_takeover_refusal_before_revert() { "{doc:#}" ); assert!(warning_detail(&doc, "redirect_vendored_revert_failed").contains("vlt-lock.json")); - assert!(!warning_codes(&doc).contains(&"redirect_takeover_reverted_vendored".to_string())); + assert!(!codes_in(&doc["redirect"]["warnings"]) + .contains(&"redirect_takeover_reverted_vendored".to_string())); assert_eq!( std::fs::read(tmp.path().join(".socket/vendor/state.json")).unwrap(), state_before @@ -2588,7 +2578,8 @@ async fn vlt_decides_before_binary_bun_and_a_refused_uuid_is_never_confirmed() { let (refused, tmp) = run(true); assert!( - warning_codes(&refused).contains(&"redirect_vlt_unsupported_lock_key".to_string()), + codes_in(&refused["redirect"]["warnings"]) + .contains(&"redirect_vlt_unsupported_lock_key".to_string()), "{refused:#}" ); assert!( @@ -2684,7 +2675,8 @@ async fn shrinkwrap_only_project_warns_npm12_ignores_it_and_vex_omits_it() { ); assert_eq!(code, 0, "{doc:#}"); assert!( - !warning_codes(&doc).contains(&"redirect_npm_shrinkwrap_only".to_string()), + !codes_in(&doc["redirect"]["warnings"]) + .contains(&"redirect_npm_shrinkwrap_only".to_string()), "{doc:#}" ); assert_eq!(doc["vex"]["statements"], 1, "{doc:#}"); @@ -2747,7 +2739,7 @@ async fn hosted_yarn_classic_pin_warns_berry_migration_risk() { ] { let (code, doc) = scan_hosted_json(tmp.path(), &server.uri(), extra, &[]); assert_eq!(code, 0, "{package_manager:?} {label}: {doc:#}"); - let codes = warning_codes(&doc); + let codes = codes_in(&doc["redirect"]["warnings"]); assert_eq!( codes .iter() @@ -2781,7 +2773,7 @@ async fn hosted_yarn_classic_pin_with_yarn1_package_manager_stays_silent() { let (code, doc) = scan_hosted_json(tmp.path(), &server.uri(), &[], &[]); assert_eq!(code, 0, "{doc:#}"); - let codes = warning_codes(&doc); + let codes = codes_in(&doc["redirect"]["warnings"]); assert!( !codes .iter() diff --git a/crates/socket-patch-cli/tests/covgap_commands_vendor.rs b/crates/socket-patch-cli/tests/covgap_commands_vendor.rs index 3a3b33f2e..2c7b2c61e 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_vendor.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_vendor.rs @@ -11,8 +11,11 @@ //! the built binary with a scrubbed child environment (`run_cli` / //! `vendor_cli`). No test mutates this process's environment. +#[path = "common/envelope.rs"] +mod envelope; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; +use envelope::{events, find_event}; async fn vendor_run(mut args: VendorArgs) -> i32 { let server = prebuilt_common::Server::project(&args.common.cwd); @@ -243,19 +246,6 @@ fn vendor_cli(cwd: &Path, extra: &[&str]) -> (i32, Value) { (code, env) } -fn events(envelope: &Value) -> &Vec { - envelope["events"].as_array().expect("events array") -} - -fn find_event<'a>(envelope: &'a Value, action: &str, error_code: Option<&str>) -> &'a Value { - events(envelope) - .iter() - .find(|e| e["action"] == action && error_code.is_none_or(|c| e["errorCode"] == c)) - .unwrap_or_else(|| { - panic!("expected a `{action}` event (errorCode={error_code:?}) in:\n{envelope:#}") - }) -} - /// A synthetic ledger with one entry of `eco` for [`PURL`], written via the /// real `save_state` serializer (what a tampered-but-parseable state.json /// deserializes into). diff --git a/crates/socket-patch-cli/tests/e2e_gradle_agent_build.rs b/crates/socket-patch-cli/tests/e2e_gradle_agent_build.rs index 16bcd6aa2..9d933deec 100644 --- a/crates/socket-patch-cli/tests/e2e_gradle_agent_build.rs +++ b/crates/socket-patch-cli/tests/e2e_gradle_agent_build.rs @@ -14,6 +14,7 @@ #[path = "common/mod.rs"] mod common; +use common::envelope::codes_in; use common::git_sha256; #[path = "common/hermetic.rs"] @@ -378,18 +379,8 @@ impl Cell { } } -/// The warning codes of an envelope. -fn codes(json: &serde_json::Value) -> Vec { - json["warnings"] - .as_array() - .into_iter() - .flatten() - .filter_map(|w| w["code"].as_str().map(str::to_string)) - .collect() -} - fn has(json: &serde_json::Value, code: &str) -> bool { - codes(json).iter().any(|c| c == code) + codes_in(&json["warnings"]).iter().any(|c| c == code) } /// Every file under `dir` with its sha1 (the cache trees a rollback must @@ -940,7 +931,7 @@ fn gradle_agent_cache_semantics_canaries() { let stale = has(&json, "gradle_transform_copy_stale"); report.insert("f_applyReportsStaleTransforms".into(), stale.into()); report.insert("applyExit".into(), code.into()); - report.insert("applyWarnings".into(), codes(&json).into()); + report.insert("applyWarnings".into(), codes_in(&json["warnings"]).into()); assert!( code == Some(0) || stale, "apply may only fail over derived copies it names: {json}" @@ -1046,7 +1037,10 @@ fn gradle_agent_cache_semantics_canaries() { // instruments the patched jar anew. let (statements, vex_json) = c.vex(&[]); report.insert("f_vexStatementsBeforeClear".into(), statements.into()); - report.insert("f_vexWarningsBeforeClear".into(), codes(&vex_json).into()); + report.insert( + "f_vexWarningsBeforeClear".into(), + codes_in(&vex_json["warnings"]).into(), + ); let caches = c.home.join("caches"); for entry in std::fs::read_dir(&caches).unwrap().flatten() { let name = entry.file_name().to_string_lossy().into_owned(); @@ -1066,7 +1060,10 @@ fn gradle_agent_cache_semantics_canaries() { ); let (statements, vex_json) = c.vex(&[]); report.insert("f_vexStatementsAfterClear".into(), statements.into()); - report.insert("f_vexWarningsAfterClear".into(), codes(&vex_json).into()); + report.insert( + "f_vexWarningsAfterClear".into(), + codes_in(&vex_json["warnings"]).into(), + ); assert!( statements > 0 && !has(&vex_json, "vex_gradle_unpatched_copy"), "(f) VEX must attest every record once the derived caches are rebuilt from the patched \ diff --git a/crates/socket-patch-cli/tests/e2e_sbt/agent.rs b/crates/socket-patch-cli/tests/e2e_sbt/agent.rs index aab015bdf..98d8925c6 100644 --- a/crates/socket-patch-cli/tests/e2e_sbt/agent.rs +++ b/crates/socket-patch-cli/tests/e2e_sbt/agent.rs @@ -13,6 +13,7 @@ use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; use super::common; +use super::common::envelope::codes_in; use super::sbt_common::{ write_coursier_artifact, write_ivy_artifact, write_sbt_build, Gav, SbtHome, }; @@ -774,15 +775,6 @@ fn ivy_opts(fx: &Fixture) -> (&'static str, String) { ) } -fn warning_codes(envelope: &Value) -> Vec { - [&envelope["warnings"], &envelope["vex"]["warnings"]] - .into_iter() - .filter_map(Value::as_array) - .flatten() - .filter_map(|w| w["code"].as_str().map(str::to_string)) - .collect() -} - /// An sbt build beside a Gradle build that never declares `mavenLocal()`: /// the sbt build's own `Resolver.mavenLocal` reads `~/.m2`, so its copy is /// patched, not refused as one "this Gradle build never reads". @@ -930,8 +922,9 @@ fn agent_vex_names_a_lone_partly_unpatched_ivy_copy() { let (statements, env) = vex_statements(&fx, std::slice::from_ref(&opts)); assert_eq!(statements, 0, "{env}"); assert!( - warning_codes(&env) - .iter() + [&env["warnings"], &env["vex"]["warnings"]] + .into_iter() + .flat_map(codes_in) .any(|c| c == "vex_gradle_unpatched_copy"), "{env}" ); diff --git a/crates/socket-patch-cli/tests/e2e_sbt_build.rs b/crates/socket-patch-cli/tests/e2e_sbt_build.rs index 7bf2b6dfc..c4c15dfae 100644 --- a/crates/socket-patch-cli/tests/e2e_sbt_build.rs +++ b/crates/socket-patch-cli/tests/e2e_sbt_build.rs @@ -24,6 +24,7 @@ mod sbt_build_common; use std::path::{Path, PathBuf}; +use common::envelope::all_codes; use sbt_build_common::*; use socket_patch_core::formats::sbt::owned_file::{HOSTED_FILE, HOSTED_REPO_REL}; use wiremock::matchers::{method, path}; @@ -188,28 +189,6 @@ fn get_hosted(ws: &Workspace, server: &Server, uuid: &str) -> serde_json::Value json } -fn codes(json: &serde_json::Value) -> Vec { - let mut out = Vec::new(); - fn walk(v: &serde_json::Value, out: &mut Vec) { - match v { - serde_json::Value::Object(map) => { - for (k, v) in map { - if k == "code" || k == "errorCode" { - if let Some(s) = v.as_str() { - out.push(s.to_string()); - } - } - walk(v, out); - } - } - serde_json::Value::Array(items) => items.iter().for_each(|v| walk(v, out)), - _ => {} - } - } - walk(json, &mut out); - out -} - /// The standard fixture: `top` → `lib`, the root on `top`, `a` on `lib`, /// plus `projects` / `extra` build lines; `sbt update` run. fn fixture(sbt: &Sbt, extra_projects: &[ProjectSpec<'_>], extra: &str) -> Workspace { @@ -403,7 +382,7 @@ fn sbt_hosted_no_evidence_noop() { server.grant(&sbt, UUID, "lib", BASE, &patched(), &patched()); let json = get_hosted(&ws, &server, UUID); assert!( - codes(&json).contains(&"redirect_sbt_no_resolution_evidence".to_string()), + all_codes(&json).contains(&"redirect_sbt_no_resolution_evidence".to_string()), "{json}" ); assert!(!ws.project.join(HOSTED_FILE).exists()); @@ -420,7 +399,7 @@ fn sbt_hosted_version_conflict_refused() { server.grant(&sbt, UUID, "lib", BASE, &patched(), &patched()); let json = get_hosted(&ws, &server, UUID); assert!( - codes(&json).contains(&"redirect_sbt_version_conflict".to_string()), + all_codes(&json).contains(&"redirect_sbt_version_conflict".to_string()), "{json}" ); assert!(!ws.project.join(HOSTED_FILE).exists()); @@ -444,7 +423,7 @@ fn sbt_hosted_zz_override_assignment_refused() { server.grant(&sbt, UUID, "lib", BASE, &patched(), &patched()); let json = get_hosted(&ws, &server, UUID); assert!( - codes(&json).contains(&"redirect_sbt_overrides_assignment".to_string()), + all_codes(&json).contains(&"redirect_sbt_overrides_assignment".to_string()), "{json}" ); assert!(!ws.project.join(HOSTED_FILE).exists()); @@ -504,13 +483,13 @@ fn sbt_hosted_vex_attests_after_update() { // Before the post-wiring update: the pin is not verified. let (_, json, _) = vex("before update"); assert!( - codes(&json).contains(&"sbt_resolution_unverified".to_string()), + all_codes(&json).contains(&"sbt_resolution_unverified".to_string()), "{json}" ); sbt.run(&ws, &["update"]).expect_ok("post-wiring update"); let (code, json, out) = vex("after update"); assert!( - !codes(&json).contains(&"sbt_resolution_unverified".to_string()), + !all_codes(&json).contains(&"sbt_resolution_unverified".to_string()), "{out}" ); assert_eq!(code, 0, "{out}"); @@ -547,14 +526,14 @@ fn sbt_hosted_dep_edit_rechecks_after_update() { // Edited, not resolved yet. let json = get_hosted(&ws, &server, UUID); assert!( - codes(&json).contains(&"redirect_sbt_pin_unverifiable".to_string()), + all_codes(&json).contains(&"redirect_sbt_pin_unverifiable".to_string()), "{json}" ); assert_eq!(std::fs::read(ws.project.join(HOSTED_FILE)).unwrap(), before); sbt.run(&ws, &["update"]).expect_ok("update after the edit"); let json = get_hosted(&ws, &server, UUID); assert!( - !codes(&json).contains(&"redirect_sbt_pin_unverifiable".to_string()), + !all_codes(&json).contains(&"redirect_sbt_pin_unverifiable".to_string()), "{json}" ); assert_eq!(json["redirect"]["redirected"], 1, "{json}"); @@ -563,7 +542,7 @@ fn sbt_hosted_dep_edit_rechecks_after_update() { // ...and the next run is quiet. let json = get_hosted(&ws, &server, UUID); assert!( - !codes(&json).contains(&"redirect_sbt_pin_unverifiable".to_string()), + !all_codes(&json).contains(&"redirect_sbt_pin_unverifiable".to_string()), "{json}" ); assert_eq!(std::fs::read(ws.project.join(HOSTED_FILE)).unwrap(), after); @@ -592,7 +571,7 @@ fn sbt_hosted_declared_bump_fails_closed() { .expect_failure("declared bump", "declares dev.socket.fixture:lib:1.1.0"); let json = get_hosted(&ws, &server, UUID); assert!( - codes(&json).contains(&"redirect_sbt_pin_declared_newer".to_string()), + all_codes(&json).contains(&"redirect_sbt_pin_declared_newer".to_string()), "{json}" ); assert_eq!(json["redirect"]["redirected"], 0, "{json}"); diff --git a/crates/socket-patch-cli/tests/e2e_sbt_hosted.rs b/crates/socket-patch-cli/tests/e2e_sbt_hosted.rs index d2e83922b..b7033fdea 100644 --- a/crates/socket-patch-cli/tests/e2e_sbt_hosted.rs +++ b/crates/socket-patch-cli/tests/e2e_sbt_hosted.rs @@ -29,6 +29,7 @@ mod sbt_common; use std::path::{Path, PathBuf}; use std::time::{Duration, SystemTime}; +use common::envelope::all_codes; use sbt_common::{copy_evidence, record_pinned_resolution, write_sbt_build, Gav, SbtHome}; use socket_patch_core::formats::sbt::owned_file::{ render, SbtFileMode, SbtLine, SbtOwnedFile, SbtPin, HOSTED_FILE, HOSTED_REPO_REL, @@ -165,29 +166,6 @@ fn get_hosted(home: &SbtHome, project: &Path, api: &Api, uuid: &str) -> (i32, se (code, json) } -/// The warning codes anywhere in an envelope. -fn codes(json: &serde_json::Value) -> Vec { - let mut out = Vec::new(); - fn walk(v: &serde_json::Value, out: &mut Vec) { - match v { - serde_json::Value::Object(map) => { - for (k, v) in map { - if k == "code" || k == "errorCode" { - if let Some(s) = v.as_str() { - out.push(s.to_string()); - } - } - walk(v, out); - } - } - serde_json::Value::Array(items) => items.iter().for_each(|v| walk(v, out)), - _ => {} - } - } - walk(json, &mut out); - out -} - /// The test-compile evidence build, its records touched newer than every /// build source (copying sets every mtime to "now", in copy order). fn evidence_project(tmp: &Path) -> PathBuf { @@ -247,7 +225,7 @@ fn hosted_no_evidence_warns_and_writes_nothing() { api.grant(UUID, GROUP, ARTIFACT, BASE); let (_, json) = get_hosted(&home, &project, &api, UUID); assert!( - codes(&json).contains(&"redirect_sbt_no_resolution_evidence".to_string()), + all_codes(&json).contains(&"redirect_sbt_no_resolution_evidence".to_string()), "{json}" ); assert!(!project.join(HOSTED_FILE).exists(), "nothing is wired"); @@ -281,7 +259,7 @@ fn hosted_mixed_root_confirms_the_pom_pin_when_sbt_refuses() { api.grant(UUID, GROUP, ARTIFACT, BASE); let (_, json) = get_hosted(&home, &project, &api, UUID); assert!( - codes(&json).contains(&"redirect_sbt_no_resolution_evidence".to_string()), + all_codes(&json).contains(&"redirect_sbt_no_resolution_evidence".to_string()), "{json}" ); assert!( @@ -373,7 +351,7 @@ fn hosted_unreadable_owned_file_is_refused_untouched() { // A whole-run refusal: `get` fails, with nothing written. assert_eq!(code, 1, "{json}"); assert!( - codes(&json).contains(&"redirect_sbt_owned_file_unreadable".to_string()), + all_codes(&json).contains(&"redirect_sbt_owned_file_unreadable".to_string()), "{json}" ); assert_eq!(std::fs::read(project.join(HOSTED_FILE)).unwrap(), latin1); @@ -388,7 +366,7 @@ fn hosted_version_conflict_is_refused() { api.grant(TEXT_UUID, "org.apache.commons", "commons-text", "1.9"); let (_, json) = get_hosted(&home, &project, &api, TEXT_UUID); assert!( - codes(&json).contains(&"redirect_sbt_version_conflict".to_string()), + all_codes(&json).contains(&"redirect_sbt_version_conflict".to_string()), "{json}" ); assert!(!project.join(HOSTED_FILE).exists()); @@ -442,7 +420,7 @@ fn hosted_vex_without_evidence_is_unverified() { api.grant(UUID, GROUP, ARTIFACT, BASE); let (_, json) = vex(&home, &project, &api.server.uri()); assert!( - codes(&json).contains(&"sbt_resolution_unverified".to_string()), + all_codes(&json).contains(&"sbt_resolution_unverified".to_string()), "{json}" ); let doc = std::fs::read_to_string(project.join("vex.json")).unwrap_or_default(); @@ -505,14 +483,16 @@ fn hosted_rerun_checks_the_pin_by_content_not_location() { let (_, json) = get_hosted(&home, &project, &api, UUID); assert_eq!(json["redirect"]["redirected"], 1, "{json}"); assert!( - !codes(&json).iter().any(|c| c.starts_with("redirect_sbt_")), + !all_codes(&json) + .iter() + .any(|c| c.starts_with("redirect_sbt_")), "{json}" ); std::fs::write(&jar, b"other bytes").unwrap(); let (_, json) = get_hosted(&home, &project, &api, UUID); assert!( - codes(&json).contains(&"redirect_sbt_resolved_elsewhere".to_string()), + all_codes(&json).contains(&"redirect_sbt_resolved_elsewhere".to_string()), "{json}" ); } @@ -543,7 +523,7 @@ fn hosted_vex_attests_with_post_wiring_evidence() { api.grant(UUID, GROUP, ARTIFACT, BASE); let (_, json) = vex(&home, &project, &api.server.uri()); assert!( - !codes(&json).contains(&"sbt_resolution_unverified".to_string()), + !all_codes(&json).contains(&"sbt_resolution_unverified".to_string()), "{json}" ); let vex = std::fs::read_to_string(project.join("vex.json")).unwrap(); diff --git a/crates/socket-patch-cli/tests/e2e_sbt_vendor.rs b/crates/socket-patch-cli/tests/e2e_sbt_vendor.rs index f44240f1d..404ac329e 100644 --- a/crates/socket-patch-cli/tests/e2e_sbt_vendor.rs +++ b/crates/socket-patch-cli/tests/e2e_sbt_vendor.rs @@ -16,6 +16,7 @@ #[path = "common/mod.rs"] mod common; +use common::envelope::event_triples; use common::git_sha256; #[path = "common/hermetic.rs"] @@ -300,18 +301,6 @@ impl Fixture { } } -fn events(env: &serde_json::Value) -> Vec<(String, String, String)> { - env["events"] - .as_array() - .unwrap() - .iter() - .map(|e| { - let s = |k: &str| e[k].as_str().unwrap_or_default().to_string(); - (s("purl"), s("action"), s("errorCode")) - }) - .collect() -} - /// The ledger entry, the generated file and the tree: every record a /// planner can produce for the coordinates, no user file edited, and a /// re-run (and `--check`) in sync. @@ -417,7 +406,7 @@ fn vendor_ledger_shape_and_generated_file() { fx.sbt_update(&[(GSON, GSON_UUID)]); let env = fx.ok(&["vendor"]); assert_eq!( - events(&env), + event_triples(&env), [( GSON.purl(), "skipped".to_string(), @@ -462,7 +451,7 @@ fn vendor_two_patches_roll_back_in_either_order() { assert_eq!(left.pins.values().next().unwrap().artifact, other.artifact); let env = fx.ok(&["vendor"]); assert_eq!( - events(&env), + event_triples(&env), [( other.purl(), "skipped".to_string(), @@ -492,7 +481,7 @@ fn vendor_patch_update_replaces_the_pin_and_sweeps() { .unwrap(); let env = fx.ok(&["vendor"]); assert!( - events(&env) + event_triples(&env) .iter() .any(|(_, _, code)| code == "vendor_stale_artifact_removed"), "{env}" @@ -540,7 +529,7 @@ fn vendor_check_drift_rerun_and_repair() { std::fs::rename(&cache, &parked).unwrap(); let env = fx.ok(&["vendor"]); assert_eq!( - events(&env), + event_triples(&env), [( GSON.purl(), "skipped".to_string(), @@ -746,7 +735,7 @@ fn vendor_no_evidence_warns_and_exits_zero() { // counted applied. assert_eq!(env["summary"]["applied"], 0, "{env}"); assert!( - events(&env).contains(&( + event_triples(&env).contains(&( GSON.purl(), "skipped".to_string(), "vendor_sbt_no_resolution_evidence".to_string() @@ -809,7 +798,7 @@ fn vendor_takeover_the_gate_would_skip_keeps_the_hosted_pin() { let (exit, env) = fx.run(&["vendor"]); assert_eq!(exit, Some(1), "{env}"); assert!( - events(&env).contains(&( + event_triples(&env).contains(&( GSON.purl(), "failed".to_string(), "vendor_sbt_no_resolution_evidence".to_string() @@ -892,7 +881,7 @@ fn vendor_eject_the_gate_would_skip_is_refused() { assert_eq!(exit, Some(1), "{env}"); assert_eq!(env["error"]["code"], "eject_refused", "{env}"); assert!( - events(&env).contains(&( + event_triples(&env).contains(&( GSON.purl(), "failed".to_string(), "vendor_sbt_no_resolution_evidence".to_string() diff --git a/crates/socket-patch-cli/tests/e2e_sbt_vendor_build.rs b/crates/socket-patch-cli/tests/e2e_sbt_vendor_build.rs index 57933cbbb..0488caffc 100644 --- a/crates/socket-patch-cli/tests/e2e_sbt_vendor_build.rs +++ b/crates/socket-patch-cli/tests/e2e_sbt_vendor_build.rs @@ -33,6 +33,7 @@ #[path = "common/mod.rs"] mod common; +use common::envelope::event_triples; use common::git_sha256; #[path = "common/hermetic.rs"] @@ -337,18 +338,6 @@ fn fresh_checkout(proj: &Path, dst: &Path) { } } -fn events(env: &serde_json::Value) -> Vec<(String, String, String)> { - env["events"] - .as_array() - .unwrap() - .iter() - .map(|e| { - let s = |k: &str| e[k].as_str().unwrap_or_default().to_string(); - (s("purl"), s("action"), s("errorCode")) - }) - .collect() -} - #[test] #[ignore = "real sbt + network (SOCKET_PATCH_SBT_E2E_*)"] fn sbt_vendor_offline_fresh_checkout() { @@ -446,7 +435,7 @@ fn sbt_vendor_declared_bump_fails_closed() { let (code, env) = ctx.socket(&ctx.proj, &["vendor"]); assert_eq!(code, Some(1), "{env}"); assert!( - events(&env) + event_triples(&env) .iter() .any(|(_, action, code)| action == "failed" && code == "vendor_sbt_pin_declared_newer"), "{env}" @@ -538,7 +527,7 @@ fn sbt_vendor_rerun_noop() { let before = snapshot(&ctx.proj); let env = ctx.ok(&["vendor"]); assert_eq!( - events(&env), + event_triples(&env), [( purl(TEXT), "skipped".to_string(), diff --git a/crates/socket-patch-cli/tests/e2e_scala_cli_vendor.rs b/crates/socket-patch-cli/tests/e2e_scala_cli_vendor.rs index 560a95871..425a0d8cb 100644 --- a/crates/socket-patch-cli/tests/e2e_scala_cli_vendor.rs +++ b/crates/socket-patch-cli/tests/e2e_scala_cli_vendor.rs @@ -29,6 +29,7 @@ #[path = "common/mod.rs"] mod common; +use common::envelope::all_codes; use common::git_sha256; #[path = "common/hermetic.rs"] @@ -300,29 +301,6 @@ fn ok(root: &Path, home: &SbtHome, args: &[&str]) -> serde_json::Value { env } -/// Every `code` / `errorCode` string anywhere in the envelope. -fn codes(env: &serde_json::Value) -> Vec { - fn walk(v: &serde_json::Value, out: &mut Vec) { - match v { - serde_json::Value::Object(m) => { - for (k, v) in m { - if k == "code" || k == "errorCode" { - if let Some(s) = v.as_str() { - out.push(s.to_string()); - } - } - walk(v, out); - } - } - serde_json::Value::Array(a) => a.iter().for_each(|v| walk(v, out)), - _ => {} - } - } - let mut out = Vec::new(); - walk(env, &mut out); - out -} - /// Every file under `proj`, minus the manifest, blobs and `.scala-build` /// (the inputs the tests stage). fn snapshot(root: &Path) -> BTreeMap>> { @@ -371,7 +349,7 @@ mod hermetic { let env = ok(root, &home, &["vendor"]); assert_eq!(env["summary"]["applied"], 1, "{env}"); assert!( - codes(&env).contains(&"vendor_scala_cli_directives_split".to_string()), + all_codes(&env).contains(&"vendor_scala_cli_directives_split".to_string()), "{env}" ); let proj = root.join("proj"); @@ -489,7 +467,7 @@ mod hermetic { } else { "vendor_scala_cli_resolution_missing" }; - assert!(codes(&env).contains(&want.to_string()), "{case}: {env}"); + assert!(all_codes(&env).contains(&want.to_string()), "{case}: {env}"); assert_eq!( env["summary"]["applied"], 0, "{case}: a skip is not applied: {env}" @@ -523,7 +501,10 @@ mod hermetic { "vendor_scala_cli_resolution_missing", "vendor_scala_cli_resolution_stale", ] { - assert!(!codes(&env).contains(&code.to_string()), "{case}: {env}"); + assert!( + !all_codes(&env).contains(&code.to_string()), + "{case}: {env}" + ); } ok(root, &home, &["vendor", "--check"]); } @@ -555,7 +536,7 @@ mod hermetic { "directive" => "vendor_scala_cli_repository_shadowed", _ => "vendor_scala_cli_owned_file_modified", }; - assert!(codes(&env).contains(&want.to_string()), "{case}: {env}"); + assert!(all_codes(&env).contains(&want.to_string()), "{case}: {env}"); assert_eq!(snapshot(root), before, "{case}: nothing is written"); } } @@ -605,7 +586,7 @@ fn windows_is_refused() { let (code, env) = socket(root, &home, &["vendor"]); assert_eq!(code, Some(1), "{env}"); assert!( - codes(&env).contains(&"vendor_scala_cli_windows_unsupported".to_string()), + all_codes(&env).contains(&"vendor_scala_cli_windows_unsupported".to_string()), "{env}" ); assert_eq!(snapshot(root), before); @@ -800,13 +781,16 @@ mod real_tool { // The vendored GAV keeps being served: re-planned in sync, no // "not vendored" skip. let env = ok(&root, &home, &["vendor", "--offline"]); - assert!(!codes(&env).contains(&stale), "edited, not compiled: {env}"); + assert!( + !all_codes(&env).contains(&stale), + "edited, not compiled: {env}" + ); assert_eq!(env["summary"]["applied"], 0, "{env}"); let (built, out) = run(&bin, &proj, &["compile", "--test", "."]); assert!(built, "{out}"); let env = ok(&root, &home, &["vendor", "--offline"]); assert!( - !codes(&env).contains(&stale), + !all_codes(&env).contains(&stale), "edited, then compiled: {env}" ); assert_eq!(env["summary"]["applied"], 0, "{env}"); diff --git a/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs index db9d1bb30..cc7cbe6fe 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_bun_build.rs @@ -76,7 +76,7 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::{binary, git_sha256, parse_json_envelope}; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; @@ -304,11 +304,6 @@ fn sri(bytes: &[u8]) -> String { format!("sha512-{}", b64(&Sha512::digest(bytes))) } -fn parse_envelope(stdout: &str) -> serde_json::Value { - serde_json::from_str(stdout) - .unwrap_or_else(|e| panic!("--json output is not JSON: {e}\nstdout:\n{stdout}")) -} - fn copy_dir_recursive(src: &Path, dst: &Path) { std::fs::create_dir_all(dst).unwrap(); for entry in std::fs::read_dir(src).unwrap() { @@ -1034,7 +1029,7 @@ fn stage_and_vendor(fx: &BunProject) { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!(env["summary"]["applied"], 1, "one package vendored: {env}"); assert_eq!(env["summary"]["failed"], 0, "no failures: {env}"); @@ -1105,7 +1100,7 @@ fn bun_vendor_fresh_checkout_frozen_install_and_revert() { code, 0, "repair failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["status"], "success", "repair envelope: {renv}"); assert_eq!( renv["summary"]["rebuilt"], 1, @@ -1143,7 +1138,7 @@ fn bun_vendor_fresh_checkout_frozen_install_and_revert() { code, 0, "re-vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env2 = parse_envelope(&stdout); + let env2 = parse_json_envelope(&stdout); assert_eq!(env2["summary"]["failed"], 0, "re-run must not fail: {env2}"); assert_eq!( env2["summary"]["applied"], 0, @@ -1184,7 +1179,7 @@ fn bun_vendor_fresh_checkout_frozen_install_and_revert() { code, 0, "revert failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["status"], "success", "revert envelope: {renv}"); assert_eq!(renv["summary"]["removed"], 1, "one entry reverted: {renv}"); assert_eq!( @@ -1373,7 +1368,7 @@ async fn bun_get_uuid_vendored_fresh_checkout_frozen_install() { code, 0, "get --mode vendored failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!(env["found"], 1, "envelope: {env}"); assert_eq!(env["downloaded"], 1, "envelope: {env}"); @@ -1568,7 +1563,7 @@ fn bun_vendor_survives_a_digest_dropping_lock_resave() { code, 0, "re-vendor over the re-saved lock failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "{env}"); assert_eq!(env["summary"]["failed"], 0, "{env}"); assert!( @@ -1610,7 +1605,7 @@ fn bun_vendor_survives_a_digest_dropping_lock_resave() { code, 0, "repair failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["status"], "success", "{renv}"); assert_eq!(renv["summary"]["rebuilt"], 1, "{renv}"); assert!(vendored_tgz(&fx).is_file(), "the artifact must be rebuilt"); @@ -1648,7 +1643,7 @@ fn bun_vendor_survives_a_digest_dropping_lock_resave() { code, 0, "revert over the re-saved lock failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["status"], "success", "{renv}"); assert_eq!(renv["summary"]["removed"], 1, "{renv}"); let restored = std::fs::read_to_string(&lock_path).unwrap(); diff --git a/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs index c9af54b82..7102bd73e 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_cargo_build.rs @@ -81,7 +81,7 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::{binary, git_sha256, parse_json_envelope}; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; @@ -276,11 +276,6 @@ fn stage_patch(proj: &Path, purl: &str, file_key: &str, before: &[u8], after: &[ std::fs::write(socket.join("blobs").join(git_sha256(after)), after).unwrap(); } -fn parse_envelope(stdout: &str) -> serde_json::Value { - serde_json::from_str(stdout) - .unwrap_or_else(|e| panic!("vendor --json output is not JSON: {e}\nstdout:\n{stdout}")) -} - fn copy_dir_recursive(src: &Path, dst: &Path) { std::fs::create_dir_all(dst).unwrap(); for entry in std::fs::read_dir(src).unwrap() { @@ -638,7 +633,7 @@ fn cargo_vendor_fresh_checkout_locked_offline_build_and_revert() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!(env["summary"]["failed"], 0, "no failures: {env}"); // summary.applied / the event action are pinned by @@ -871,7 +866,7 @@ fn cargo_vendor_fresh_checkout_locked_offline_build_and_revert() { code, 0, "revert failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["status"], "success", "revert envelope: {renv}"); assert_eq!(renv["summary"]["removed"], 1, "one entry reverted: {renv}"); assert_eq!( @@ -931,7 +926,7 @@ fn cargo_vendor_reports_applied_event() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!( env["summary"]["applied"], 1, "a successful first-time vendor must count as applied: {env}" @@ -1041,7 +1036,7 @@ async fn cargo_get_uuid_vendored_fresh_checkout_locked_build() { code, 0, "get --mode vendored failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!(env["found"], 1, "envelope: {env}"); assert_eq!(env["downloaded"], 1, "envelope: {env}"); @@ -1215,7 +1210,7 @@ fn vendor_ok(proj: &Path, cargo_home: &Path, tag: &str) -> serde_json::Value { code, 0, "{tag}: vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - parse_envelope(&stdout) + parse_json_envelope(&stdout) } fn revert_ok(proj: &Path, cargo_home: &Path, tag: &str) { diff --git a/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs index f3f1f376b..f6e8aa18c 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs @@ -60,7 +60,7 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::{binary, git_sha256, parse_json_envelope}; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; @@ -185,11 +185,6 @@ fn stage_patch_with_vuln(proj: &Path, purl: &str, file_key: &str, before: &[u8], std::fs::write(socket.join("blobs").join(git_sha256(after)), after).unwrap(); } -fn parse_envelope(stdout: &str) -> serde_json::Value { - serde_json::from_str(stdout) - .unwrap_or_else(|e| panic!("vendor --json output is not JSON: {e}\nstdout:\n{stdout}")) -} - fn copy_dir_recursive(src: &Path, dst: &Path) { std::fs::create_dir_all(dst).unwrap(); for entry in std::fs::read_dir(src).unwrap() { @@ -510,7 +505,7 @@ fn gem_vendor_fresh_checkout_bundle_install_and_revert() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!(env["summary"]["applied"], 1, "one package vendored: {env}"); assert_eq!(env["summary"]["failed"], 0, "no failures: {env}"); @@ -699,7 +694,7 @@ fn gem_vendor_fresh_checkout_bundle_install_and_revert() { code, 0, "re-vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env2 = parse_envelope(&stdout); + let env2 = parse_json_envelope(&stdout); assert_eq!(env2["summary"]["failed"], 0, "re-run must not fail: {env2}"); assert_eq!( std::fs::read(&gemfile_path).unwrap(), @@ -728,7 +723,7 @@ fn gem_vendor_fresh_checkout_bundle_install_and_revert() { code, 0, "revert failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["status"], "success", "revert envelope: {renv}"); assert_eq!(renv["summary"]["removed"], 1, "one entry reverted: {renv}"); assert_eq!( @@ -879,7 +874,7 @@ fn gem_vendor_transitive_dep_fresh_checkout_and_revert() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["summary"]["applied"], 1, "one package vendored: {env}"); assert_eq!(env["summary"]["failed"], 0, "no failures: {env}"); @@ -1003,7 +998,7 @@ fn gem_vendor_transitive_dep_fresh_checkout_and_revert() { code, 0, "re-vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env2 = parse_envelope(&stdout); + let env2 = parse_json_envelope(&stdout); assert_eq!(env2["summary"]["failed"], 0, "re-run must not fail: {env2}"); assert_eq!( std::fs::read(&gemfile_path).unwrap(), @@ -1033,7 +1028,7 @@ fn gem_vendor_transitive_dep_fresh_checkout_and_revert() { code, 0, "revert failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["summary"]["removed"], 1, "one entry reverted: {renv}"); assert_eq!( std::fs::read(&gemfile_path).unwrap(), @@ -1207,7 +1202,7 @@ async fn gem_get_uuid_vendored_fresh_checkout_bundle_install() { code, 0, "get --mode vendored failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!(env["found"], 1, "envelope: {env}"); assert_eq!(env["downloaded"], 1, "envelope: {env}"); @@ -1473,7 +1468,7 @@ fn assert_vendor_refuses_unloaded_gemfile(proj: &Path, purl: &str, loaded: &str, code, 0, "vendor must not succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["summary"]["applied"], 0, "nothing vendored: {env}"); let event = env["events"] .as_array() @@ -1595,7 +1590,7 @@ fn gem_vendor_refuses_an_eval_gemfile_direct_dep() { code, 0, "vendor must not succeed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["summary"]["applied"], 0, "nothing vendored: {env}"); let event = env["events"] .as_array() @@ -1758,7 +1753,7 @@ fn gem_vendor_second_gem_section_fresh_checkout_and_revert() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["summary"]["applied"], 1, "one package vendored: {env}"); assert_eq!(env["summary"]["failed"], 0, "no failures: {env}"); @@ -1843,7 +1838,7 @@ fn gem_vendor_second_gem_section_fresh_checkout_and_revert() { code, 0, "revert failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["summary"]["removed"], 1, "one entry reverted: {renv}"); assert_eq!( std::fs::read(&gemfile_path).unwrap(), @@ -1900,7 +1895,7 @@ fn gem_vendor_drops_positional_constraints() { code, 0, "{gemfile:?}: vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["summary"]["applied"], 1, "{gemfile:?}: {env}"); let wired = std::fs::read_to_string(proj.join("Gemfile")).unwrap(); diff --git a/crates/socket-patch-cli/tests/e2e_vendor_golang_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_golang_build.rs index b306b8775..e24d1ea89 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_golang_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_golang_build.rs @@ -27,7 +27,8 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::envelope::find_event; +use common::{binary, git_sha256, parse_json_envelope}; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; @@ -220,22 +221,6 @@ fn write_patch(consumer: &Path) { .unwrap(); } -fn parse_envelope(stdout: &str) -> serde_json::Value { - serde_json::from_str(stdout) - .unwrap_or_else(|e| panic!("--json output is not JSON: {e}\nstdout:\n{stdout}")) -} - -fn find_event<'a>( - env: &'a serde_json::Value, - action: &str, - error_code: &str, -) -> Option<&'a serde_json::Value> { - env["events"] - .as_array()? - .iter() - .find(|e| e["action"] == action && e["errorCode"] == error_code) -} - fn copy_dir_recursive(src: &Path, dst: &Path) { std::fs::create_dir_all(dst).unwrap(); for entry in std::fs::read_dir(src).unwrap() { @@ -395,7 +380,7 @@ fn go_vendor_fresh_checkout_offline_build_and_revert() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!(env["summary"]["failed"], 0, "no failures: {env}"); // summary.applied / the event action are pinned by @@ -554,7 +539,7 @@ fn go_vendor_fresh_checkout_offline_build_and_revert() { code, 0, "revert failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["status"], "success", "revert envelope: {renv}"); assert_eq!(renv["summary"]["removed"], 1, "one entry reverted: {renv}"); assert_eq!( @@ -667,7 +652,7 @@ async fn go_get_uuid_vendored_fresh_checkout_offline_build() { code, 0, "get --mode vendored failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!(env["downloaded"], 1, "one record downloaded: {env}"); assert!( @@ -859,12 +844,10 @@ fn go_apply_vendor_interplay_takeover_and_yield() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "takeover is a success: {env}"); - assert!( - find_event(&env, "skipped", "vendor_takeover").is_some(), - "the takeover must be surfaced as a `vendor_takeover` event: {env}" - ); + // The takeover must be surfaced as a `vendor_takeover` event. + find_event(&env, "skipped", Some("vendor_takeover")); let gomod = std::fs::read_to_string(consumer.join("go.mod")).unwrap(); let expected_replace = @@ -918,11 +901,10 @@ fn go_apply_vendor_interplay_takeover_and_yield() { code, 0, "apply on a vendored module must exit 0.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let aenv = parse_envelope(&stdout); + let aenv = parse_json_envelope(&stdout); assert_eq!(aenv["status"], "success", "apply envelope: {aenv}"); - let yielded = find_event(&aenv, "skipped", "vendored").unwrap_or_else(|| { - panic!("apply must skip the vendored purl with errorCode `vendored`: {aenv}") - }); + // Apply must skip the vendored purl with errorCode `vendored`. + let yielded = find_event(&aenv, "skipped", Some("vendored")); assert_eq!( yielded["purl"], UPURL, "the vendored purl is the one skipped: {aenv}" @@ -968,11 +950,9 @@ fn go_apply_vendor_interplay_takeover_and_yield() { code, 0, "revert failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); - assert!( - find_event(&renv, "skipped", "takeover_not_restored").is_some(), - "revert must warn that the go-patches redirect was not restored: {renv}" - ); + let renv = parse_json_envelope(&stdout); + // Revert must warn that the go-patches redirect was not restored. + find_event(&renv, "skipped", Some("takeover_not_restored")); let gomod_reverted = std::fs::read_to_string(consumer.join("go.mod")).unwrap(); assert!( !gomod_reverted.contains("replace "), @@ -1033,7 +1013,7 @@ fn go_vendor_reports_applied_event() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!( env["summary"]["applied"], 1, "a successful first-time vendor must count as applied: {env}" diff --git a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs index c1f53a99a..eea20c50a 100644 --- a/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs +++ b/crates/socket-patch-cli/tests/e2e_vendor_yarn_classic_build.rs @@ -48,7 +48,7 @@ #[path = "common/mod.rs"] mod common; -use common::{binary, git_sha256}; +use common::{binary, git_sha256, parse_json_envelope}; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; @@ -168,11 +168,6 @@ fn stage_patch(proj: &Path, purl: &str, file_key: &str, before: &[u8], after: &[ std::fs::write(socket.join("blobs").join(git_sha256(after)), after).unwrap(); } -fn parse_envelope(stdout: &str) -> serde_json::Value { - serde_json::from_str(stdout) - .unwrap_or_else(|e| panic!("vendor --json output is not JSON: {e}\nstdout:\n{stdout}")) -} - fn copy_dir_recursive(src: &Path, dst: &Path) { std::fs::create_dir_all(dst).unwrap(); for entry in std::fs::read_dir(src).unwrap() { @@ -276,7 +271,7 @@ fn yarn_classic_vendor_fresh_checkout_frozen_offline_install_and_revert() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!(env["summary"]["applied"], 1, "one package vendored: {env}"); assert_eq!(env["summary"]["failed"], 0, "no failures: {env}"); @@ -526,7 +521,7 @@ fn yarn_classic_vendor_fresh_checkout_frozen_offline_install_and_revert() { code, 0, "re-vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env2 = parse_envelope(&stdout); + let env2 = parse_json_envelope(&stdout); assert_eq!(env2["summary"]["failed"], 0, "re-run must not fail: {env2}"); assert_eq!( std::fs::read(&lock_path).unwrap(), @@ -562,7 +557,7 @@ fn yarn_classic_vendor_fresh_checkout_frozen_offline_install_and_revert() { code, 0, "pinned re-vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env3 = parse_envelope(&stdout); + let env3 = parse_json_envelope(&stdout); assert!( env3.get("warnings").is_none(), "a yarn@1 packageManager pin must suppress the advisory (and empty \ @@ -585,7 +580,7 @@ fn yarn_classic_vendor_fresh_checkout_frozen_offline_install_and_revert() { code, 0, "revert failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let renv = parse_envelope(&stdout); + let renv = parse_json_envelope(&stdout); assert_eq!(renv["status"], "success", "revert envelope: {renv}"); assert_eq!(renv["summary"]["removed"], 1, "one entry reverted: {renv}"); assert!( @@ -747,7 +742,7 @@ fn yarn_classic_detached_scan_vendored_fresh_checkout_manifestless_vex() { code, 0, "scan --mode vendored failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["status"], "success", "envelope: {env}"); assert_eq!( env["vendor"]["summary"]["applied"], 1, @@ -944,7 +939,7 @@ fn yarn_classic_vendored_tarball_survives_a_tgz_gitignore_rule() { code, 0, "vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!(env["summary"]["applied"], 1, "one package vendored: {env}"); git(&proj, &["add", "-A"]); @@ -1006,7 +1001,7 @@ fn yarn_classic_vendored_tarball_survives_a_tgz_gitignore_rule() { code, 1, "vendor --check must fail on an unledgered lock reference.\nstdout:\n{stdout}\nstderr:\n{stderr}" ); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert!( env["events"].as_array().unwrap().iter().any(|e| { e["errorCode"] == "vendor_ledger_missing" @@ -1124,7 +1119,7 @@ fn yarn_classic_vendor_refuses_a_symlinked_lock() { &["vendor", "--json", "--offline", "--dry-run", "--cwd", cwd], ); assert_eq!(code, 0, "dry run.\nstdout:\n{stdout}\nstderr:\n{stderr}"); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert!( env["events"].as_array().unwrap().iter().any(|e| { e["errorCode"] == "vendor_would_refuse_symlinked_file" @@ -1136,7 +1131,7 @@ fn yarn_classic_vendor_refuses_a_symlinked_lock() { let (code, stdout, stderr) = run_socket(&proj, &["vendor", "--json", "--offline", "--cwd", cwd]); assert_eq!(code, 1, "vendor.\nstdout:\n{stdout}\nstderr:\n{stderr}"); - let env = parse_envelope(&stdout); + let env = parse_json_envelope(&stdout); assert_eq!( env["error"]["code"], "redirect_symlinked_file_unsupported", "{env}" diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/golang.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/golang.rs index 336d5a660..8ed5174b5 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/golang.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/golang.rs @@ -44,6 +44,7 @@ //! Embedded: `scan --vex` and `apply --vex` on a manifest-less project. use crate::common::binary; +use crate::common::envelope::codes_in; use crate::vex_e2e_common; @@ -437,17 +438,6 @@ fn subcomponents(doc: &Value) -> Vec { purls } -fn warning_codes(run: &Run) -> Vec { - run.env["warnings"] - .as_array() - .map(|w| { - w.iter() - .filter_map(|w| w["code"].as_str().map(str::to_string)) - .collect() - }) - .unwrap_or_default() -} - // ── golang fixtures ─────────────────────────────────────────────────── fn go_socket_module(uuid: &str) -> String { @@ -564,7 +554,11 @@ fn golang_hosted_a_attests_every_flavor_without_manifest_or_ledgers() { write_go_hosted(&fx, U, file); let run = fx.vex(&["--proxy-url", &api.uri()]); assert_attested(&fx, &run, GO_PURL, U, "redirected", &format!("{file:?}")); - assert!(warning_codes(&run).is_empty(), "{file:?}: {}", run.env); + assert!( + codes_in(&run.env["warnings"]).is_empty(), + "{file:?}: {}", + run.env + ); } } @@ -795,7 +789,11 @@ fn golang_vendored_a_attests_every_flavor_without_manifest_or_ledgers() { write_go_vendored(&fx, U, file, PATCHED_GO); let run = fx.vex(&["--proxy-url", &api.uri()]); assert_attested(&fx, &run, GO_PURL, U, "vendored", &format!("{file:?}")); - assert!(warning_codes(&run).is_empty(), "{file:?}: {}", run.env); + assert!( + codes_in(&run.env["warnings"]).is_empty(), + "{file:?}: {}", + run.env + ); } } @@ -810,7 +808,7 @@ fn golang_vendored_a_ignores_a_pristine_module_cache_copy() { let run = fx.vex(&["--proxy-url", &api.uri()]); assert_attested(&fx, &run, GO_PURL, U, "vendored", "pristine cache copy"); // …and that immutable cache copy is not "drift" to disclose. - assert!(warning_codes(&run).is_empty(), "{}", run.env); + assert!(codes_in(&run.env["warnings"]).is_empty(), "{}", run.env); } #[test] diff --git a/crates/socket-patch-cli/tests/e2e_vex_lockfile/sbt_vendored.rs b/crates/socket-patch-cli/tests/e2e_vex_lockfile/sbt_vendored.rs index 600e740ad..535f44529 100644 --- a/crates/socket-patch-cli/tests/e2e_vex_lockfile/sbt_vendored.rs +++ b/crates/socket-patch-cli/tests/e2e_vex_lockfile/sbt_vendored.rs @@ -19,6 +19,7 @@ //! | d | the generated file edited | `sbt_owned_file_modified` | //! | e | no ledger | nothing attested; the tree check still runs | +use crate::common::envelope::all_codes; use crate::common::git_sha256; use std::path::{Path, PathBuf}; @@ -184,29 +185,6 @@ impl Fx { } } -/// Every `code` / `errorCode` in an envelope. -fn codes(env: &Value) -> Vec { - fn walk(v: &Value, out: &mut Vec) { - match v { - Value::Object(m) => { - for (k, v) in m { - if k == "code" || k == "errorCode" { - if let Some(s) = v.as_str() { - out.push(s.to_string()); - } - } - walk(v, out); - } - } - Value::Array(a) => a.iter().for_each(|v| walk(v, out)), - _ => {} - } - } - let mut out = Vec::new(); - walk(env, &mut out); - out -} - /// Whether `doc` attests gson `not_affected` under [`VULN`]. fn attested(doc: Option<&Value>) -> bool { doc.and_then(|d| d["statements"].as_array()) @@ -232,7 +210,7 @@ fn sbt_vendored_vex_attests_after_sbt_update() { let (code, env, doc) = fx.vex(); assert_eq!(code, Some(0), "{env}"); assert!(attested(doc.as_ref()), "{env}\n{doc:?}"); - let got = codes(&env); + let got = all_codes(&env); assert!( !SBT_DIAGS.iter().any(|d| got.iter().any(|c| c == d)), "{env}" @@ -244,7 +222,7 @@ fn sbt_vendored_vex_before_sbt_update_is_unverified() { let fx = Fx::vendored(); let (_, env, _) = fx.vex(); assert!( - codes(&env).contains(&"sbt_resolution_unverified".to_string()), + all_codes(&env).contains(&"sbt_resolution_unverified".to_string()), "{env}" ); } @@ -256,7 +234,7 @@ fn sbt_vendored_vex_flags_a_tampered_tree() { std::fs::write(tree_jar(&fx.proj), jar(b"EVIL\n")).unwrap(); let (_, env, doc) = fx.vex(); assert!( - codes(&env).contains(&"vendored_tree_missing".to_string()), + all_codes(&env).contains(&"vendored_tree_missing".to_string()), "{env}" ); assert!( @@ -274,7 +252,7 @@ fn sbt_vendored_vex_flags_an_edited_generated_file() { std::fs::write(&path, text.replace("// socket-patch ", "// mine ")).unwrap(); let (_, env, _) = fx.vex(); assert!( - codes(&env).contains(&"sbt_owned_file_modified".to_string()), + all_codes(&env).contains(&"sbt_owned_file_modified".to_string()), "{env}" ); } @@ -291,7 +269,7 @@ fn sbt_vendored_vex_without_a_ledger_attests_nothing() { std::fs::write(tree_jar(&fx.proj), jar(b"EVIL\n")).unwrap(); let (_, env, _) = fx.vex(); assert!( - codes(&env).contains(&"vendored_tree_missing".to_string()), + all_codes(&env).contains(&"vendored_tree_missing".to_string()), "{env}" ); } diff --git a/crates/socket-patch-cli/tests/gradle_agent_cli.rs b/crates/socket-patch-cli/tests/gradle_agent_cli.rs index 5d301dec5..db7b147f5 100644 --- a/crates/socket-patch-cli/tests/gradle_agent_cli.rs +++ b/crates/socket-patch-cli/tests/gradle_agent_cli.rs @@ -13,6 +13,7 @@ #[path = "common/mod.rs"] mod common; +use common::envelope::codes_in; use common::git_sha256; #[path = "common/hermetic.rs"] @@ -348,20 +349,10 @@ struct Out { impl Out { fn warning_codes(&self) -> Vec { - let mut codes: Vec = self.json["warnings"] - .as_array() + [&self.json["warnings"], &self.json["vex"]["warnings"]] .into_iter() - .flatten() - .filter_map(|w| w["code"].as_str().map(str::to_string)) - .collect(); - codes.extend( - self.json["vex"]["warnings"] - .as_array() - .into_iter() - .flatten() - .filter_map(|w| w["code"].as_str().map(str::to_string)), - ); - codes + .flat_map(codes_in) + .collect() } fn ok(&self) -> &Self { diff --git a/crates/socket-patch-cli/tests/mode_migration_bun.rs b/crates/socket-patch-cli/tests/mode_migration_bun.rs index e7ea8aa77..3dc388ce4 100644 --- a/crates/socket-patch-cli/tests/mode_migration_bun.rs +++ b/crates/socket-patch-cli/tests/mode_migration_bun.rs @@ -89,6 +89,7 @@ #[path = "common/mod.rs"] mod common; use common::binary; +use common::envelope::{codes_in, event_codes}; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; @@ -601,27 +602,6 @@ fn packages_line(lock: &str, name: &str) -> String { .to_string() } -fn warning_codes(v: &Value) -> Vec { - v.as_array() - .map(|w| { - w.iter() - .filter_map(|x| x["code"].as_str().map(str::to_string)) - .collect() - }) - .unwrap_or_default() -} - -fn event_codes(v: &Value) -> Vec { - v["events"] - .as_array() - .map(|evs| { - evs.iter() - .filter_map(|e| e["errorCode"].as_str().map(str::to_string)) - .collect() - }) - .unwrap_or_default() -} - // ── fixture ──────────────────────────────────────────────────────────────── /// The pristine and marker-patched `index.js` of one installed dep. @@ -1279,7 +1259,7 @@ fn take_over_to_hosted(fx: &Fixture, proj: &Path, api: &str, hp: &HostedPatch, t let env = envelope(&stdout, &stderr); assert_eq!(env["status"], "success", "{env:#}"); assert_eq!(env["redirect"]["redirected"], 1, "{env:#}"); - let codes = warning_codes(&env["redirect"]["warnings"]); + let codes = codes_in(&env["redirect"]["warnings"]); assert!( codes .iter() @@ -1749,7 +1729,7 @@ async fn bun_dry_run_previews_match_wet_outcomes() { ); let env = envelope(&stdout, &stderr); assert_eq!(env["redirect"]["dryRun"], true, "{env:#}"); - let codes = warning_codes(&env["redirect"]["warnings"]); + let codes = codes_in(&env["redirect"]["warnings"]); assert!( codes.iter().any(|c| c == "redirect_would_revert_vendored"), "the hosted preview must announce the vendored takeover: {codes:?}\n{env:#}" diff --git a/crates/socket-patch-cli/tests/mode_migration_vlt.rs b/crates/socket-patch-cli/tests/mode_migration_vlt.rs index 0d6c4b17f..ae010a2e4 100644 --- a/crates/socket-patch-cli/tests/mode_migration_vlt.rs +++ b/crates/socket-patch-cli/tests/mode_migration_vlt.rs @@ -12,6 +12,10 @@ use std::path::Path; use serde_json::Value; +#[path = "common/envelope.rs"] +mod envelope; +use envelope::all_codes; + #[path = "vlt_e2e_common/mod.rs"] mod vlt_e2e_common; @@ -29,30 +33,8 @@ fn migration_leg(name: &'static str) -> Option { Some(leg) } -/// Every object in `doc` carrying an `errorCode`, at any depth. -fn codes(doc: &Value) -> Vec { - fn walk(v: &Value, out: &mut Vec) { - match v { - Value::Object(m) => { - if let Some(c) = m.get("errorCode").and_then(Value::as_str) { - out.push(c.to_string()); - } - if let Some(c) = m.get("code").and_then(Value::as_str) { - out.push(c.to_string()); - } - m.values().for_each(|c| walk(c, out)); - } - Value::Array(a) => a.iter().for_each(|c| walk(c, out)), - _ => {} - } - } - let mut out = Vec::new(); - walk(doc, &mut out); - out -} - fn has_code(doc: &Value, code: &str) -> bool { - codes(doc).iter().any(|c| c == code) + all_codes(doc).iter().any(|c| c == code) } /// Every committable byte of `proj` (no installed trees). diff --git a/crates/socket-patch-cli/tests/vendor_eject.rs b/crates/socket-patch-cli/tests/vendor_eject.rs index ec7950d93..aafb5dfb7 100644 --- a/crates/socket-patch-cli/tests/vendor_eject.rs +++ b/crates/socket-patch-cli/tests/vendor_eject.rs @@ -13,8 +13,11 @@ //! hosted) all point at a wiremock; `SOCKET_VENDOR_SOURCE=build` keeps the //! vendoring service out of it. +#[path = "common/envelope.rs"] +mod envelope; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; +use envelope::find_event; use std::path::{Path, PathBuf}; use std::process::Command; @@ -201,17 +204,6 @@ async fn mock_view(p: &Project) { .await; } -fn find_event<'a>(env: &'a Value, action: &str, code: Option<&str>) -> &'a Value { - env["events"] - .as_array() - .and_then(|events| { - events - .iter() - .find(|e| e["action"] == action && code.is_none_or(|c| e["errorCode"] == c)) - }) - .unwrap_or_else(|| panic!("no `{action}` event (errorCode={code:?}) in:\n{env:#}")) -} - /// (1) + (2): eject vendors the hosted pin into `.socket/vendor/`, rewires /// the lock to the vendored artifact, writes no hosted ledger and no /// manifest; `vendor --revert` then returns the lock to the UPSTREAM diff --git a/crates/socket-patch-cli/tests/vendor_group_commit_e2e.rs b/crates/socket-patch-cli/tests/vendor_group_commit_e2e.rs index 83db60ce1..374e3a9b0 100644 --- a/crates/socket-patch-cli/tests/vendor_group_commit_e2e.rs +++ b/crates/socket-patch-cli/tests/vendor_group_commit_e2e.rs @@ -20,27 +20,14 @@ #[path = "vendor_ecosystem_fixtures/mod.rs"] mod fx; +#[path = "common/envelope.rs"] +mod envelope; + +use envelope::{event_triples, parse_json_envelope}; use fx::{masked_tree, Fixture}; const OFF: (&str, &str) = ("SOCKET_PATCH_SWITCH_OFF", "group_commit"); -fn events(stdout: &str) -> Vec<(String, String, String)> { - let v: serde_json::Value = serde_json::from_str(stdout.trim()) - .unwrap_or_else(|e| panic!("envelope JSON: {e}\n{stdout}")); - v["events"] - .as_array() - .expect("events") - .iter() - .map(|e| { - ( - e["purl"].as_str().unwrap_or_default().to_string(), - e["action"].as_str().unwrap_or_default().to_string(), - e["errorCode"].as_str().unwrap_or_default().to_string(), - ) - }) - .collect() -} - /// The paths whose bytes differ between two trees (or exist in one only). fn differing(a: &[(String, String)], b: &[(String, String)]) -> Vec { let mut out: Vec = a @@ -70,7 +57,7 @@ fn group_commit_ends_where_per_package_commits_end_for_every_ecosystem() { oracle_code, 0, "{eco} (oracle): {oracle_stdout}\n{oracle_stderr}" ); - let applied: Vec<_> = events(&stdout) + let applied: Vec<_> = event_triples(&parse_json_envelope(&stdout)) .into_iter() .filter(|(_, action, _)| action == "applied") .map(|(purl, ..)| purl) @@ -81,8 +68,8 @@ fn group_commit_ends_where_per_package_commits_end_for_every_ecosystem() { "{eco}: both packages vendor: {stdout}" ); assert_eq!( - events(&stdout), - events(&oracle_stdout), + event_triples(&parse_json_envelope(&stdout)), + event_triples(&parse_json_envelope(&oracle_stdout)), "{eco}: same events" ); assert_eq!( @@ -160,7 +147,7 @@ fn a_partial_failure_commits_the_packages_that_succeeded() { let (oracle_code, oracle_stdout, _) = oracle.vendor(&[], &[OFF]); assert_eq!(code, oracle_code, "{eco}: {stdout}\n{oracle_stdout}"); assert_ne!(code, 0, "{eco}: the failed package fails the run"); - let ev = events(&stdout); + let ev = event_triples(&parse_json_envelope(&stdout)); assert!( ev.iter() .any(|(p, a, _)| *p == grouped.patches[0].purl && a == "applied"), @@ -171,7 +158,11 @@ fn a_partial_failure_commits_the_packages_that_succeeded() { .any(|(p, a, _)| *p == grouped.patches[1].purl && a == "applied"), "{eco}: {stdout}" ); - assert_eq!(ev, events(&oracle_stdout), "{eco}: same events"); + assert_eq!( + ev, + event_triples(&parse_json_envelope(&oracle_stdout)), + "{eco}: same events" + ); assert_eq!( masked_tree(&grouped.root), masked_tree(&oracle.root), diff --git a/crates/socket-patch-cli/tests/vendor_partial_staging_e2e.rs b/crates/socket-patch-cli/tests/vendor_partial_staging_e2e.rs index f3bee2737..f2b8a3ab0 100644 --- a/crates/socket-patch-cli/tests/vendor_partial_staging_e2e.rs +++ b/crates/socket-patch-cli/tests/vendor_partial_staging_e2e.rs @@ -1,6 +1,9 @@ //! Vendoring consumes immutable service artifacts without downloading patch blobs. +#[path = "common/envelope.rs"] +mod envelope; #[path = "prebuilt_common/mod.rs"] mod prebuilt_common; +use envelope::events; use std::path::Path; use std::process::Command; @@ -186,10 +189,6 @@ fn vendor_cli_with_source(root: &Path, api_url: &str, source: &str) -> (i32, Val (out.status.code().unwrap_or(-1), env, stderr) } -fn events(env: &Value) -> &Vec { - env["events"].as_array().expect("events array") -} - fn event_for<'a>(env: &'a Value, purl: &str) -> &'a Value { events(env) .iter()