diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 5fac438b3..7987d3011 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -6,7 +6,7 @@ use std::env; use std::ffi::OsString; use std::iter::Iterator; use std::path::{Path, PathBuf}; -use std::process::{Command, Output}; +use std::process::{Command, ExitStatus, Output}; use std::str::FromStr; use assert_cmd::assert::{Assert, OutputAssertExt}; @@ -559,6 +559,14 @@ impl TestContext { command } + /// Create a `uv add --no-sync` command for the given requirements. + pub fn add_no_sync(&self, reqs: &[&str]) -> Command { + let mut command = Command::new(get_bin()); + command.arg("add").arg("--no-sync").args(reqs); + self.add_shared_args(&mut command); + command + } + /// Create a `uv remove` command for the given requirements. pub fn remove(&self, reqs: &[&str]) -> Command { let mut command = Command::new(get_bin()); @@ -567,6 +575,14 @@ impl TestContext { command } + /// Create a `uv remove --no-sync` command for the given requirements. + pub fn remove_no_sync(&self, reqs: &[&str]) -> Command { + let mut command = Command::new(get_bin()); + command.arg("remove").arg("--no-sync").args(reqs); + self.add_shared_args(&mut command); + command + } + /// Create a `uv tree` command with options shared across scenarios. pub fn tree(&self) -> Command { let mut command = Command::new(get_bin()); @@ -738,6 +754,51 @@ impl TestContext { let project_dir = PathBuf::from(format!("../../ecosystem/{name}")); self.temp_dir.copy_from(project_dir, &["*"]).unwrap(); } + + /// Creates a way to compare the changes made to a lock file. + /// + /// This routine starts by copying (not moves) the generated lock file to + /// memory. It then calls the given closure with this test context to get a + /// `Command` and runs the command. The diff between the old lock file and + /// the new one is then returned. + /// + /// This assumes that a lock has already been performed. + pub fn diff_lock(&self, change: impl Fn(&TestContext) -> Command) -> String { + static TRIM_TRAILING_WHITESPACE: std::sync::LazyLock = + std::sync::LazyLock::new(|| Regex::new(r"(?m)^\s+$").unwrap()); + + let lock_path = ChildPath::new(self.temp_dir.join("uv.lock")); + let old_lock = fs_err::read_to_string(&lock_path).unwrap(); + let (snapshot, _, status) = run_and_format_with_status( + change(self), + self.filters(), + "diff_lock", + Some(crate::common::WindowsFilters::Platform), + ); + assert!(status.success(), "{snapshot}"); + let new_lock = fs_err::read_to_string(&lock_path).unwrap(); + diff_snapshot(&old_lock, &new_lock) + } +} + +/// Creates a "unified" diff between the two line-oriented strings suitable +/// for snapshotting. +pub fn diff_snapshot(old: &str, new: &str) -> String { + static TRIM_TRAILING_WHITESPACE: std::sync::LazyLock = + std::sync::LazyLock::new(|| Regex::new(r"(?m)^\s+$").unwrap()); + + let diff = similar::TextDiff::from_lines(old, new); + let unified = diff + .unified_diff() + .context_radius(10) + .header("old", "new") + .to_string(); + // Not totally clear why, but some lines end up containing only + // whitespace in the diff, even though they don't appear in the + // original data. So just strip them here. + TRIM_TRAILING_WHITESPACE + .replace_all(&unified, "") + .into_owned() } pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf { @@ -869,11 +930,25 @@ pub enum WindowsFilters { /// /// This function is derived from `insta_cmd`s `spawn_with_info`. pub fn run_and_format>( - mut command: impl BorrowMut, + command: impl BorrowMut, filters: impl AsRef<[(T, T)]>, function_name: &str, windows_filters: Option, ) -> (String, Output) { + let (snapshot, output, _) = + run_and_format_with_status(command, filters, function_name, windows_filters); + (snapshot, output) +} + +/// Execute the command and format its output status, stdout and stderr into a snapshot string. +/// +/// This function is derived from `insta_cmd`s `spawn_with_info`. +pub fn run_and_format_with_status>( + mut command: impl BorrowMut, + filters: impl AsRef<[(T, T)]>, + function_name: &str, + windows_filters: Option, +) -> (String, Output, ExitStatus) { let program = command .borrow_mut() .get_program() @@ -955,7 +1030,8 @@ pub fn run_and_format>( } } - (snapshot, output) + let status = output.status; + (snapshot, output, status) } /// Recursively copy a directory and its contents.