Skip to content
Merged
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
112 changes: 112 additions & 0 deletions crates/socket-patch-cli/tests/e2e_redirect_gem_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3029,6 +3029,118 @@ async fn gem_hosted_pin_previews_and_runs_a_clean_eject() {
}
}

/// #1808: a hosted gem whose `Gemfile.lock` (or whole pair) is CRLF, as a
/// Git for Windows `autocrlf` checkout leaves it, ejects to vendored mode
/// like an LF one. The `vendor` eject and the `scan --mode vendored`
/// takeover agree dry and wet: both exit 0, and the wet run wires the
/// vendored copy with the lock still CRLF on every line instead of failing
/// `apply_failed` ("Gemfile.lock has no GEM section") and rolling back.
/// A CRLF Gemfile keeps CRLF on every line too (#1821).
#[tokio::test(flavor = "multi_thread")]
#[ignore = "host capstone: shells out to a real ruby/gem/bundler (>= 1.17); \
the unpinned `test` job skips it, an e2e job with a pinned toolchain runs it via --ignored"]
async fn gem_hosted_pin_with_a_crlf_lock_ejects_cleanly() {
for (command, crlf_files) in [
("vendor", &["Gemfile.lock"][..]),
("vendor", &["Gemfile", "Gemfile.lock"][..]),
("scan", &["Gemfile", "Gemfile.lock"][..]),
] {
let Some(fx) = redirect_scanned_project(
"hosted crlf eject",
Spelling::Gemfile,
false,
true,
None,
Driver::ScanVex,
)
.await
else {
return;
};
let install = bundle(&fx.proj, &["install"]);
assert!(
install.status.success(),
"converging install:\n{}",
String::from_utf8_lossy(&install.stderr)
);
for file in crlf_files {
let path = fx.proj.join(file);
let text = std::fs::read_to_string(&path).unwrap();
std::fs::write(&path, text.replace('\n', "\r\n")).unwrap();
}
mount_vendored_grant(&fx).await;
let proj = fx.proj.to_str().expect("utf8 tmp path");
let api = fx._server.uri();
let upstream = format!("{api}/upstream");
for dry_run in [true, false] {
let before: Vec<Vec<u8>> = ["Gemfile", "Gemfile.lock"]
.iter()
.map(|f| std::fs::read(fx.proj.join(f)).unwrap())
.collect();
let mut argv = vec![command];
if command == "scan" {
argv.extend(["--mode", "vendored"]);
}
argv.extend([
"--json",
"--yes",
"--cwd",
proj,
"--api-url",
&api,
"--org",
ORG,
"--api-token",
"fake",
"--patch-server-url",
&api,
]);
if dry_run {
argv.push("--dry-run");
}
let label = format!("{command} eject, CRLF {crlf_files:?} (dry_run={dry_run})");
let (code, stdout, stderr) =
run_socket_env(&fx.proj, &argv, &[("SOCKET_RUBYGEMS_URL", &upstream)]);
assert_eq!(code, 0, "{label}:\nstdout:\n{stdout}\nstderr:\n{stderr}");
for bad in ["apply_failed", "eject_rolled_back", "no GEM section"] {
assert!(!stdout.contains(bad), "{label}: {bad}:\n{stdout}");
}
if dry_run {
for (file, before) in ["Gemfile", "Gemfile.lock"].iter().zip(before) {
assert_eq!(
std::fs::read(fx.proj.join(file)).unwrap(),
before,
"{label}: a dry run writes nothing to {file}"
);
}
} else {
let gemfile = std::fs::read_to_string(fx.proj.join("Gemfile")).unwrap();
assert!(
gemfile.contains("path: \".socket/vendor/gem/"),
"{label}: the Gemfile now points at the vendored copy:\n{gemfile}"
);
if crlf_files.contains(&"Gemfile") {
assert_eq!(
gemfile.matches('\n').count(),
gemfile.matches("\r\n").count(),
"{label}: every line of the Gemfile keeps CRLF:\n{gemfile:?}"
);
}
let lock = std::fs::read_to_string(fx.proj.join("Gemfile.lock")).unwrap();
assert!(
lock.contains("PATH\r\n remote: .socket/vendor/gem/"),
"{label}: the lock wires the vendored copy:\n{lock:?}"
);
assert_eq!(
lock.matches('\n').count(),
lock.matches("\r\n").count(),
"{label}: every line of the lock keeps CRLF:\n{lock:?}"
);
}
}
}
}

/// One refused `vendor` eject of the group-block fixture (#1761): dry or
/// wet, the eject is refused whole with `gemfile_declaration_not_editable`
/// and `eject_refused` (exit 1), and the hosted `Gemfile` /
Expand Down
111 changes: 111 additions & 0 deletions crates/socket-patch-cli/tests/e2e_vendor_gem_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2187,3 +2187,114 @@ fn gem_vendor_gemspec_dev_dependency_refuses_or_installs_frozen() {
"revert restores Gemfile.lock byte-identical"
);
}

/// #1808: a CRLF `Gemfile.lock` (a Git for Windows `autocrlf` checkout, or
/// `eol=crlf`; bundler keeps CRLF on purpose) vendors like an LF one. The
/// dry run and the wet run both succeed, the wired lock keeps CRLF on every
/// line, a frozen fresh-checkout install accepts it byte-stably, a re-run
/// is byte-stable, and `--revert` hands back the CRLF lock byte for byte.
#[test]
#[ignore = "host capstone: shells out to a real bundler >= 1.17; the unpinned `test` job \
skips it, the e2e job runs it with a pinned toolchain via --ignored"]
fn gem_vendor_crlf_lock_fresh_checkout_and_revert() {
let Some((tmp, proj, _bundler, _purl)) = staged_rack_project("crlf lock") else {
return;
};
let lock_path = proj.join("Gemfile.lock");
let gemfile_path = proj.join("Gemfile");
let lf = std::fs::read_to_string(&lock_path).unwrap();
let crlf = lf.replace('\n', "\r\n");
std::fs::write(&lock_path, &crlf).unwrap();
let gemfile_before = std::fs::read(&gemfile_path).unwrap();
// Some bundlers (4.0.21) refuse a frozen install of any CRLF lock, even
// one socket-patch never touched; the frozen leg below then has nothing
// to compare against.
let pristine = tmp.path().join("pristine");
std::fs::create_dir_all(&pristine).unwrap();
std::fs::copy(&gemfile_path, pristine.join("Gemfile")).unwrap();
std::fs::copy(&lock_path, pristine.join("Gemfile.lock")).unwrap();
copy_dir_recursive(&proj.join(".bundle"), &pristine.join(".bundle"));
let frozen_crlf_ok = bundle(&pristine, &["install"], true).status.success();
let cwd = proj.to_str().unwrap();
let vendor = |extra: &[&str]| {
let mut args = vec!["vendor", "--json", "--offline", "--cwd", cwd];
args.extend_from_slice(extra);
run_socket(&proj, &args)
};

let (code, stdout, stderr) = vendor(&["--dry-run"]);
assert_eq!(code, 0, "dry run:\nstdout:\n{stdout}\nstderr:\n{stderr}");
assert!(!stdout.contains("apply_failed"), "dry run:\n{stdout}");
assert_eq!(
std::fs::read_to_string(&lock_path).unwrap(),
crlf,
"a dry run writes nothing"
);

let (code, stdout, stderr) = vendor(&[]);
assert_eq!(code, 0, "vendor:\nstdout:\n{stdout}\nstderr:\n{stderr}");
let env = parse_json_envelope(&stdout);
assert_eq!(env["summary"]["applied"], 1, "{env}");
let wired = std::fs::read_to_string(&lock_path).unwrap();
assert_eq!(
wired.matches('\n').count(),
wired.matches("\r\n").count(),
"every line of the wired lock keeps CRLF:\n{wired:?}"
);
let copy_rel = format!(".socket/vendor/gem/{UUID}/");
assert!(
wired.starts_with(&format!("PATH\r\n remote: {copy_rel}")),
"the PATH section is spliced in CRLF:\n{wired:?}"
);
assert!(
wired.contains(&format!("\r\n {DEP} (= ")),
"the DEPENDENCIES pin is spliced in CRLF:\n{wired:?}"
);

// Bundler reads the CRLF pair as wired and keeps it byte-identical.
let fresh = tmp.path().join("fresh");
if !frozen_crlf_ok {
println!(
"SKIP frozen leg of e2e_vendor_gem_build (crlf lock): this bundler refuses a \
frozen install of the pristine CRLF lock too"
);
}
std::fs::create_dir_all(&fresh).unwrap();
std::fs::copy(&gemfile_path, fresh.join("Gemfile")).unwrap();
std::fs::copy(&lock_path, fresh.join("Gemfile.lock")).unwrap();
copy_dir_recursive(&proj.join(".socket"), &fresh.join(".socket"));
copy_dir_recursive(&proj.join(".bundle"), &fresh.join(".bundle"));
let ci = bundle(&fresh, &["install"], frozen_crlf_ok);
assert!(
ci.status.success(),
"install of the CRLF pair (frozen: {frozen_crlf_ok}):\nstdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&ci.stdout),
String::from_utf8_lossy(&ci.stderr),
);
assert_eq!(
std::fs::read_to_string(fresh.join("Gemfile.lock")).unwrap(),
wired,
"bundler accepts the wired CRLF lock as is"
);

let (code, stdout, stderr) = vendor(&[]);
assert_eq!(code, 0, "re-run:\nstdout:\n{stdout}\nstderr:\n{stderr}");
assert_eq!(
std::fs::read_to_string(&lock_path).unwrap(),
wired,
"a re-run takes the in-sync path and leaves the lock alone"
);

let (code, stdout, stderr) = run_socket(&proj, &["vendor", "--revert", "--json", "--cwd", cwd]);
assert_eq!(code, 0, "revert:\nstdout:\n{stdout}\nstderr:\n{stderr}");
assert!(
!stdout.contains("drifted"),
"a clean revert reports no drift:\n{stdout}"
);
assert_eq!(std::fs::read(&gemfile_path).unwrap(), gemfile_before);
assert_eq!(
std::fs::read_to_string(&lock_path).unwrap(),
crlf,
"revert restores the CRLF lock byte for byte"
);
}
Loading
Loading