Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
46 changes: 46 additions & 0 deletions crates/socket-patch-cli/tests/e2e_vex_build/hatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,52 @@ fn flow(flavor: Flavor, mode: Mode) {
"pass",
);

// ── #385: everyday pyproject edits must not block the revert ──────
// A release bump and a new sibling dependency after vendoring: the
// revert restores six's pin and drops the permission, keeping both.
if mode == Mode::Vendored && flavor == Flavor::Project {
let edited = tmp.path().join("edited");
copy_tree(&project, &edited, &[]);
let edit = |text: &str| {
text.replacen("version = \"0.1.0\"", "version = \"0.2.0\"", 1)
.replacen("dependencies = [", "dependencies = [\"idna==3.7\", ", 1)
};
std::fs::write(edited.join("pyproject.toml"), edit(&wired)).unwrap();
let out = std::process::Command::new(vex_e2e_common::binary())
.args(["vendor", "--revert", "--json", "--cwd"])
.arg(&edited)
.current_dir(&edited)
.output()
.unwrap();
assert_eq!(
out.status.code(),
Some(0),
"{what}: revert after project edits: {}",
out_text(&out)
);
let native = &flavor.native()[0].1;
assert_eq!(
std::fs::read_to_string(edited.join("pyproject.toml")).unwrap(),
edit(native),
"{what}: revert after project edits: {}",
out_text(&out)
);
assert!(
!edited
.join(".socket/vendor/pypi")
.join(mode.uuid())
.exists(),
"{what}: the vendored artifact was kept"
);
record(
"hatch",
&version,
&format!("{cell}/{}", mode.label()),
"revert-after-project-edits",
"pass",
);
}

// ── fresh checkout, real `hatch env create` ───────────────────────
let fresh = tmp.path().join("fresh");
copy_tree(&project, &fresh, &[".socket/manifest.json"]);
Expand Down
84 changes: 84 additions & 0 deletions crates/socket-patch-cli/tests/vex_e2e_common/uv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1560,6 +1560,11 @@ pub fn run_lane(suite: &str, uv: &Uv, mode: Mode, lane: Lane) {
&|step, result| report.row(step, result),
);

// ── 5b. a sibling dependency added after vendoring (#474) ─────────
if mode == Mode::Vendored && lane == Lane::Script {
script_sibling_revert(uv, &report, &proj, tmp.path());
}

// ── 6. the real revert ────────────────────────────────────────────
// Vendored: `vendor --revert` restores every wiring file byte for
// byte. Hosted (v5): `rollback` rewrites each pin back to the DEFAULT
Expand Down Expand Up @@ -1682,6 +1687,85 @@ pub fn run_lane(suite: &str, uv: &Uv, mode: Mode, lane: Lane) {
report.row("revert", "byte-identical");
}

/// #474: `uv add --script` after vendoring adds an unrelated requirement
/// to the script and its lock (a new `[manifest] requirements` element and
/// `[[package]]`). `vendor --revert` must still restore six's registry
/// wiring and keep the user's addition, leaving a lock uv accepts as is.
/// Runs on a copy so the byte-identical revert below is unaffected.
fn script_sibling_revert(uv: &Uv, report: &Report<'_>, proj: &Path, tmp: &Path) {
let dir = tmp.join("sibling");
std::fs::create_dir_all(&dir).unwrap();
for f in [SCRIPT, "tool.py.lock"] {
std::fs::copy(proj.join(f), dir.join(f)).unwrap();
}
copy_tree(&proj.join(".socket"), &dir.join(".socket"));
let cache = tmp.join("sibling-cache");
let out = uv.run_py(&dir, &["add", "--script", SCRIPT, "idna==3.7"], &cache);
if !ok(&out) {
report.row("sibling-revert", "n/a (`uv add --script` failed)");
println!("{}", dump(&out));
return;
}
let lock = std::fs::read_to_string(dir.join("tool.py.lock")).unwrap();
assert!(
lock.contains("name = \"idna\"") && lock.contains(".socket/vendor"),
"{}: uv add did not keep the vendored lock:\n{lock}",
report.what("sibling-revert")
);
let out = socket_patch(
&dir,
&[
"vendor",
"--revert",
"--json",
"--cwd",
dir.to_str().unwrap(),
],
);
let text = format!("{}\n{}", String::from_utf8_lossy(&out.stdout), dump(&out));
assert_eq!(
out.status.code(),
Some(0),
"{}:\n{text}",
report.what("sibling-revert")
);
assert!(
!text.contains("vendor_lock_entry_drifted"),
"{}: an added sibling counted as drift:\n{text}",
report.what("sibling-revert")
);
for f in [SCRIPT, "tool.py.lock"] {
let body = std::fs::read_to_string(dir.join(f)).unwrap();
assert!(
!body.contains(".socket/vendor") && body.contains("idna"),
"{}: {f} still vendored or lost idna:\n{body}",
report.what("sibling-revert")
);
}
assert!(
!dir.join(".socket/vendor/pypi")
.join(Mode::Vendored.uuid())
.exists(),
"{}: the vendored artifact was kept",
report.what("sibling-revert")
);
let reverted = std::fs::read(dir.join("tool.py.lock")).unwrap();
let out = uv.run_py(&dir, &["lock", "--script", SCRIPT], &cache);
assert!(
ok(&out),
"{}: uv lock --script:\n{}",
report.what("sibling-revert"),
dump(&out)
);
assert_eq!(
String::from_utf8_lossy(&std::fs::read(dir.join("tool.py.lock")).unwrap()),
String::from_utf8_lossy(&reverted),
"{}: uv rewrote the reverted lock",
report.what("sibling-revert")
);
report.row("sibling-revert", "restored, user addition kept");
}

// ── production legs ────────────────────────────────────────────────────

/// The uv program a production leg drives: `SOCKET_PATCH_UV_E2E_BIN`, else
Expand Down
50 changes: 50 additions & 0 deletions crates/socket-patch-core/src/vendor/pypi_hatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -464,4 +464,54 @@ mod tests {
ORIGINAL
);
}

/// #385: ordinary pyproject edits after vendoring (a release bump, a
/// comment on `name`, a new sibling dependency) must not block rollback.
#[tokio::test]
async fn revert_keeps_unrelated_project_edits() {
let original =
"[project]\nname = \"app\"\nversion = \"0.1.0\"\ndependencies = [\"six==1.16.0\"]\n";
let edits: [(&str, &str); 4] = [
("version = \"0.1.0\"", "version = \"0.2.0\""),
("name = \"app\"", "name = \"app\" # renamed soon"),
("dependencies = [", "dependencies = [\"idna==3.7\", "),
(
"version = \"0.1.0\"\n",
"version = \"0.3.0\"\ndescription = \"x\"\n",
),
];
for (from, to) in edits {
let temp = tempfile::tempdir().unwrap();
let root = temp.path();
tokio::fs::write(root.join("pyproject.toml"), original)
.await
.unwrap();
let project = load(root, "six", "1.16.0", UUID).await.unwrap();
let wheel = format!(".socket/vendor/pypi/{UUID}/six-1.16.0-py2.py3-none-any.whl");
let wiring = wire(&project, root, "six", "1.16.0", &wheel, &"0".repeat(64))
.await
.unwrap();
let entry = entry(UUID, "six", &wheel, &"0".repeat(64), wiring);
let mut state = VendorState::default();
state.entries.insert("six".into(), entry.clone());
save_state(root, &state).await.unwrap();
let patched = tokio::fs::read_to_string(root.join("pyproject.toml"))
.await
.unwrap();
assert!(patched.contains(&wheel));
assert!(patched.contains(from), "{patched}");
tokio::fs::write(root.join("pyproject.toml"), patched.replacen(from, to, 1))
.await
.unwrap();
let outcome = revert(&entry, root, false).await;
assert!(outcome.success, "{from} -> {to}: {:?}", outcome.error);
assert_eq!(
tokio::fs::read_to_string(root.join("pyproject.toml"))
.await
.unwrap(),
original.replacen(from, to, 1),
"{from} -> {to}"
);
}
}
}
Loading
Loading