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
129 changes: 129 additions & 0 deletions crates/socket-patch-cli/tests/e2e_vendor_pypi_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1300,6 +1300,135 @@ fn uv_vendor_revert_after_dev_group_declaration_edit() {
);
}

/// #869, the PEP 723 script-lock twin of #840: `uv add --script s.py
/// "six>=1.15"` while six is vendored rewrites the script but leaves
/// `s.py.lock` byte-identical (a path source records no specifier). The
/// revert must write the script's NEW specifier into the lock's
/// `[manifest] requirements`, not the recorded `==1.16.0` that leaves
/// `uv run --locked --script` red while the revert reports success.
#[test]
#[serial_test::serial]
fn uv_vendor_script_revert_after_declaration_edit() {
const TAG: &str = "uv-script-declaration-edit";
let Some((uv, python)) = capstone_uv(TAG) else {
return;
};
if !uv_vex::uv_under_test().is_ok_and(|uv| uv.help_has(&["lock"], "--script")) {
println!("UV-VEX lane=script({TAG}) step=all result=n/a (no `uv lock --script`)");
return;
}
bake_leak_guards();
let tmp = tempfile::tempdir().unwrap();
let proj = tmp.path().join("proj");
std::fs::create_dir_all(&proj).unwrap();
let cache = tmp.path().join("uv-cache");
let venv = proj.join(".venv");
let mut cache_env: Vec<(&str, &str)> = vec![("UV_CACHE_DIR", cache.to_str().unwrap())];
if let Some(py) = python.as_deref() {
cache_env.push(("UV_PYTHON", py));
}
let mut venv_env = cache_env.clone();
venv_env.push(("VIRTUAL_ENV", venv.to_str().unwrap()));
std::fs::write(
proj.join("s.py"),
"# /// script\n# requires-python = \">=3.9\"\n# dependencies = [\n# \"six==1.16.0\",\n# \"attrs>=20\",\n# ]\n# ///\nimport six\n",
)
.unwrap();
// uv runs a script in a cache env the crawlers cannot see; the
// developer's in-project venv holds the copy the writers read.
for (step, env) in [
(&["lock", "-q", "--script", "s.py"][..], &cache_env),
(&["venv", "-q", ".venv"][..], &cache_env),
(&["pip", "install", "-q", "six==1.16.0"][..], &venv_env),
] {
let out = tool(&uv, &proj, step, env);
if !out.status.success() {
println!(
"SKIP e2e_vendor_pypi_build({TAG}): `uv {}` failed (PyPI unreachable?):\n{}",
step.join(" "),
String::from_utf8_lossy(&out.stderr)
);
return;
}
}
stage_patch(&proj, &site_packages(&venv).join("six.py"));

let (code, stdout, stderr) = run_vendored(&VendorDriver::VendorOffline, &proj);
assert_eq!(
code, 0,
"vendor failed.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
assert_vendored_applied(&parse_envelope(&stdout));
let check = &["lock", "--script", "s.py", "--check"][..];
assert_tool_ok(
&tool(&uv, &proj, check, &cache_env),
"`uv lock --script --check` on the wired pair",
);
let wired = std::fs::read_to_string(proj.join("s.py.lock")).unwrap();

let out = tool(
&uv,
&proj,
&["add", "-q", "--script", "s.py", "six>=1.15"],
&cache_env,
);
if !out.status.success() {
println!(
"SKIP e2e_vendor_pypi_build({TAG}): `uv add --script` failed (PyPI unreachable?):\n{}",
String::from_utf8_lossy(&out.stderr)
);
return;
}
let script = std::fs::read_to_string(proj.join("s.py")).unwrap();
assert!(script.contains("six>=1.15"), "{script}");
assert_eq!(
std::fs::read_to_string(proj.join("s.py.lock")).unwrap(),
wired,
"uv leaves the vendored script lock byte-identical"
);

let (code, stdout, stderr) = run_socket(
&proj,
&[
"vendor",
"--revert",
"--json",
"--cwd",
proj.to_str().unwrap(),
],
);
assert_eq!(
code, 0,
"revert failed.\nstdout:\n{stdout}\nstderr:\n{stderr}"
);
let renv = parse_envelope(&stdout);
assert_eq!(renv["status"], "success", "revert envelope: {renv}");
assert!(
!stdout.contains("vendor_lock_entry_drifted"),
"a single-clause declaration edit is not drift: {renv}"
);
for file in ["s.py", "s.py.lock"] {
let text = std::fs::read_to_string(proj.join(file)).unwrap();
assert!(
!text.contains(".socket/vendor"),
"{file} must be fully unwired:\n{text}"
);
}
let lock = std::fs::read_to_string(proj.join("s.py.lock")).unwrap();
assert!(
lock.contains("{ name = \"six\", specifier = \">=1.15\" }"),
"the restored lock carries the script's specifier:\n{lock}"
);
assert_tool_ok(
&tool(&uv, &proj, check, &cache_env),
"`uv lock --script --check` after the revert",
);
assert!(
!proj.join(".socket/vendor").exists(),
".socket/vendor must be fully removed after revert"
);
}

/// `get <uuid> --mode vendored` twin of the uv capstone above (v3.6): the
/// SAME vendor engine and wiring, driven through get's uuid path — exempt
/// from installed narrowing, so only the mocked `view/{uuid}` route is
Expand Down
11 changes: 7 additions & 4 deletions crates/socket-patch-core/src/patch/redirect/upstream/uv.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1272,7 +1272,9 @@ pub(crate) enum LockRequirementArray<'a> {
}

/// The `specifier` a vendored revert should restore for `name`'s entry
/// in `array`, given the one it recorded when vendoring (`None` when the
/// in `array` of the lock paired with `pyproject_text` (the metadata
/// document `rel` names: pyproject.toml, or a script's PEP 723 block),
/// given the one it recorded when vendoring (`None` when the
/// entry had none) and the entry's `marker`.
///
/// A path source records no specifier, so a user who changes the
Expand All @@ -1292,6 +1294,7 @@ pub(crate) enum LockRequirementArray<'a> {
/// mirrors is ambiguous, so restoring any spelling may break `--locked`.
pub(crate) fn respell_lock_specifier(
pyproject_text: &str,
rel: &str,
array: LockRequirementArray<'_>,
name: &str,
recorded: Option<&str>,
Expand All @@ -1301,7 +1304,7 @@ pub(crate) fn respell_lock_specifier(
return Ok(None);
};
let meta = Metadata {
rel: "pyproject.toml".to_string(),
rel: rel.to_string(),
text: String::new(),
script: false,
doc,
Expand Down Expand Up @@ -1354,8 +1357,8 @@ pub(crate) fn respell_lock_specifier(
// longer shows this entry's spelling, so any guess may break
// `--locked`.
_ => Err(format!(
"pyproject.toml now declares {name} with a multi-clause specifier, whose \
spelling in uv.lock is not derivable"
"{rel} now declares {name} with a multi-clause specifier, whose spelling in \
the lock is not derivable"
)),
}
}
Expand Down
Loading
Loading