Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions crates/socket-patch-cli/CLI_CONTRACT.md

Large diffs are not rendered by default.

190 changes: 188 additions & 2 deletions crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,17 @@ enum Driver {
/// environment, so bundler still loads `Gemfile.next` and the run must
/// still redirect and attest nothing.
ScanVexDualBootEnvGemfile,
/// [`Driver::ScanVex`] on a bundler 4 project whose `.bundle/config`
/// sets `lockfile custom.lock` beside a leftover `Gemfile.lock` (#749):
/// bundler reads `custom.lock`, which the rewriter never pins, so the
/// run must redirect nothing and attest nothing. Bundler >= 4 only; the
/// fixture asserts the contract itself and yields `None`.
ScanVexCustomLockfile,
/// [`Driver::ScanVex`] on a `Gemfile` + `gems.rb` twin locked by
/// bundler 1.x (#751): bundler 1.x loads the `Gemfile`, so the run must
/// wire the `Gemfile` pair and leave `gems.rb` / `gems.locked`
/// untouched. Bundler < 2 only.
ScanVexBundler1Twin,
/// [`Driver::ScanVex`] on a project whose committed `.bundle/config`
/// sets `mirror.all` (#681): bundler fetches the patch-registry source
/// from the mirror, which serves the upstream gem. The run must refuse,
Expand Down Expand Up @@ -502,6 +513,10 @@ impl Driver {
Driver::ScanVexDualBootEnvGemfile => {
"scan --mode hosted (config Gemfile.next, env BUNDLE_GEMFILE=Gemfile)"
}
Driver::ScanVexCustomLockfile => "scan --mode hosted (lockfile custom.lock)",
Driver::ScanVexBundler1Twin => {
"scan --mode hosted (bundler 1.x Gemfile + gems.rb twin)"
}
Driver::ScanVexSemicolonJoinedDeclaration => {
"scan --mode hosted (two `;`-joined gem declarations)"
}
Expand Down Expand Up @@ -615,6 +630,23 @@ async fn redirect_scanned_project(
let bundler = bundler_e2e::gate("e2e_redirect_gem_build", tag, floor, &|c| {
cache_env::isolate(c);
})?;
// Drivers that only mean something on one bundler line.
let only = match driver {
Driver::ScanVexCustomLockfile if !bundler.at_least(4, 0) => {
Some("custom lockfiles need bundler >= 4")
}
Driver::ScanVexBundler1Twin if bundler.at_least(2, 0) => {
Some("bundler >= 2 loads a twin's gems.rb (covered in-process)")
}
_ => None,
};
if let Some(why) = only {
println!(
"SKIP e2e_redirect_gem_build ({tag}): bundler {}: {why}",
bundler.version
);
return None;
}

let tmp = tempfile::tempdir().unwrap();
let (gemfile_name, lock_name) = spelling.pair();
Expand Down Expand Up @@ -964,6 +996,26 @@ async fn redirect_scanned_project(
String::from_utf8_lossy(&cfg.stderr)
);
}
let custom_lockfile = driver == Driver::ScanVexCustomLockfile;
if custom_lockfile {
// `bundle config set --local lockfile custom.lock`; the default
// lock stays behind as a leftover bundler 4 ignores.
std::fs::copy(proj.join(lock_name), proj.join("custom.lock")).unwrap();
let args = bundler.config_local_args("lockfile", "custom.lock");
let args: Vec<&str> = args.iter().map(String::as_str).collect();
let cfg = bundle(&proj, &args);
assert!(
cfg.status.success(),
"bundle config set --local lockfile failed:\n{}",
String::from_utf8_lossy(&cfg.stderr)
);
}
if driver == Driver::ScanVexBundler1Twin {
// Identical twins, both `BUNDLED WITH 1.x`: bundler 1.x loads the
// Gemfile pair.
std::fs::copy(proj.join(gemfile_name), proj.join("gems.rb")).unwrap();
std::fs::copy(proj.join(lock_name), proj.join("gems.locked")).unwrap();
}
// Synthetic credentials must never appear in the scan's automatic
// diagnostics. The loopback mirror itself serves the unpatched gem.
let mirror = format!("{}/upstream/", server.uri()).replacen(
Expand Down Expand Up @@ -996,6 +1048,8 @@ async fn redirect_scanned_project(
| Driver::ScanVexMirrorSource
| Driver::ScanVexMirrorSourceEnv
| Driver::ScanVexMirrorAllEnv
| Driver::ScanVexCustomLockfile
| Driver::ScanVexBundler1Twin
| Driver::ScanVexDualBoot
| Driver::ScanVexDualBootEnvGemfile
| Driver::ScanVexDuplicateDeclaration
Expand Down Expand Up @@ -1070,6 +1124,16 @@ async fn redirect_scanned_project(
assert_dual_boot_redirects_nothing(&env, &proj, &pristine_gemfile, &pristine_lock);
return None;
}
if custom_lockfile {
assert_custom_lockfile_redirects_nothing(
&bundler,
(code, &stdout, &stderr),
&proj,
&pristine_gemfile,
&pristine_lock,
);
return None;
}
if let Some(warning) = match driver {
Driver::ScanVexDuplicateDeclaration => Some("redirect_gem_declared_more_than_once"),
Driver::ScanVexEvalGemfile => Some("redirect_gem_declaration_not_visible"),
Expand Down Expand Up @@ -1171,7 +1235,9 @@ async fn redirect_scanned_project(
);
}
match driver {
Driver::ScanVex | Driver::ScanVexTrailingSemicolonDeclaration => {
Driver::ScanVex
| Driver::ScanVexBundler1Twin
| Driver::ScanVexTrailingSemicolonDeclaration => {
assert_eq!(env["vex"]["statements"], 1, "vex block: {env}");
assert_eq!(
env["vex"]["verified"], false,
Expand All @@ -1180,6 +1246,7 @@ async fn redirect_scanned_project(
}
Driver::ScanVexDualBoot
| Driver::ScanVexDualBootEnvGemfile
| Driver::ScanVexCustomLockfile
| Driver::ScanVexDuplicateDeclaration
| Driver::ScanVexEvalGemfile
| Driver::ScanVexMirrorAll
Expand Down Expand Up @@ -1292,6 +1359,60 @@ fn assert_unwirable_declaration_redirects_nothing(
);
}

/// #749's contract on a bundler 4 project whose `.bundle/config` names
/// `custom.lock`: the hosted scan names the setting, leaves the Gemfile,
/// the leftover `Gemfile.lock` (which bundler ignores) and `custom.lock`
/// byte-identical, and attests nothing. Bundler still installs the
/// untouched project frozen (before the fix every frozen install failed).
fn assert_custom_lockfile_redirects_nothing(
bundler: &bundler_e2e::Bundler,
(code, stdout, stderr): (i32, &str, &str),
proj: &Path,
pristine_gemfile: &[u8],
pristine_lock: &[u8],
) {
let env: serde_json::Value = serde_json::from_str(stdout)
.unwrap_or_else(|e| panic!("not JSON: {e}\nstdout:\n{stdout}\nstderr:\n{stderr}"));
let warning_codes: Vec<&str> = env["redirect"]["warnings"]
.as_array()
.map(|a| a.iter().filter_map(|w| w["code"].as_str()).collect())
.unwrap_or_default();
assert!(
warning_codes.contains(&"redirect_gem_bundle_lockfile_unsupported"),
"the BUNDLE_LOCKFILE refusal must be reported: {env}"
);
assert_ne!(code, 0, "nothing was patched or attested: {env}");
assert_eq!(
env["redirect"]["redirected"], 0,
"nothing redirected: {env}"
);
assert!(
env["vex"]["statements"].as_u64().unwrap_or(0) == 0,
"no in-run attestation for a lock that was never pinned: {env}"
);
for (file, want) in [
("Gemfile", pristine_gemfile),
("Gemfile.lock", pristine_lock),
("custom.lock", pristine_lock),
] {
assert_eq!(
std::fs::read(proj.join(file)).unwrap(),
want,
"{file} must be byte-untouched"
);
}
let args = bundler.config_local_args("frozen", "true");
let args: Vec<&str> = args.iter().map(String::as_str).collect();
assert!(bundle(proj, &args).status.success());
let install = bundle(proj, &["install"]);
assert!(
install.status.success(),
"bundler {} must still install the untouched project frozen:\n{}",
bundler.version,
String::from_utf8_lossy(&install.stderr)
);
}

/// #390's contract on a `BUNDLE_GEMFILE: Gemfile.next` project: the hosted
/// scan names the setting, rewrites neither the `Gemfile` pair (which
/// bundler ignores) nor `Gemfile.next`, and its in-run VEX attests nothing.
Expand Down Expand Up @@ -1905,6 +2026,68 @@ async fn gem_hosted_bundle_gemfile_dual_boot_redirects_nothing() {
assert!(fx.is_none(), "the dual-boot driver asserts in place");
}

/// #749: bundler 4's `bundle config set --local lockfile custom.lock`
/// makes bundler read `custom.lock`. The hosted scan used to wire the
/// Gemfile (and the ignored leftover `Gemfile.lock`), report success and
/// attest, while every frozen install then failed; it must redirect and
/// attest nothing.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "host capstone: shells out to a real ruby/gem/bundler (>= 4.0 for this arm); \
the unpinned `test` job skips it, an e2e job with a pinned toolchain runs it via --ignored"]
async fn gem_hosted_bundler4_custom_lockfile_redirects_nothing() {
let fx = redirect_scanned_project(
"custom-lockfile",
Spelling::Gemfile,
true,
true,
None,
Driver::ScanVexCustomLockfile,
)
.await;
assert!(fx.is_none(), "the custom-lockfile driver asserts in place");
}

/// #751: bundler 1.x loads a twin's `Gemfile`, not its `gems.rb`. The
/// hosted scan used to wire `gems.rb` and attest while bundler 1.17
/// installed the unpatched gem from the `Gemfile`; it must wire the
/// `Gemfile` pair, and a fresh checkout of the twin must install the
/// patched bytes.
#[tokio::test(flavor = "multi_thread")]
#[ignore = "host capstone: shells out to a real ruby/gem/bundler (< 2.0 for this arm); \
the unpinned `test` job skips it, an e2e job with a pinned toolchain runs it via --ignored"]
async fn gem_hosted_bundler1_twin_wires_the_gemfile_and_installs() {
let Some(fx) = redirect_scanned_project(
"bundler1-twin",
Spelling::Gemfile,
false,
true,
None,
Driver::ScanVexBundler1Twin,
)
.await
else {
return;
};
let gems_rb = std::fs::read(fx.proj.join("gems.rb")).unwrap();
let gems_locked = std::fs::read(fx.proj.join("gems.locked")).unwrap();
assert_eq!(gems_rb, fx.pristine_gemfile, "gems.rb must be untouched");
assert_eq!(
gems_locked, fx.pristine_lock,
"gems.locked must be untouched"
);
let fresh = stage_fresh_checkout(&fx, "fresh");
std::fs::write(fresh.join("gems.rb"), &gems_rb).unwrap();
std::fs::write(fresh.join("gems.locked"), &gems_locked).unwrap();
let install = bundle(&fresh, &["install"]);
assert!(
install.status.success(),
"fresh-checkout `bundle install` of the twin must succeed.\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&install.stdout),
String::from_utf8_lossy(&install.stderr),
);
assert_patched_install(&fx, &fresh);
}

/// #548: a gem declared in two `group` blocks must not be half-rewritten
/// (bundler refuses `= 1.0.0` next to `>= 0` on every install).
#[tokio::test(flavor = "multi_thread")]
Expand Down Expand Up @@ -1960,7 +2143,10 @@ async fn gem_hosted_custom_git_source_is_refused_and_still_installs() {
Driver::ScanVexCustomGitSource,
)
.await;
assert!(fx.is_none(), "the custom git_source driver asserts in place");
assert!(
fx.is_none(),
"the custom git_source driver asserts in place"
);
}

/// #340: a `gem` declaration that continues on the next line must not be
Expand Down
Loading