Skip to main content

uv_test/
lib.rs

1// The `unreachable_pub` is to silence false positives in RustRover.
2#![allow(dead_code, unreachable_pub)]
3
4pub mod find_links;
5mod http_server;
6pub mod packse;
7pub mod pypi_proxy;
8mod vendor;
9
10use std::borrow::BorrowMut;
11use std::ffi::OsString;
12use std::io::Write as _;
13use std::iter::Iterator;
14use std::path::{Path, PathBuf};
15use std::process::{Command, Output, Stdio};
16use std::str::FromStr;
17use std::{env, io};
18use uv_python::downloads::ManagedPythonDownloadList;
19
20use assert_cmd::assert::{Assert, OutputAssertExt};
21use assert_fs::assert::PathAssert;
22use assert_fs::fixture::{
23    ChildPath, FileWriteStr, PathChild, PathCopy, PathCreateDir, SymlinkToFile,
24};
25use base64::{Engine, prelude::BASE64_STANDARD as base64};
26use futures::StreamExt;
27use indoc::{formatdoc, indoc};
28use itertools::Itertools;
29use predicates::prelude::predicate;
30use regex::Regex;
31use tokio::io::AsyncWriteExt;
32
33use uv_cache::{Cache, CacheBucket};
34use uv_fs::Simplified;
35use uv_python::managed::ManagedPythonInstallations;
36use uv_python::{
37    EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest, PythonVersion,
38};
39use uv_static::EnvVars;
40
41// Shared test timestamp for deterministic package availability and relative times.
42static TEST_TIMESTAMP: &str = "2024-03-25T00:00:00Z";
43
44pub const DEFAULT_PYTHON_VERSION: &str = "3.12";
45
46// The expected latest patch version for each Python minor version.
47const LATEST_PYTHON_3_15: &str = "3.15.0b3";
48const LATEST_PYTHON_3_14: &str = "3.14.6";
49const LATEST_PYTHON_3_13: &str = "3.13.14";
50pub const LATEST_PYTHON_3_12: &str = "3.12.13";
51const LATEST_PYTHON_3_11: &str = "3.11.15";
52const LATEST_PYTHON_3_10: &str = "3.10.20";
53
54/// Create a new [`TestContext`] with the given Python version.
55///
56/// Creates a virtual environment for the test.
57///
58/// This macro captures the uv binary path at compile time using `env!("CARGO_BIN_EXE_uv")`,
59/// which is only available in the test crate.
60#[macro_export]
61macro_rules! test_context {
62    ($python_version:expr) => {
63        $crate::TestContext::new_with_bin(
64            $python_version,
65            std::path::PathBuf::from(env!("CARGO_BIN_EXE_uv")),
66        )
67    };
68}
69
70/// Create a new [`TestContext`] with zero or more Python versions.
71///
72/// Unlike [`test_context!`], this does not create a virtual environment.
73///
74/// This macro captures the uv binary path at compile time using `env!("CARGO_BIN_EXE_uv")`,
75/// which is only available in the test crate.
76#[macro_export]
77macro_rules! test_context_with_versions {
78    ($python_versions:expr) => {
79        $crate::TestContext::new_with_versions_and_bin(
80            $python_versions,
81            std::path::PathBuf::from(env!("CARGO_BIN_EXE_uv")),
82        )
83    };
84}
85
86/// Return the path to the uv binary.
87///
88/// This macro captures the uv binary path at compile time using `env!("CARGO_BIN_EXE_uv")`,
89/// which is only available in the test crate.
90#[macro_export]
91macro_rules! get_bin {
92    () => {
93        std::path::PathBuf::from(env!("CARGO_BIN_EXE_uv"))
94    };
95}
96
97#[doc(hidden)] // Macro and test context only, don't use directly.
98pub const INSTA_FILTERS: &[(&str, &str)] = &[
99    (r"--cache-dir [^\s]+", "--cache-dir [CACHE_DIR]"),
100    // Operation times
101    (r"(\s|\()(\d+m )?(\d+\.)?\d+(ms|s)", "$1[TIME]"),
102    // File sizes
103    (r"(\s|\()(\d+\.)?\d+([KM]i)?B", "$1[SIZE]"),
104    // Timestamps
105    (r"tv_sec: \d+", "tv_sec: [TIME]"),
106    (r"tv_nsec: \d+", "tv_nsec: [TIME]"),
107    // Rewrite Windows output to Unix output
108    (r"\\([\w\d]|\.)", "/$1"),
109    (r"uv\.exe", "uv"),
110    // uv version display
111    (
112        r"uv(-.*)? \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?( \([^)]*\))?",
113        r"uv [VERSION] ([COMMIT] DATE)",
114    ),
115    // Trim end-of-line whitespaces, to allow removing them on save.
116    (r"([^\s])[ \t]+(\r?\n)", "$1$2"),
117    // Filter SSL certificate loading debug messages (environment-dependent)
118    (r"DEBUG Loaded \d+ certificate\(s\) from [^\n]+\n", ""),
119];
120
121/// Create a context for tests which simplifies shared behavior across tests.
122///
123/// * Set the current directory to a temporary directory (`temp_dir`).
124/// * Set the cache dir to a different temporary directory (`cache_dir`).
125/// * Set a shared test timestamp so snapshots don't change after a new release.
126/// * Set the venv to a fresh `.venv` in `temp_dir`
127pub struct TestContext {
128    pub root: ChildPath,
129    pub temp_dir: ChildPath,
130    pub cache_dir: ChildPath,
131    python_dir: ChildPath,
132    pub home_dir: ChildPath,
133    pub user_config_dir: ChildPath,
134    pub bin_dir: ChildPath,
135    pub venv: ChildPath,
136    pub workspace_root: PathBuf,
137
138    /// The Python version used for the virtual environment, if any.
139    python_version: Option<PythonVersion>,
140
141    /// All the Python versions available during this test context.
142    pub python_versions: Vec<(PythonVersion, PathBuf)>,
143
144    /// Path to the uv binary.
145    uv_bin: PathBuf,
146
147    /// Standard filters for this test context.
148    filters: Vec<(String, String)>,
149
150    /// Extra environment variables to apply to all commands.
151    extra_env: Vec<(OsString, OsString)>,
152
153    #[allow(dead_code)]
154    _root: tempfile::TempDir,
155
156    /// Extra temporary directories whose lifetimes are tied to this context (e.g., directories
157    /// on alternate filesystems created by [`TestContext::with_cache_on_cow_fs`]).
158    #[allow(dead_code)]
159    _extra_tempdirs: Vec<tempfile::TempDir>,
160}
161
162impl TestContext {
163    /// Create a new test context with a virtual environment and explicit uv binary path.
164    ///
165    /// This is called by the `test_context!` macro.
166    pub fn new_with_bin(python_version: &str, uv_bin: PathBuf) -> Self {
167        let new = Self::new_with_versions_and_bin(&[python_version], uv_bin);
168        new.create_venv();
169        new
170    }
171
172    /// Set the "exclude newer" timestamp for all commands in this context.
173    #[must_use]
174    pub fn with_exclude_newer(mut self, exclude_newer: &str) -> Self {
175        self.extra_env
176            .push((EnvVars::UV_EXCLUDE_NEWER.into(), exclude_newer.into()));
177        self
178    }
179
180    /// Set the "http timeout" for all commands in this context.
181    #[must_use]
182    pub fn with_http_timeout(mut self, http_timeout: &str) -> Self {
183        self.extra_env
184            .push((EnvVars::UV_HTTP_TIMEOUT.into(), http_timeout.into()));
185        self
186    }
187
188    /// Set the "concurrent installs" for all commands in this context.
189    #[must_use]
190    pub fn with_concurrent_installs(mut self, concurrent_installs: &str) -> Self {
191        self.extra_env.push((
192            EnvVars::UV_CONCURRENT_INSTALLS.into(),
193            concurrent_installs.into(),
194        ));
195        self
196    }
197
198    /// Add extra standard filtering for messages like "Resolved 10 packages" which
199    /// can differ between platforms.
200    ///
201    /// In some cases, these counts are helpful for the snapshot and should not be filtered.
202    #[must_use]
203    pub fn with_filtered_counts(mut self) -> Self {
204        for verb in &[
205            "Resolved",
206            "Prepared",
207            "Installed",
208            "Uninstalled",
209            "Checked",
210        ] {
211            self.filters.push((
212                format!("{verb} \\d+ packages?"),
213                format!("{verb} [N] packages"),
214            ));
215        }
216        self.filters.push((
217            "Removed \\d+ files?".to_string(),
218            "Removed [N] files".to_string(),
219        ));
220        self
221    }
222
223    /// Add extra filtering for cache size output
224    #[must_use]
225    pub fn with_filtered_cache_size(mut self) -> Self {
226        // Filter raw byte counts (numbers on their own line)
227        self.filters
228            .push((r"(?m)^\d+\n".to_string(), "[SIZE]\n".to_string()));
229        // Filter human-readable sizes (e.g., "384.2 KiB")
230        self.filters.push((
231            r"(?m)^\d+(\.\d+)? [KMGT]i?B\n".to_string(),
232            "[SIZE]\n".to_string(),
233        ));
234        self
235    }
236
237    /// Filter hashes from backticked centralized environment cache entry names.
238    #[must_use]
239    pub fn with_filtered_centralized_environment_hashes(mut self) -> Self {
240        self.filters.push((
241            r"`([\w.\[\]-]+)-[a-f0-9]{16}`".to_string(),
242            "`$1-[HASH]`".to_string(),
243        ));
244        self
245    }
246
247    /// Add extra standard filtering for Windows-compatible missing file errors.
248    #[must_use]
249    pub fn with_filtered_missing_file_error(mut self) -> Self {
250        // The exact message string depends on the system language, so we remove it.
251        // We want to only remove the phrase after `Caused by:`
252        self.filters.push((
253            r"[^:\n]* \(os error 2\)".to_string(),
254            " [OS ERROR 2]".to_string(),
255        ));
256        // Replace the Windows "The system cannot find the path specified. (os error 3)"
257        // with the Unix "No such file or directory (os error 2)"
258        // and mask the language-dependent message.
259        self.filters.push((
260            r"[^:\n]* \(os error 3\)".to_string(),
261            " [OS ERROR 2]".to_string(),
262        ));
263        self
264    }
265
266    /// Add extra standard filtering for executable suffixes on the current platform e.g.
267    /// drops `.exe` on Windows.
268    #[must_use]
269    pub fn with_filtered_exe_suffix(mut self) -> Self {
270        self.filters
271            .push((regex::escape(env::consts::EXE_SUFFIX), String::new()));
272        self
273    }
274
275    /// Add extra standard filtering for Python interpreter sources
276    #[must_use]
277    pub fn with_filtered_python_sources(mut self) -> Self {
278        self.filters.push((
279            "virtual environments, managed installations, or search path".to_string(),
280            "[PYTHON SOURCES]".to_string(),
281        ));
282        self.filters.push((
283            "virtual environments, managed installations, search path, or registry".to_string(),
284            "[PYTHON SOURCES]".to_string(),
285        ));
286        self.filters.push((
287            "virtual environments, search path, or registry".to_string(),
288            "[PYTHON SOURCES]".to_string(),
289        ));
290        self.filters.push((
291            "virtual environments, registry, or search path".to_string(),
292            "[PYTHON SOURCES]".to_string(),
293        ));
294        self.filters.push((
295            "virtual environments or search path".to_string(),
296            "[PYTHON SOURCES]".to_string(),
297        ));
298        self.filters.push((
299            "managed installations or search path".to_string(),
300            "[PYTHON SOURCES]".to_string(),
301        ));
302        self.filters.push((
303            "managed installations, search path, or registry".to_string(),
304            "[PYTHON SOURCES]".to_string(),
305        ));
306        self.filters.push((
307            "search path or registry".to_string(),
308            "[PYTHON SOURCES]".to_string(),
309        ));
310        self.filters.push((
311            "registry or search path".to_string(),
312            "[PYTHON SOURCES]".to_string(),
313        ));
314        self.filters
315            .push(("search path".to_string(), "[PYTHON SOURCES]".to_string()));
316        self
317    }
318
319    /// Add extra standard filtering for Python executable names, e.g., stripping version number
320    /// and `.exe` suffixes.
321    #[must_use]
322    pub fn with_filtered_python_names(mut self) -> Self {
323        for name in ["python", "pypy"] {
324            // Note we strip version numbers from the executable names because, e.g., on Windows
325            // `python.exe` is the equivalent to a Unix `python3.12`.`
326            let suffix = if cfg!(windows) {
327                // On Windows, we'll require a `.exe` suffix for disambiguation
328                // We'll also strip version numbers if present, which is not common for `python.exe`
329                // but can occur for, e.g., `pypy3.12.exe`
330                let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
331                format!(r"(\d\.\d+|\d)?{exe_suffix}")
332            } else {
333                // On Unix, we'll strip version numbers
334                if name == "python" {
335                    // We can't require them in this case since `/python` is common
336                    r"(\d\.\d+|\d)?(t|d|td)?".to_string()
337                } else {
338                    // However, for other names we'll require them to avoid over-matching
339                    r"(\d\.\d+|\d)(t|d|td)?".to_string()
340                }
341            };
342
343            self.filters.push((
344                // We use a leading path separator to help disambiguate cases where the name is not
345                // used in a path.
346                format!(r"[\\/]{name}{suffix}"),
347                format!("/[{}]", name.to_uppercase()),
348            ));
349        }
350
351        self
352    }
353
354    /// Add extra standard filtering for venv executable directories on the current platform e.g.
355    /// `Scripts` on Windows and `bin` on Unix.
356    #[must_use]
357    pub fn with_filtered_virtualenv_bin(mut self) -> Self {
358        self.filters.push((
359            format!(
360                r"[\\/]{}[\\/]",
361                venv_bin_path(PathBuf::new()).to_string_lossy()
362            ),
363            "/[BIN]/".to_string(),
364        ));
365        self.filters.push((
366            format!(r"[\\/]{}", venv_bin_path(PathBuf::new()).to_string_lossy()),
367            "/[BIN]".to_string(),
368        ));
369        self
370    }
371
372    /// Add extra standard filtering for Python installation `bin/` directories, which are not
373    /// present on Windows but are on Unix. See [`TestContext::with_filtered_virtualenv_bin`] for
374    /// the virtual environment equivalent.
375    #[must_use]
376    pub fn with_filtered_python_install_bin(mut self) -> Self {
377        // We don't want to eagerly match paths that aren't actually Python executables, so we
378        // do our best to detect that case
379        let suffix = if cfg!(windows) {
380            let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
381            // On Windows, we usually don't have a version attached but we might, e.g., for pypy3.12
382            format!(r"(\d\.\d+|\d)?{exe_suffix}")
383        } else {
384            // On Unix, we'll require a version to be attached to avoid over-matching
385            r"\d\.\d+|\d".to_string()
386        };
387
388        if cfg!(unix) {
389            self.filters.push((
390                format!(r"[\\/]bin/python({suffix})"),
391                "/[INSTALL-BIN]/python$1".to_string(),
392            ));
393            self.filters.push((
394                format!(r"[\\/]bin/pypy({suffix})"),
395                "/[INSTALL-BIN]/pypy$1".to_string(),
396            ));
397        } else {
398            self.filters.push((
399                format!(r"[\\/]python({suffix})"),
400                "/[INSTALL-BIN]/python$1".to_string(),
401            ));
402            self.filters.push((
403                format!(r"[\\/]pypy({suffix})"),
404                "/[INSTALL-BIN]/pypy$1".to_string(),
405            ));
406        }
407        self
408    }
409
410    /// Filtering for various keys in a `pyvenv.cfg` file that will vary
411    /// depending on the specific machine used:
412    /// - `home = foo/bar/baz/python3.X.X/bin`
413    /// - `uv = X.Y.Z`
414    /// - `extends-environment = <path/to/parent/venv>`
415    #[must_use]
416    pub fn with_pyvenv_cfg_filters(mut self) -> Self {
417        let added_filters = [
418            (r"home = .+".to_string(), "home = [PYTHON_HOME]".to_string()),
419            (
420                r"uv = \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?".to_string(),
421                "uv = [UV_VERSION]".to_string(),
422            ),
423            (
424                r"extends-environment = .+".to_string(),
425                "extends-environment = [PARENT_VENV]".to_string(),
426            ),
427        ];
428        for filter in added_filters {
429            self.filters.insert(0, filter);
430        }
431        self
432    }
433
434    /// Add extra filtering for ` -> <PATH>` symlink display for Python versions in the test
435    /// context, e.g., for use in `uv python list`.
436    #[must_use]
437    pub fn with_filtered_python_symlinks(mut self) -> Self {
438        for (version, executable) in &self.python_versions {
439            if fs_err::symlink_metadata(executable).unwrap().is_symlink() {
440                self.filters.extend(
441                    Self::path_patterns(executable.read_link().unwrap())
442                        .into_iter()
443                        .map(|pattern| (format! {" -> {pattern}"}, String::new())),
444                );
445            }
446            // Drop links that are byproducts of the test context too
447            self.filters.push((
448                regex::escape(&format!(" -> [PYTHON-{version}]")),
449                String::new(),
450            ));
451        }
452        self
453    }
454
455    /// Add extra standard filtering for a given path.
456    #[must_use]
457    pub fn with_filtered_path(mut self, path: &Path, name: &str) -> Self {
458        // Note this is sloppy, ideally we wouldn't push to the front of the `Vec` but we need
459        // this to come in front of other filters or we can transform the path (e.g., with `[TMP]`)
460        // before we reach this filter.
461        for pattern in Self::path_patterns(path)
462            .into_iter()
463            .map(|pattern| (pattern, format!("[{name}]/")))
464        {
465            self.filters.insert(0, pattern);
466        }
467        self
468    }
469
470    /// Adds a filter that specifically ignores the link mode warning.
471    ///
472    /// This occurs in some cases and can be used on an ad hoc basis to squash
473    /// the warning in the snapshots. This is useful because the warning does
474    /// not consistently appear. It is dependent on the environment. (For
475    /// example, sometimes it's dependent on whether `/tmp` and `~/.local` live
476    /// on the same file system.)
477    #[inline]
478    #[must_use]
479    pub fn with_filtered_link_mode_warning(mut self) -> Self {
480        let pattern = "warning: Failed to hardlink files; .*\n.*\n.*\n";
481        self.filters.push((pattern.to_string(), String::new()));
482        self
483    }
484
485    /// Adds a filter for platform-specific errors when a file is not executable.
486    #[inline]
487    #[must_use]
488    pub fn with_filtered_not_executable(mut self) -> Self {
489        let pattern = if cfg!(unix) {
490            r"Permission denied \(os error 13\)"
491        } else {
492            r"\%1 is not a valid Win32 application. \(os error 193\)"
493        };
494        self.filters
495            .push((pattern.to_string(), "[PERMISSION DENIED]".to_string()));
496        self
497    }
498
499    /// Adds a filter that ignores platform information in a Python installation key.
500    #[must_use]
501    pub fn with_filtered_python_keys(mut self) -> Self {
502        // Filter platform keys
503        let platform_re = r"(?x)
504  (                         # We capture the group before the platform
505    (?:cpython|pypy|graalpy)# Python implementation
506    -
507    \d+\.\d+                # Major and minor version
508    (?:                     # The patch version is handled separately
509      \.
510      (?:
511        \[X\]               # A previously filtered patch version [X]
512        |                   # OR
513        \[LATEST\]          # A previously filtered latest patch version [LATEST]
514        |                   # OR
515        \d+                 # An actual patch version
516      )
517    )?                      # (we allow the patch version to be missing entirely, e.g., in a request)
518    (?:(?:a|b|rc)[0-9]+)?   # Pre-release version component, e.g., `a6` or `rc2`
519    (?:[td])?               # A short variant, such as `t` (for freethreaded) or `d` (for debug)
520    (?:(\+[a-z]+)+)?        # A long variant, such as `+freethreaded` or `+freethreaded+debug`
521  )
522  -
523  [a-z0-9]+                 # Operating system (e.g., 'macos')
524  -
525  [a-z0-9_]+                # Architecture (e.g., 'aarch64')
526  -
527  [a-z]+                    # Libc (e.g., 'none')
528";
529        self.filters
530            .push((platform_re.to_string(), "$1-[PLATFORM]".to_string()));
531        self
532    }
533
534    /// Adds a filter that replaces the latest Python patch versions with `[LATEST]` placeholder.
535    #[must_use]
536    pub fn with_filtered_latest_python_versions(mut self) -> Self {
537        // Filter the latest patch versions with [LATEST] placeholder
538        // The order matters - we want to match the full version first
539        for (minor, patch) in [
540            ("3.15", LATEST_PYTHON_3_15.strip_prefix("3.15.").unwrap()),
541            ("3.14", LATEST_PYTHON_3_14.strip_prefix("3.14.").unwrap()),
542            ("3.13", LATEST_PYTHON_3_13.strip_prefix("3.13.").unwrap()),
543            ("3.12", LATEST_PYTHON_3_12.strip_prefix("3.12.").unwrap()),
544            ("3.11", LATEST_PYTHON_3_11.strip_prefix("3.11.").unwrap()),
545            ("3.10", LATEST_PYTHON_3_10.strip_prefix("3.10.").unwrap()),
546        ] {
547            // Match the full version in various contexts (cpython-X.Y.Z, Python X.Y.Z, etc.)
548            let pattern = format!(r"(\b){minor}\.{patch}(\b)");
549            let replacement = format!("${{1}}{minor}.[LATEST]${{2}}");
550            self.filters.push((pattern, replacement));
551        }
552        self
553    }
554
555    /// Add a filter that ignores temporary directory in path.
556    #[must_use]
557    #[cfg(windows)]
558    pub fn with_filtered_windows_temp_dir(mut self) -> Self {
559        let pattern = regex::escape(
560            &self
561                .temp_dir
562                .simplified_display()
563                .to_string()
564                .replace('/', "\\"),
565        );
566        self.filters.push((pattern, "[TEMP_DIR]".to_string()));
567        self
568    }
569
570    /// Add a filter for (bytecode) compilation file counts
571    #[must_use]
572    pub fn with_filtered_compiled_file_count(mut self) -> Self {
573        self.filters.push((
574            r"compiled \d+ files".to_string(),
575            "compiled [COUNT] files".to_string(),
576        ));
577        self
578    }
579
580    /// Add a (not context aware) filter for the current uv version `v<major>.<minor>.<patch>`
581    #[must_use]
582    pub fn with_filtered_current_version(mut self) -> Self {
583        self.filters.push((
584            regex::escape(&format!("v{}", env!("CARGO_PKG_VERSION"))),
585            "v[CURRENT_VERSION]".to_string(),
586        ));
587        self
588    }
589
590    /// Adds filters for non-deterministic `CycloneDX` data
591    #[must_use]
592    pub fn with_cyclonedx_filters(mut self) -> Self {
593        self.filters.push((
594            r"urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}".to_string(),
595            "[SERIAL_NUMBER]".to_string(),
596        ));
597        self.filters.push((
598            r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#
599                .to_string(),
600            r#""timestamp": "[TIMESTAMP]""#.to_string(),
601        ));
602        self.filters.push((
603            r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#
604                .to_string(),
605            r#""name": "uv",
606        "version": "[VERSION]""#
607                .to_string(),
608        ));
609        self
610    }
611
612    /// Add a filter that collapses duplicate whitespace.
613    #[must_use]
614    pub fn with_collapsed_whitespace(mut self) -> Self {
615        self.filters.push((r"[ \t]+".to_string(), " ".to_string()));
616        self
617    }
618
619    /// Use a shared global cache for Python downloads.
620    #[must_use]
621    pub fn with_python_download_cache(mut self) -> Self {
622        self.extra_env.push((
623            EnvVars::UV_PYTHON_CACHE_DIR.into(),
624            // Respect `UV_PYTHON_CACHE_DIR` if set, or use the default cache directory
625            env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).unwrap_or_else(|| {
626                uv_cache::Cache::from_settings(false, None)
627                    .unwrap()
628                    .bucket(CacheBucket::Python)
629                    .into()
630            }),
631        ));
632        self
633    }
634
635    #[must_use]
636    pub fn with_empty_python_install_mirror(mut self) -> Self {
637        self.extra_env.push((
638            EnvVars::UV_PYTHON_INSTALL_MIRROR.into(),
639            String::new().into(),
640        ));
641        self
642    }
643
644    /// Add extra directories and configuration for managed Python installations.
645    #[must_use]
646    pub fn with_managed_python_dirs(mut self) -> Self {
647        let managed = self.temp_dir.join("managed");
648
649        self.extra_env.push((
650            EnvVars::UV_PYTHON_BIN_DIR.into(),
651            self.bin_dir.as_os_str().to_owned(),
652        ));
653        self.extra_env
654            .push((EnvVars::UV_PYTHON_INSTALL_DIR.into(), managed.into()));
655        self.extra_env
656            .push((EnvVars::UV_PYTHON_DOWNLOADS.into(), "automatic".into()));
657
658        self
659    }
660
661    #[must_use]
662    pub fn with_versions_as_managed(mut self, versions: &[&str]) -> Self {
663        self.extra_env.push((
664            EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED.into(),
665            versions.iter().join(" ").into(),
666        ));
667
668        self
669    }
670
671    /// Add a custom filter to the `TestContext`.
672    #[must_use]
673    pub fn with_filter(mut self, filter: (impl Into<String>, impl Into<String>)) -> Self {
674        self.filters.push((filter.0.into(), filter.1.into()));
675        self
676    }
677
678    // Unsets the git credential helper using temp home gitconfig
679    #[must_use]
680    pub fn with_unset_git_credential_helper(self) -> Self {
681        let git_config = self.home_dir.child(".gitconfig");
682        git_config
683            .write_str(indoc! {r"
684                [credential]
685                    helper =
686            "})
687            .expect("Failed to unset git credential helper");
688
689        self
690    }
691
692    /// Clear filters on `TestContext`.
693    #[must_use]
694    #[cfg(windows)]
695    pub fn clear_filters(mut self) -> Self {
696        self.filters.clear();
697        self
698    }
699
700    /// Use a cache directory on the filesystem specified by
701    /// [`EnvVars::UV_INTERNAL__TEST_COW_FS`].
702    ///
703    /// Returns `Ok(None)` if the environment variable is not set.
704    pub fn with_cache_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
705        let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
706            return Ok(None);
707        };
708        self.with_cache_on_fs(&dir, "COW_FS").map(Some)
709    }
710
711    /// Use a cache directory on the filesystem specified by
712    /// [`EnvVars::UV_INTERNAL__TEST_ALT_FS`].
713    ///
714    /// Returns `Ok(None)` if the environment variable is not set.
715    pub fn with_cache_on_alt_fs(self) -> anyhow::Result<Option<Self>> {
716        let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_ALT_FS).ok() else {
717            return Ok(None);
718        };
719        self.with_cache_on_fs(&dir, "ALT_FS").map(Some)
720    }
721
722    /// Use a cache directory on the filesystem specified by
723    /// [`EnvVars::UV_INTERNAL__TEST_LOWLINKS_FS`].
724    ///
725    /// Returns `Ok(None)` if the environment variable is not set.
726    pub fn with_cache_on_lowlinks_fs(self) -> anyhow::Result<Option<Self>> {
727        let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_LOWLINKS_FS).ok() else {
728            return Ok(None);
729        };
730        self.with_cache_on_fs(&dir, "LOWLINKS_FS").map(Some)
731    }
732
733    /// Use a cache directory on the filesystem specified by
734    /// [`EnvVars::UV_INTERNAL__TEST_NOCOW_FS`].
735    ///
736    /// Returns `Ok(None)` if the environment variable is not set.
737    pub fn with_cache_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
738        let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
739            return Ok(None);
740        };
741        self.with_cache_on_fs(&dir, "NOCOW_FS").map(Some)
742    }
743
744    /// Use a working directory on the filesystem specified by
745    /// [`EnvVars::UV_INTERNAL__TEST_COW_FS`].
746    ///
747    /// Returns `Ok(None)` if the environment variable is not set.
748    ///
749    /// Note a virtual environment is not created automatically.
750    pub fn with_working_dir_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
751        let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
752            return Ok(None);
753        };
754        self.with_working_dir_on_fs(&dir, "COW_FS").map(Some)
755    }
756
757    /// Use a working directory on the filesystem specified by
758    /// [`EnvVars::UV_INTERNAL__TEST_NOCOW_FS`].
759    ///
760    /// Returns `Ok(None)` if the environment variable is not set.
761    ///
762    /// Note a virtual environment is not created automatically.
763    pub fn with_working_dir_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
764        let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
765            return Ok(None);
766        };
767        self.with_working_dir_on_fs(&dir, "NOCOW_FS").map(Some)
768    }
769
770    fn with_cache_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
771        fs_err::create_dir_all(dir)?;
772        let tmp = tempfile::TempDir::new_in(dir)?;
773        self.cache_dir = ChildPath::new(tmp.path()).child("cache");
774        fs_err::create_dir_all(&self.cache_dir)?;
775        let replacement = format!("[{name}]/[CACHE_DIR]/");
776        self.filters.extend(
777            Self::path_patterns(&self.cache_dir)
778                .into_iter()
779                .map(|pattern| (pattern, replacement.clone())),
780        );
781        self._extra_tempdirs.push(tmp);
782        Ok(self)
783    }
784
785    fn with_working_dir_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
786        fs_err::create_dir_all(dir)?;
787        let tmp = tempfile::TempDir::new_in(dir)?;
788        self.temp_dir = ChildPath::new(tmp.path()).child("temp");
789        fs_err::create_dir_all(&self.temp_dir)?;
790        // Place the venv inside temp_dir (matching the default TestContext layout)
791        // so that `context.venv()` creates it at the same path that `VIRTUAL_ENV` points to.
792        let canonical_temp_dir = self.temp_dir.canonicalize()?;
793        self.venv = ChildPath::new(canonical_temp_dir.join(".venv"));
794        let temp_replacement = format!("[{name}]/[TEMP_DIR]/");
795        self.filters.extend(
796            Self::path_patterns(&self.temp_dir)
797                .into_iter()
798                .map(|pattern| (pattern, temp_replacement.clone())),
799        );
800        let venv_replacement = format!("[{name}]/[VENV]/");
801        self.filters.extend(
802            Self::path_patterns(&self.venv)
803                .into_iter()
804                .map(|pattern| (pattern, venv_replacement.clone())),
805        );
806        self._extra_tempdirs.push(tmp);
807        Ok(self)
808    }
809
810    /// Default to the canonicalized path to the temp directory. We need to do this because on
811    /// macOS (and Windows on GitHub Actions) the standard temp dir is a symlink. (On macOS, the
812    /// temporary directory is, like `/var/...`, which resolves to `/private/var/...`.)
813    ///
814    /// It turns out that, at least on macOS, if we pass a symlink as `current_dir`, it gets
815    /// _immediately_ resolved (such that if you call `current_dir` in the running `Command`, it
816    /// returns resolved symlink). This breaks some snapshot tests, since we _don't_ want to
817    /// resolve symlinks for user-provided paths.
818    pub fn test_bucket_dir() -> PathBuf {
819        std::env::temp_dir()
820            .simple_canonicalize()
821            .expect("failed to canonicalize temp dir")
822            .join("uv")
823            .join("tests")
824    }
825
826    /// Create a new test context with multiple Python versions and explicit uv binary path.
827    ///
828    /// Does not create a virtual environment by default, but the first Python version
829    /// can be used to create a virtual environment with [`TestContext::create_venv`].
830    ///
831    /// This is called by the `test_context_with_versions!` macro.
832    pub fn new_with_versions_and_bin(python_versions: &[&str], uv_bin: PathBuf) -> Self {
833        let bucket = Self::test_bucket_dir();
834        fs_err::create_dir_all(&bucket).expect("Failed to create test bucket");
835
836        let root = tempfile::TempDir::new_in(bucket).expect("Failed to create test root directory");
837
838        // Create a `.git` directory to isolate tests that search for git boundaries from the state
839        // of the file system
840        fs_err::create_dir_all(root.path().join(".git"))
841            .expect("Failed to create `.git` placeholder in test root directory");
842
843        let temp_dir = ChildPath::new(root.path()).child("temp");
844        fs_err::create_dir_all(&temp_dir).expect("Failed to create test working directory");
845
846        let cache_dir = ChildPath::new(root.path()).child("cache");
847        fs_err::create_dir_all(&cache_dir).expect("Failed to create test cache directory");
848
849        let python_dir = ChildPath::new(root.path()).child("python");
850        fs_err::create_dir_all(&python_dir).expect("Failed to create test Python directory");
851
852        let bin_dir = ChildPath::new(root.path()).child("bin");
853        fs_err::create_dir_all(&bin_dir).expect("Failed to create test bin directory");
854
855        // When the `git` feature is disabled, enforce that the test suite does not use `git`
856        if cfg!(not(feature = "git")) {
857            Self::disallow_git_cli(&bin_dir).expect("Failed to setup disallowed `git` command");
858        }
859
860        let home_dir = ChildPath::new(root.path()).child("home");
861        fs_err::create_dir_all(&home_dir).expect("Failed to create test home directory");
862
863        let user_config_dir = if cfg!(windows) {
864            ChildPath::new(home_dir.path())
865        } else {
866            ChildPath::new(home_dir.path()).child(".config")
867        };
868
869        // Canonicalize the temp dir for consistent snapshot behavior
870        let canonical_temp_dir = temp_dir.canonicalize().unwrap();
871        let venv = ChildPath::new(canonical_temp_dir.join(".venv"));
872
873        let python_version = python_versions
874            .first()
875            .map(|version| PythonVersion::from_str(version).unwrap());
876
877        let site_packages = python_version
878            .as_ref()
879            .map(|version| site_packages_path(&venv, &format!("python{version}")));
880
881        // The workspace root directory is not available without walking up the tree
882        // https://github.com/rust-lang/cargo/issues/3946
883        let workspace_root = Path::new(&env::var(EnvVars::CARGO_MANIFEST_DIR).unwrap())
884            .parent()
885            .expect("CARGO_MANIFEST_DIR should be nested in workspace")
886            .parent()
887            .expect("CARGO_MANIFEST_DIR should be doubly nested in workspace")
888            .to_path_buf();
889
890        let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
891
892        let python_versions: Vec<_> = python_versions
893            .iter()
894            .map(|version| PythonVersion::from_str(version).unwrap())
895            .zip(
896                python_installations_for_versions(&temp_dir, python_versions, &download_list)
897                    .expect("Failed to find test Python versions"),
898            )
899            .collect();
900
901        // Construct directories for each Python executable on Unix where the executable names
902        // need to be normalized
903        if cfg!(unix) {
904            for (version, executable) in &python_versions {
905                let parent = python_dir.child(version.to_string());
906                parent.create_dir_all().unwrap();
907                parent.child("python3").symlink_to_file(executable).unwrap();
908            }
909        }
910
911        let mut filters = Vec::new();
912
913        filters.extend(
914            Self::path_patterns(&uv_bin)
915                .into_iter()
916                .map(|pattern| (pattern, "[UV]".to_string())),
917        );
918
919        // Exclude `link-mode` on Windows since we set it in the remote test suite
920        if cfg!(windows) {
921            filters.push((" --link-mode <LINK_MODE>".to_string(), String::new()));
922            filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new()));
923            // Unix uses "exit status", Windows uses "exit code"
924            filters.push((r"exit code: ".to_string(), "exit status: ".to_string()));
925        }
926
927        for (version, executable) in &python_versions {
928            // Add filtering for the interpreter path
929            filters.extend(
930                Self::path_patterns(executable)
931                    .into_iter()
932                    .map(|pattern| (pattern, format!("[PYTHON-{version}]"))),
933            );
934
935            // And for the symlink we created in the test the Python path
936            filters.extend(
937                Self::path_patterns(python_dir.join(version.to_string()))
938                    .into_iter()
939                    .map(|pattern| {
940                        (
941                            format!("{pattern}[a-zA-Z0-9]*"),
942                            format!("[PYTHON-{version}]"),
943                        )
944                    }),
945            );
946
947            // Add Python patch version filtering unless explicitly requested to ensure
948            // snapshots are patch version agnostic when it is not a part of the test.
949            if version.patch().is_none() {
950                filters.push((
951                    format!(r"({})\.\d+", regex::escape(version.to_string().as_str())),
952                    "$1.[X]".to_string(),
953                ));
954            }
955        }
956
957        filters.extend(
958            Self::path_patterns(&bin_dir)
959                .into_iter()
960                .map(|pattern| (pattern, "[BIN]/".to_string())),
961        );
962        filters.extend(
963            Self::path_patterns(&cache_dir)
964                .into_iter()
965                .map(|pattern| (pattern, "[CACHE_DIR]/".to_string())),
966        );
967        if let Some(ref site_packages) = site_packages {
968            filters.extend(
969                Self::path_patterns(site_packages)
970                    .into_iter()
971                    .map(|pattern| (pattern, "[SITE_PACKAGES]/".to_string())),
972            );
973        }
974        filters.extend(
975            Self::path_patterns(&venv)
976                .into_iter()
977                .map(|pattern| (pattern, "[VENV]/".to_string())),
978        );
979
980        // Account for [`Simplified::user_display`] which is relative to the command working directory
981        if let Some(site_packages) = site_packages {
982            filters.push((
983                Self::path_pattern(
984                    site_packages
985                        .strip_prefix(&canonical_temp_dir)
986                        .expect("The test site-packages directory is always in the tempdir"),
987                ),
988                "[SITE_PACKAGES]/".to_string(),
989            ));
990        }
991
992        // Filter Python library path differences between Windows and Unix
993        filters.push((
994            r"[\\/]lib[\\/]python\d+\.\d+[\\/]".to_string(),
995            "/[PYTHON-LIB]/".to_string(),
996        ));
997        filters.push((r"[\\/]Lib[\\/]".to_string(), "/[PYTHON-LIB]/".to_string()));
998
999        filters.extend(
1000            Self::path_patterns(&temp_dir)
1001                .into_iter()
1002                .map(|pattern| (pattern, "[TEMP_DIR]/".to_string())),
1003        );
1004        filters.extend(
1005            Self::path_patterns(&python_dir)
1006                .into_iter()
1007                .map(|pattern| (pattern, "[PYTHON_DIR]/".to_string())),
1008        );
1009        let mut uv_user_config_dir = PathBuf::from(user_config_dir.path());
1010        uv_user_config_dir.push("uv");
1011        filters.extend(
1012            Self::path_patterns(&uv_user_config_dir)
1013                .into_iter()
1014                .map(|pattern| (pattern, "[UV_USER_CONFIG_DIR]/".to_string())),
1015        );
1016        filters.extend(
1017            Self::path_patterns(&user_config_dir)
1018                .into_iter()
1019                .map(|pattern| (pattern, "[USER_CONFIG_DIR]/".to_string())),
1020        );
1021        filters.extend(
1022            Self::path_patterns(&home_dir)
1023                .into_iter()
1024                .map(|pattern| (pattern, "[HOME]/".to_string())),
1025        );
1026        filters.extend(
1027            Self::path_patterns(&workspace_root)
1028                .into_iter()
1029                .map(|pattern| (pattern, "[WORKSPACE]/".to_string())),
1030        );
1031
1032        // Make virtual environment activation cross-platform and shell-agnostic
1033        filters.push((
1034            r"Activate with: (.*)\\Scripts\\activate".to_string(),
1035            "Activate with: source $1/[BIN]/activate".to_string(),
1036        ));
1037        filters.push((
1038            r"Activate with: Scripts\\activate".to_string(),
1039            "Activate with: source [BIN]/activate".to_string(),
1040        ));
1041        filters.push((
1042            r"Activate with: source (.*/|)bin/activate(?:\.\w+)?".to_string(),
1043            "Activate with: source $1[BIN]/activate".to_string(),
1044        ));
1045
1046        // Filter non-deterministic temporary directory names
1047        // Note we apply this _after_ all the full paths to avoid breaking their matching
1048        filters.push((r"(\\|\/)\.tmp.*(\\|\/)".to_string(), "/[TMP]/".to_string()));
1049
1050        // Account for platform prefix differences `file://` (Unix) vs `file:///` (Windows)
1051        filters.push((r"file:///".to_string(), "file://".to_string()));
1052
1053        // Destroy any remaining UNC prefixes (Windows only)
1054        filters.push((r"\\\\\?\\".to_string(), String::new()));
1055
1056        // For wiremock tests
1057        filters.push((r"127\.0\.0\.1:\d*".to_string(), "[LOCALHOST]".to_string()));
1058        // Avoid breaking the tests when bumping the uv version
1059        filters.push((
1060            format!(
1061                r#"requires = \["uv_build>={},<[0-9.]+"\]"#,
1062                uv_version::version()
1063            ),
1064            r#"requires = ["uv_build>=[CURRENT_VERSION],<[NEXT_BREAKING]"]"#.to_string(),
1065        ));
1066        // Filter environment cache entry hashes
1067        filters.push((
1068            r"environments-v(\d+)[\\/]([\w.\[\]-]+)-[a-f0-9]{16}".to_string(),
1069            "environments-v$1/$2-[HASH]".to_string(),
1070        ));
1071        // Filter archive hashes
1072        filters.push((
1073            r"archive-v(\d+)[\\/][A-Za-z0-9\-\_]+".to_string(),
1074            "archive-v$1/[HASH]".to_string(),
1075        ));
1076
1077        Self {
1078            root: ChildPath::new(root.path()),
1079            temp_dir,
1080            cache_dir,
1081            python_dir,
1082            home_dir,
1083            user_config_dir,
1084            bin_dir,
1085            venv,
1086            workspace_root,
1087            python_version,
1088            python_versions,
1089            uv_bin,
1090            filters,
1091            extra_env: vec![],
1092            _root: root,
1093            _extra_tempdirs: vec![],
1094        }
1095    }
1096
1097    /// Create a uv command for testing.
1098    pub fn command(&self) -> Command {
1099        let mut command = self.new_command();
1100        self.add_shared_options(&mut command, true);
1101        command
1102    }
1103
1104    pub fn disallow_git_cli(bin_dir: &Path) -> std::io::Result<()> {
1105        let contents = r"#!/bin/sh
1106    echo 'error: `git` operations are not allowed — are you missing a cfg for the `git` feature?' >&2
1107    exit 127";
1108        let git = bin_dir.join(format!("git{}", env::consts::EXE_SUFFIX));
1109        fs_err::write(&git, contents)?;
1110
1111        #[cfg(unix)]
1112        {
1113            use std::os::unix::fs::PermissionsExt;
1114            let mut perms = fs_err::metadata(&git)?.permissions();
1115            perms.set_mode(0o755);
1116            fs_err::set_permissions(&git, perms)?;
1117        }
1118
1119        Ok(())
1120    }
1121
1122    /// Setup Git LFS Filters
1123    ///
1124    /// You can find the default filters in <https://github.com/git-lfs/git-lfs/blob/v3.7.1/lfs/attribute.go#L66-L71>
1125    /// We set required to true to get a full stacktrace when these commands fail.
1126    #[must_use]
1127    pub fn with_git_lfs_config(mut self) -> Self {
1128        let git_lfs_config = self.root.child(".gitconfig");
1129        git_lfs_config
1130            .write_str(indoc! {r#"
1131                [filter "lfs"]
1132                    clean = git-lfs clean -- %f
1133                    smudge = git-lfs smudge -- %f
1134                    process = git-lfs filter-process
1135                    required = true
1136            "#})
1137            .expect("Failed to setup `git-lfs` filters");
1138
1139        // Its possible your system config can cause conflicts with the Git LFS tests.
1140        // In such cases, add self.extra_env.push(("GIT_CONFIG_NOSYSTEM".into(), "1".into()));
1141        self.extra_env.push((
1142            EnvVars::GIT_CONFIG_GLOBAL.into(),
1143            git_lfs_config.as_os_str().into(),
1144        ));
1145        self
1146    }
1147
1148    /// Shared behaviour for almost all test commands.
1149    ///
1150    /// * Use a temporary cache directory
1151    /// * Use a temporary virtual environment with the Python version of [`Self`]
1152    /// * Don't wrap text output based on the terminal we're in, the test output doesn't get printed
1153    ///   but snapshotted to a string.
1154    /// * Use a fake `HOME` to avoid accidentally changing the developer's machine.
1155    /// * Ignore system configuration to avoid reading machine-specific settings.
1156    /// * Hide other Pythons with `UV_PYTHON_INSTALL_DIR` and installed interpreters with
1157    ///   `UV_PYTHON_SEARCH_PATH` and an active venv (if applicable) by removing `VIRTUAL_ENV`.
1158    /// * Increase the stack size to avoid stack overflows on windows due to large async functions.
1159    pub fn add_shared_options(&self, command: &mut Command, activate_venv: bool) {
1160        self.add_shared_args(command);
1161        self.add_shared_env(command, activate_venv);
1162    }
1163
1164    /// Only the arguments of [`TestContext::add_shared_options`].
1165    fn add_shared_args(&self, command: &mut Command) {
1166        command.arg("--cache-dir").arg(self.cache_dir.path());
1167    }
1168
1169    /// Only the environment variables of [`TestContext::add_shared_options`].
1170    pub fn add_shared_env(&self, command: &mut Command, activate_venv: bool) {
1171        // Push the test context bin to the front of the PATH
1172        let path = env::join_paths(std::iter::once(self.bin_dir.to_path_buf()).chain(
1173            env::split_paths(&env::var(EnvVars::PATH).unwrap_or_default()),
1174        ))
1175        .unwrap();
1176
1177        // Ensure the tests aren't sensitive to the running user's shell without forcing
1178        // `bash` on Windows
1179        if cfg!(not(windows)) {
1180            command.env(EnvVars::SHELL, "bash");
1181        }
1182
1183        command
1184            // When running the tests in a venv, ignore that venv, otherwise we'll capture warnings.
1185            .env_remove(EnvVars::VIRTUAL_ENV)
1186            // Disable wrapping of uv output for readability / determinism in snapshots.
1187            .env(EnvVars::UV_NO_WRAP, "1")
1188            // Avoid reading host system configuration unless a test opts in.
1189            .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1190            // While we disable wrapping in uv above, invoked tools may still wrap their output so
1191            // we set a fixed `COLUMNS` value for isolation from terminal width.
1192            .env(EnvVars::COLUMNS, "100")
1193            .env(EnvVars::PATH, path)
1194            .env(EnvVars::HOME, self.home_dir.as_os_str())
1195            .env(EnvVars::APPDATA, self.home_dir.as_os_str())
1196            .env(EnvVars::USERPROFILE, self.home_dir.as_os_str())
1197            .env(
1198                EnvVars::XDG_CONFIG_DIRS,
1199                self.home_dir.join("config").as_os_str(),
1200            )
1201            .env(
1202                EnvVars::XDG_DATA_HOME,
1203                self.home_dir.join("data").as_os_str(),
1204            )
1205            .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1206            .env(EnvVars::UV_PYTHON_INSTALL_DIR, "")
1207            // Installations are not allowed by default; see `Self::with_managed_python_dirs`
1208            .env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
1209            .env(EnvVars::UV_PYTHON_SEARCH_PATH, self.python_path())
1210            .env(EnvVars::UV_EXCLUDE_NEWER, TEST_TIMESTAMP)
1211            .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, TEST_TIMESTAMP)
1212            .env(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF, TEST_TIMESTAMP)
1213            // Keep Python discovery hermetic and avoid mutating global state, like the Windows
1214            // registry, unless a test opts in explicitly.
1215            .env(EnvVars::UV_PYTHON_NO_REGISTRY, "1")
1216            .env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "0")
1217            // Since downloads, fetches and builds run in parallel, their message output order is
1218            // non-deterministic, so can't capture them in test output.
1219            .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1")
1220            // I believe the intent of all tests is that they are run outside the
1221            // context of an existing git repository. And when they aren't, state
1222            // from the parent git repository can bleed into the behavior of `uv
1223            // init` in a way that makes it difficult to test consistently. By
1224            // setting GIT_CEILING_DIRECTORIES, we specifically prevent git from
1225            // climbing up past the root of our test directory to look for any
1226            // other git repos.
1227            //
1228            // If one wants to write a test specifically targeting uv within a
1229            // pre-existing git repository, then the test should make the parent
1230            // git repo explicitly. The GIT_CEILING_DIRECTORIES here shouldn't
1231            // impact it, since it only prevents git from discovering repositories
1232            // at or above the root.
1233            .env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path())
1234            .current_dir(self.temp_dir.path());
1235
1236        for (key, value) in &self.extra_env {
1237            command.env(key, value);
1238        }
1239
1240        if activate_venv {
1241            command.env(EnvVars::VIRTUAL_ENV, self.venv.as_os_str());
1242        }
1243
1244        if cfg!(unix) {
1245            // Avoid locale issues in tests
1246            command.env(EnvVars::LC_ALL, "C");
1247        }
1248    }
1249
1250    /// Create a `pip compile` command for testing.
1251    pub fn pip_compile(&self) -> Command {
1252        let mut command = self.new_command();
1253        command.arg("pip").arg("compile");
1254        self.add_shared_options(&mut command, true);
1255        command
1256    }
1257
1258    /// Create a `pip compile` command for testing.
1259    pub fn pip_sync(&self) -> Command {
1260        let mut command = self.new_command();
1261        command.arg("pip").arg("sync");
1262        self.add_shared_options(&mut command, true);
1263        command
1264    }
1265
1266    pub fn pip_show(&self) -> Command {
1267        let mut command = self.new_command();
1268        command.arg("pip").arg("show");
1269        self.add_shared_options(&mut command, true);
1270        command
1271    }
1272
1273    /// Create a `pip freeze` command with options shared across scenarios.
1274    pub fn pip_freeze(&self) -> Command {
1275        let mut command = self.new_command();
1276        command.arg("pip").arg("freeze");
1277        self.add_shared_options(&mut command, true);
1278        command
1279    }
1280
1281    /// Create a `pip check` command with options shared across scenarios.
1282    pub fn pip_check(&self) -> Command {
1283        let mut command = self.new_command();
1284        command.arg("pip").arg("check");
1285        self.add_shared_options(&mut command, true);
1286        command
1287    }
1288
1289    pub fn pip_list(&self) -> Command {
1290        let mut command = self.new_command();
1291        command.arg("pip").arg("list");
1292        self.add_shared_options(&mut command, true);
1293        command
1294    }
1295
1296    /// Create a `uv venv` command
1297    pub fn venv(&self) -> Command {
1298        let mut command = self.new_command();
1299        command.arg("venv");
1300        self.add_shared_options(&mut command, false);
1301        command
1302    }
1303
1304    /// Create a `pip install` command with options shared across scenarios.
1305    pub fn pip_install(&self) -> Command {
1306        let mut command = self.new_command();
1307        command.arg("pip").arg("install");
1308        self.add_shared_options(&mut command, true);
1309        command
1310    }
1311
1312    /// Create a `pip uninstall` command with options shared across scenarios.
1313    pub fn pip_uninstall(&self) -> Command {
1314        let mut command = self.new_command();
1315        command.arg("pip").arg("uninstall");
1316        self.add_shared_options(&mut command, true);
1317        command
1318    }
1319
1320    /// Create a `pip tree` command for testing.
1321    pub fn pip_tree(&self) -> Command {
1322        let mut command = self.new_command();
1323        command.arg("pip").arg("tree");
1324        self.add_shared_options(&mut command, true);
1325        command
1326    }
1327
1328    /// Create a `pip debug` command for testing.
1329    pub fn pip_debug(&self) -> Command {
1330        let mut command = self.new_command();
1331        command.arg("pip").arg("debug");
1332        self.add_shared_options(&mut command, true);
1333        command
1334    }
1335
1336    /// Create a `uv help` command with options shared across scenarios.
1337    pub fn help(&self) -> Command {
1338        let mut command = self.new_command();
1339        command.arg("help");
1340        self.add_shared_env(&mut command, false);
1341        command
1342    }
1343
1344    /// Create a `uv init` command with options shared across scenarios and
1345    /// isolated from any git repository that may exist in a parent directory.
1346    pub fn init(&self) -> Command {
1347        let mut command = self.new_command();
1348        command.arg("init");
1349        self.add_shared_options(&mut command, false);
1350        command
1351    }
1352
1353    /// Create a `uv sync` command with options shared across scenarios.
1354    pub fn sync(&self) -> Command {
1355        let mut command = self.new_command();
1356        command.arg("sync");
1357        self.add_shared_options(&mut command, false);
1358        command
1359    }
1360
1361    /// Create a `uv lock` command with options shared across scenarios.
1362    pub fn lock(&self) -> Command {
1363        let mut command = self.new_command();
1364        command.arg("lock");
1365        self.add_shared_options(&mut command, false);
1366        command
1367    }
1368
1369    /// Create a `uv upgrade` command with options shared across scenarios.
1370    pub fn upgrade(&self) -> Command {
1371        let mut command = self.new_command();
1372        command.arg("upgrade");
1373        self.add_shared_options(&mut command, false);
1374        command
1375    }
1376
1377    /// Create a `uv audit` command with options shared across scenarios.
1378    pub fn audit(&self) -> Command {
1379        let mut command = self.new_command();
1380        command.arg("audit");
1381        self.add_shared_options(&mut command, false);
1382        command
1383    }
1384
1385    /// Create a `uv workspace metadata` command with options shared across scenarios.
1386    pub fn workspace_metadata(&self) -> Command {
1387        let mut command = self.new_command();
1388        command.arg("workspace").arg("metadata");
1389        self.add_shared_options(&mut command, false);
1390        command
1391    }
1392
1393    /// Create a `uv workspace dir` command with options shared across scenarios.
1394    pub fn workspace_dir(&self) -> Command {
1395        let mut command = self.new_command();
1396        command.arg("workspace").arg("dir");
1397        self.add_shared_options(&mut command, false);
1398        command
1399    }
1400
1401    /// Create a `uv workspace list` command with options shared across scenarios.
1402    pub fn workspace_list(&self) -> Command {
1403        let mut command = self.new_command();
1404        command.arg("workspace").arg("list");
1405        self.add_shared_options(&mut command, false);
1406        command
1407    }
1408
1409    /// Create a `uv export` command with options shared across scenarios.
1410    pub fn export(&self) -> Command {
1411        let mut command = self.new_command();
1412        command.arg("export");
1413        self.add_shared_options(&mut command, false);
1414        command
1415    }
1416
1417    /// Create a `uv format` command with options shared across scenarios.
1418    pub fn format(&self) -> Command {
1419        let mut command = self.new_command();
1420        command.arg("format");
1421        self.add_shared_options(&mut command, false);
1422        // Override to a more recent date for ruff version resolution
1423        command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1424        command
1425    }
1426
1427    /// Create a `uv check` command with options shared across scenarios.
1428    pub fn check(&self) -> Command {
1429        let mut command = self.new_command();
1430        command.arg("check");
1431        self.add_shared_options(&mut command, false);
1432        // Override to a more recent date for ty version resolution
1433        command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1434        command
1435    }
1436
1437    /// Create a `uv build` command with options shared across scenarios.
1438    pub fn build(&self) -> Command {
1439        let mut command = self.new_command();
1440        command.arg("build");
1441        self.add_shared_options(&mut command, false);
1442        command
1443    }
1444
1445    pub fn version(&self) -> Command {
1446        let mut command = self.new_command();
1447        command.arg("version");
1448        self.add_shared_options(&mut command, false);
1449        command
1450    }
1451
1452    pub fn self_version(&self) -> Command {
1453        let mut command = self.new_command();
1454        command.arg("self").arg("version");
1455        self.add_shared_options(&mut command, false);
1456        command
1457    }
1458
1459    pub fn self_update(&self) -> Command {
1460        let mut command = self.new_command();
1461        command.arg("self").arg("update");
1462        self.add_shared_options(&mut command, false);
1463        command
1464    }
1465
1466    /// Create a `uv publish` command with options shared across scenarios.
1467    pub fn publish(&self) -> Command {
1468        let mut command = self.new_command();
1469        command.arg("publish");
1470        self.add_shared_options(&mut command, false);
1471        command
1472    }
1473
1474    /// Create a `uv python find` command with options shared across scenarios.
1475    pub fn python_find(&self) -> Command {
1476        let mut command = self.new_command();
1477        command
1478            .arg("python")
1479            .arg("find")
1480            .env(EnvVars::UV_PREVIEW, "1")
1481            .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1482        self.add_shared_options(&mut command, false);
1483        command
1484    }
1485
1486    /// Create a `uv python list` command with options shared across scenarios.
1487    pub fn python_list(&self) -> Command {
1488        let mut command = self.new_command();
1489        command
1490            .arg("python")
1491            .arg("list")
1492            .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1493        self.add_shared_options(&mut command, false);
1494        command
1495    }
1496
1497    /// Create a `uv python install` command with options shared across scenarios.
1498    pub fn python_install(&self) -> Command {
1499        let mut command = self.new_command();
1500        command.arg("python").arg("install");
1501        self.add_shared_options(&mut command, true);
1502        command
1503    }
1504
1505    /// Create a `uv python uninstall` command with options shared across scenarios.
1506    pub fn python_uninstall(&self) -> Command {
1507        let mut command = self.new_command();
1508        command.arg("python").arg("uninstall");
1509        self.add_shared_options(&mut command, true);
1510        command
1511    }
1512
1513    /// Create a `uv python upgrade` command with options shared across scenarios.
1514    pub fn python_upgrade(&self) -> Command {
1515        let mut command = self.new_command();
1516        command.arg("python").arg("upgrade");
1517        self.add_shared_options(&mut command, true);
1518        command
1519    }
1520
1521    /// Create a `uv python pin` command with options shared across scenarios.
1522    pub fn python_pin(&self) -> Command {
1523        let mut command = self.new_command();
1524        command.arg("python").arg("pin");
1525        self.add_shared_options(&mut command, true);
1526        command
1527    }
1528
1529    /// Create a `uv python dir` command with options shared across scenarios.
1530    pub fn python_dir(&self) -> Command {
1531        let mut command = self.new_command();
1532        command.arg("python").arg("dir");
1533        self.add_shared_options(&mut command, true);
1534        command
1535    }
1536
1537    /// Create a `uv run` command with options shared across scenarios.
1538    pub fn run(&self) -> Command {
1539        let mut command = self.new_command();
1540        command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1");
1541        self.add_shared_options(&mut command, true);
1542        command
1543    }
1544
1545    /// Create a `uv tool run` command with options shared across scenarios.
1546    pub fn tool_run(&self) -> Command {
1547        let mut command = self.new_command();
1548        command
1549            .arg("tool")
1550            .arg("run")
1551            .env(EnvVars::UV_SHOW_RESOLUTION, "1");
1552        self.add_shared_options(&mut command, false);
1553        command
1554    }
1555
1556    /// Create a `uv upgrade run` command with options shared across scenarios.
1557    pub fn tool_upgrade(&self) -> Command {
1558        let mut command = self.new_command();
1559        command.arg("tool").arg("upgrade");
1560        self.add_shared_options(&mut command, false);
1561        command
1562    }
1563
1564    /// Create a `uv tool install` command with options shared across scenarios.
1565    pub fn tool_install(&self) -> Command {
1566        let mut command = self.new_command();
1567        command.arg("tool").arg("install");
1568        self.add_shared_options(&mut command, false);
1569        command
1570    }
1571
1572    /// Create a `uv tool list` command with options shared across scenarios.
1573    pub fn tool_list(&self) -> Command {
1574        let mut command = self.new_command();
1575        command.arg("tool").arg("list");
1576        self.add_shared_options(&mut command, false);
1577        command
1578    }
1579
1580    /// Create a `uv tool dir` command with options shared across scenarios.
1581    pub fn tool_dir(&self) -> Command {
1582        let mut command = self.new_command();
1583        command.arg("tool").arg("dir");
1584        self.add_shared_options(&mut command, false);
1585        command
1586    }
1587
1588    /// Create a `uv tool uninstall` command with options shared across scenarios.
1589    pub fn tool_uninstall(&self) -> Command {
1590        let mut command = self.new_command();
1591        command.arg("tool").arg("uninstall");
1592        self.add_shared_options(&mut command, false);
1593        command
1594    }
1595
1596    /// Create a `uv add` command for the given requirements.
1597    pub fn add(&self) -> Command {
1598        let mut command = self.new_command();
1599        command.arg("add");
1600        self.add_shared_options(&mut command, false);
1601        command
1602    }
1603
1604    /// Create a `uv remove` command for the given requirements.
1605    pub fn remove(&self) -> Command {
1606        let mut command = self.new_command();
1607        command.arg("remove");
1608        self.add_shared_options(&mut command, false);
1609        command
1610    }
1611
1612    /// Create a `uv tree` command with options shared across scenarios.
1613    pub fn tree(&self) -> Command {
1614        let mut command = self.new_command();
1615        command.arg("tree");
1616        self.add_shared_options(&mut command, false);
1617        command
1618    }
1619
1620    /// Create a `uv cache clean` command.
1621    pub fn clean(&self) -> Command {
1622        let mut command = self.new_command();
1623        command.arg("cache").arg("clean");
1624        self.add_shared_options(&mut command, false);
1625        command
1626    }
1627
1628    /// Create a `uv cache prune` command.
1629    pub fn prune(&self) -> Command {
1630        let mut command = self.new_command();
1631        command.arg("cache").arg("prune");
1632        self.add_shared_options(&mut command, false);
1633        command
1634    }
1635
1636    /// Create a `uv cache size` command.
1637    pub fn cache_size(&self) -> Command {
1638        let mut command = self.new_command();
1639        command.arg("cache").arg("size");
1640        self.add_shared_options(&mut command, false);
1641        command
1642    }
1643
1644    /// Create a `uv build_backend` command.
1645    ///
1646    /// Note that this command is hidden and only invoking it through a build frontend is supported.
1647    pub fn build_backend(&self) -> Command {
1648        let mut command = self.new_command();
1649        command.arg("build-backend");
1650        self.add_shared_options(&mut command, false);
1651        command
1652    }
1653
1654    /// The path to the Python interpreter in the venv.
1655    ///
1656    /// Don't use this for `Command::new`, use `Self::python_command` instead.
1657    pub fn interpreter(&self) -> PathBuf {
1658        let venv = &self.venv;
1659        if cfg!(unix) {
1660            venv.join("bin").join("python")
1661        } else if cfg!(windows) {
1662            venv.join("Scripts").join("python.exe")
1663        } else {
1664            unimplemented!("Only Windows and Unix are supported")
1665        }
1666    }
1667
1668    pub fn python_command(&self) -> Command {
1669        let mut interpreter = self.interpreter();
1670
1671        // If there's not a virtual environment, use the first Python interpreter in the context
1672        if !interpreter.exists() {
1673            interpreter.clone_from(
1674                &self
1675                    .python_versions
1676                    .first()
1677                    .expect("At least one Python version is required")
1678                    .1,
1679            );
1680        }
1681
1682        let mut command = Self::new_command_with(&interpreter);
1683        command
1684            // Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files
1685            // https://github.com/python/cpython/issues/75953
1686            .arg("-B")
1687            // Python on windows
1688            .env(EnvVars::PYTHONUTF8, "1");
1689
1690        self.add_shared_env(&mut command, false);
1691
1692        command
1693    }
1694
1695    /// Create a `uv auth login` command.
1696    pub fn auth_login(&self) -> Command {
1697        let mut command = self.new_command();
1698        command.arg("auth").arg("login");
1699        self.add_shared_options(&mut command, false);
1700        command
1701    }
1702
1703    /// Create a `uv auth logout` command.
1704    pub fn auth_logout(&self) -> Command {
1705        let mut command = self.new_command();
1706        command.arg("auth").arg("logout");
1707        self.add_shared_options(&mut command, false);
1708        command
1709    }
1710
1711    /// Create a `uv auth helper --protocol bazel get` command.
1712    pub fn auth_helper(&self) -> Command {
1713        let mut command = self.new_command();
1714        command.arg("auth").arg("helper");
1715        self.add_shared_options(&mut command, false);
1716        command
1717    }
1718
1719    /// Create a `uv auth token` command.
1720    pub fn auth_token(&self) -> Command {
1721        let mut command = self.new_command();
1722        command.arg("auth").arg("token");
1723        self.add_shared_options(&mut command, false);
1724        command
1725    }
1726
1727    /// Set `HOME` to the real home directory.
1728    ///
1729    /// We need this for testing commands which use the macOS keychain.
1730    #[must_use]
1731    pub fn with_real_home(mut self) -> Self {
1732        if let Some(home) = env::var_os(EnvVars::HOME) {
1733            self.extra_env
1734                .push((EnvVars::HOME.to_string().into(), home));
1735        }
1736        // Use the test's isolated config directory to avoid reading user
1737        // configuration files (like `.python-version`) that could interfere with tests.
1738        self.extra_env.push((
1739            EnvVars::XDG_CONFIG_HOME.into(),
1740            self.user_config_dir.as_os_str().into(),
1741        ));
1742        self
1743    }
1744
1745    /// Run the given python code and check whether it succeeds.
1746    pub fn assert_command(&self, command: &str) -> Assert {
1747        self.python_command()
1748            .arg("-c")
1749            .arg(command)
1750            .current_dir(&self.temp_dir)
1751            .assert()
1752    }
1753
1754    /// Run the given python file and check whether it succeeds.
1755    pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
1756        self.python_command()
1757            .arg(file.as_ref())
1758            .current_dir(&self.temp_dir)
1759            .assert()
1760    }
1761
1762    /// Assert a package is installed with the given version.
1763    pub fn assert_installed(&self, package: &'static str, version: &'static str) {
1764        self.assert_command(
1765            format!("import {package} as package; print(package.__version__, end='')").as_str(),
1766        )
1767        .success()
1768        .stdout(version);
1769    }
1770
1771    /// Assert a package is not installed.
1772    pub fn assert_not_installed(&self, package: &'static str) {
1773        self.assert_command(format!("import {package}").as_str())
1774            .failure();
1775    }
1776
1777    /// Generate various escaped regex patterns for the given path.
1778    pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
1779        let mut patterns = Vec::new();
1780
1781        // We can only canonicalize paths that exist already
1782        if path.as_ref().exists() {
1783            patterns.push(Self::path_pattern(
1784                path.as_ref()
1785                    .canonicalize()
1786                    .expect("Failed to create canonical path"),
1787            ));
1788        }
1789
1790        // Include a non-canonicalized version
1791        patterns.push(Self::path_pattern(path));
1792
1793        patterns
1794    }
1795
1796    /// Generate an escaped regex pattern for the given path.
1797    fn path_pattern(path: impl AsRef<Path>) -> String {
1798        format!(
1799            // Trim the trailing separator for cross-platform directories filters
1800            r"{}\\?/?",
1801            regex::escape(&path.as_ref().simplified_display().to_string())
1802                // Make separators platform agnostic because on Windows we will display
1803                // paths with Unix-style separators sometimes
1804                .replace(r"\\", r"(\\|\/)")
1805        )
1806    }
1807
1808    pub fn python_path(&self) -> OsString {
1809        if cfg!(unix) {
1810            // On Unix, we needed to normalize the Python executable names to `python3` for the tests
1811            env::join_paths(
1812                self.python_versions
1813                    .iter()
1814                    .map(|(version, _)| self.python_dir.join(version.to_string())),
1815            )
1816            .unwrap()
1817        } else {
1818            // On Windows, just join the parent directories of the executables
1819            env::join_paths(
1820                self.python_versions
1821                    .iter()
1822                    .map(|(_, executable)| executable.parent().unwrap().to_path_buf()),
1823            )
1824            .unwrap()
1825        }
1826    }
1827
1828    /// Standard snapshot filters _plus_ those for this test context.
1829    pub fn filters(&self) -> Vec<(&str, &str)> {
1830        // Put test context snapshots before the default filters
1831        // This ensures we don't replace other patterns inside paths from the test context first
1832        self.filters
1833            .iter()
1834            .map(|(p, r)| (p.as_str(), r.as_str()))
1835            .chain(INSTA_FILTERS.iter().copied())
1836            .collect()
1837    }
1838
1839    /// Only the filters added to this test context.
1840    #[cfg(windows)]
1841    pub fn filters_without_standard_filters(&self) -> Vec<(&str, &str)> {
1842        self.filters
1843            .iter()
1844            .map(|(p, r)| (p.as_str(), r.as_str()))
1845            .collect()
1846    }
1847
1848    /// For when we add pypy to the test suite.
1849    pub fn python_kind(&self) -> &'static str {
1850        "python"
1851    }
1852
1853    /// Returns the site-packages folder inside the venv.
1854    pub fn site_packages(&self) -> PathBuf {
1855        site_packages_path(
1856            &self.venv,
1857            &format!(
1858                "{}{}",
1859                self.python_kind(),
1860                self.python_version.as_ref().expect(
1861                    "A Python version must be provided to retrieve the test site packages path"
1862                )
1863            ),
1864        )
1865    }
1866
1867    /// Reset the virtual environment in the test context.
1868    pub fn reset_venv(&self) {
1869        self.create_venv();
1870    }
1871
1872    /// Create a new virtual environment named `.venv` in the test context.
1873    fn create_venv(&self) {
1874        let executable = get_python(
1875            self.python_version
1876                .as_ref()
1877                .expect("A Python version must be provided to create a test virtual environment"),
1878        );
1879        create_venv_from_executable(&self.venv, &self.cache_dir, &executable, &self.uv_bin);
1880    }
1881
1882    /// Copies the files from the ecosystem project given into this text
1883    /// context.
1884    ///
1885    /// This will almost always write at least a `pyproject.toml` into this
1886    /// test context.
1887    ///
1888    /// The given name should correspond to the name of a sub-directory (not a
1889    /// path to it) in the `test/ecosystem` directory.
1890    ///
1891    /// This panics (fails the current test) for any failure.
1892    pub fn copy_ecosystem_project(&self, name: &str) {
1893        let project_dir = PathBuf::from(format!("../../test/ecosystem/{name}"));
1894        self.temp_dir.copy_from(project_dir, &["*"]).unwrap();
1895        // If there is a (gitignore) lockfile, remove it.
1896        if let Err(err) = fs_err::remove_file(self.temp_dir.join("uv.lock")) {
1897            assert_eq!(
1898                err.kind(),
1899                io::ErrorKind::NotFound,
1900                "Failed to remove uv.lock: {err}"
1901            );
1902        }
1903    }
1904
1905    /// Creates a way to compare the changes made to a lock file.
1906    ///
1907    /// This routine starts by copying (not moves) the generated lock file to
1908    /// memory. It then calls the given closure with this test context to get a
1909    /// `Command` and runs the command. The diff between the old lock file and
1910    /// the new one is then returned.
1911    ///
1912    /// This assumes that a lock has already been performed.
1913    pub fn diff_lock(&self, change: impl Fn(&Self) -> Command) -> String {
1914        static TRIM_TRAILING_WHITESPACE: std::sync::LazyLock<Regex> =
1915            std::sync::LazyLock::new(|| Regex::new(r"(?m)^\s+$").unwrap());
1916
1917        let lock_path = ChildPath::new(self.temp_dir.join("uv.lock"));
1918        let old_lock = fs_err::read_to_string(&lock_path).unwrap();
1919        let (snapshot, output) = run_and_format(
1920            change(self),
1921            self.filters(),
1922            "diff_lock",
1923            Some(WindowsFilters::Platform),
1924            None,
1925        );
1926        assert!(output.status.success(), "{snapshot}");
1927        let new_lock = fs_err::read_to_string(&lock_path).unwrap();
1928        diff_snapshot(&old_lock, &new_lock, 10)
1929    }
1930
1931    /// Read a file in the temporary directory
1932    pub fn read(&self, file: impl AsRef<Path>) -> String {
1933        fs_err::read_to_string(self.temp_dir.join(&file))
1934            .unwrap_or_else(|_| panic!("Missing file: `{}`", file.user_display()))
1935    }
1936
1937    /// Creates a new `Command` that is intended to be suitable for use in
1938    /// all tests.
1939    fn new_command(&self) -> Command {
1940        Self::new_command_with(&self.uv_bin)
1941    }
1942
1943    /// Creates a new `Command` that is intended to be suitable for use in
1944    /// all tests, but with the given binary.
1945    ///
1946    /// Clears environment variables defined in [`EnvVars`] to avoid reading
1947    /// test host settings.
1948    fn new_command_with(bin: &Path) -> Command {
1949        let mut command = Command::new(bin);
1950
1951        let passthrough = [
1952            // For linux distributions
1953            EnvVars::PATH,
1954            // For debugging tests.
1955            EnvVars::RUST_LOG,
1956            EnvVars::RUST_BACKTRACE,
1957            // Windows System configuration.
1958            EnvVars::SYSTEMDRIVE,
1959            // Work around small default stack sizes and large futures in debug builds.
1960            EnvVars::RUST_MIN_STACK,
1961            EnvVars::UV_STACK_SIZE,
1962            // Allow running tests with custom network settings.
1963            EnvVars::ALL_PROXY,
1964            EnvVars::HTTPS_PROXY,
1965            EnvVars::HTTP_PROXY,
1966            EnvVars::NO_PROXY,
1967            EnvVars::SSL_CERT_DIR,
1968            EnvVars::SSL_CERT_FILE,
1969            EnvVars::UV_NATIVE_TLS,
1970            EnvVars::UV_SYSTEM_CERTS,
1971        ];
1972
1973        for env_var in EnvVars::all_names()
1974            .iter()
1975            .filter(|name| !passthrough.contains(name))
1976        {
1977            command.env_remove(env_var);
1978        }
1979
1980        command
1981    }
1982}
1983
1984/// Creates a "unified" diff between the two line-oriented strings suitable
1985/// for snapshotting.
1986pub fn diff_snapshot(old: &str, new: &str, context_radius: usize) -> String {
1987    static TRIM_TRAILING_WHITESPACE: std::sync::LazyLock<Regex> =
1988        std::sync::LazyLock::new(|| Regex::new(r"(?m)^\s+$").unwrap());
1989
1990    let diff = similar::TextDiff::from_lines(old, new);
1991    let unified = diff
1992        .unified_diff()
1993        .context_radius(context_radius)
1994        .header("old", "new")
1995        .to_string();
1996    // Not totally clear why, but some lines end up containing only
1997    // whitespace in the diff, even though they don't appear in the
1998    // original data. So just strip them here.
1999    TRIM_TRAILING_WHITESPACE
2000        .replace_all(&unified, "")
2001        .into_owned()
2002}
2003
2004/// Assert a snapshot of the diff between `old` and a command's output.
2005///
2006/// Returns the command's snapshot, this is useful for chaining diffs.
2007#[macro_export]
2008macro_rules! diff_uv_snapshot {
2009    ($filters:expr, $old:expr, $spawnable:expr, @$snapshot:literal) => {{
2010        let new = $crate::capture_uv_snapshot!($filters, $spawnable);
2011        let snapshot = $crate::diff_snapshot($old, &new, 3);
2012        let mut settings = ::insta::Settings::clone_current();
2013        // Show the complete diff on failure while avoiding assertions on its unstable metadata.
2014        let description = match settings.description() {
2015            Some(description) => format!("{description}\n\nUnfiltered diff:\n{snapshot}"),
2016            None => format!("Unfiltered diff:\n{snapshot}"),
2017        };
2018        settings.set_description(description);
2019        settings.add_filter(r"^--- old\n\+\+\+ new\n", "");
2020        settings.add_filter(r"(?m)^@@.*$", "...");
2021        settings.add_filter(r"\n$", "\n...\n");
2022        settings.bind(|| {
2023            ::insta::assert_snapshot!(snapshot, @$snapshot);
2024        });
2025        new
2026    }};
2027}
2028
2029/// Capture a command's output, optionally asserting it against a snapshot.
2030#[macro_export]
2031macro_rules! capture_uv_snapshot {
2032    ($filters:expr, $spawnable:expr) => {{
2033        // Don't echo the output to stderr while capturing without asserting.
2034        let (snapshot, _) = $crate::run_and_format_silent(
2035            $spawnable,
2036            &$filters,
2037            $crate::function_name!(),
2038            Some($crate::WindowsFilters::Platform),
2039            None,
2040        );
2041        snapshot
2042    }};
2043    ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2044        let (snapshot, _) = $crate::run_and_format(
2045            $spawnable,
2046            &$filters,
2047            $crate::function_name!(),
2048            Some($crate::WindowsFilters::Platform),
2049            None,
2050        );
2051        ::insta::assert_snapshot!(snapshot, @$snapshot);
2052        snapshot
2053    }};
2054}
2055
2056pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
2057    if cfg!(unix) {
2058        venv.join("lib").join(python).join("site-packages")
2059    } else if cfg!(windows) {
2060        venv.join("Lib").join("site-packages")
2061    } else {
2062        unimplemented!("Only Windows and Unix are supported")
2063    }
2064}
2065
2066pub fn venv_bin_path(venv: impl AsRef<Path>) -> PathBuf {
2067    if cfg!(unix) {
2068        venv.as_ref().join("bin")
2069    } else if cfg!(windows) {
2070        venv.as_ref().join("Scripts")
2071    } else {
2072        unimplemented!("Only Windows and Unix are supported")
2073    }
2074}
2075
2076/// Get the path to the python interpreter for a specific python version.
2077fn get_python(version: &PythonVersion) -> PathBuf {
2078    ManagedPythonInstallations::from_settings(None)
2079        .map(|installed_pythons| {
2080            installed_pythons
2081                .find_version(version)
2082                .expect("Tests are run on a supported platform")
2083                .next()
2084                .as_ref()
2085                .map(|python| python.executable(false))
2086        })
2087        // We'll search for the request Python on the PATH if not found in the python versions
2088        // We hack this into a `PathBuf` to satisfy the compiler but it's just a string
2089        .unwrap_or_default()
2090        .unwrap_or(PathBuf::from(version.to_string()))
2091}
2092
2093/// Create a virtual environment at the given path.
2094fn create_venv_from_executable<P: AsRef<Path>>(
2095    path: P,
2096    cache_dir: &ChildPath,
2097    python: &Path,
2098    uv_bin: &Path,
2099) {
2100    TestContext::new_command_with(uv_bin)
2101        .arg("venv")
2102        .arg(path.as_ref().as_os_str())
2103        .arg("--clear")
2104        .arg("--cache-dir")
2105        .arg(cache_dir.path())
2106        .arg("--python")
2107        .arg(python)
2108        .current_dir(path.as_ref().parent().unwrap())
2109        .assert()
2110        .success();
2111    ChildPath::new(path.as_ref()).assert(predicate::path::is_dir());
2112}
2113
2114/// Create a `PATH` with the requested Python versions available in order.
2115///
2116/// Generally this should be used with `UV_PYTHON_SEARCH_PATH`.
2117pub fn python_path_with_versions(
2118    temp_dir: &ChildPath,
2119    python_versions: &[&str],
2120) -> anyhow::Result<OsString> {
2121    let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
2122    Ok(env::join_paths(
2123        python_installations_for_versions(temp_dir, python_versions, &download_list)?
2124            .into_iter()
2125            .map(|path| path.parent().unwrap().to_path_buf()),
2126    )?)
2127}
2128
2129/// Returns a list of Python executables for the given versions.
2130///
2131/// Generally this should be used with `UV_PYTHON_SEARCH_PATH`.
2132fn python_installations_for_versions(
2133    temp_dir: &ChildPath,
2134    python_versions: &[&str],
2135    download_list: &ManagedPythonDownloadList,
2136) -> anyhow::Result<Vec<PathBuf>> {
2137    let cache = Cache::from_path(temp_dir.child("cache").to_path_buf())
2138        .init_no_wait()?
2139        .expect("No cache contention when setting up Python in tests");
2140    let _preview = uv_preview::test::with_features(&[]);
2141    let selected_pythons = python_versions
2142        .iter()
2143        .map(|python_version| {
2144            if let Ok(python) = PythonInstallation::find(
2145                &PythonRequest::parse(python_version),
2146                EnvironmentPreference::OnlySystem,
2147                PythonPreference::Managed,
2148                download_list,
2149                &cache,
2150            ) {
2151                python.into_interpreter().sys_executable().to_owned()
2152            } else {
2153                panic!("Could not find Python {python_version} for test\nTry `cargo run python install` first, or refer to CONTRIBUTING.md");
2154            }
2155        })
2156        .collect::<Vec<_>>();
2157
2158    assert!(
2159        python_versions.is_empty() || !selected_pythons.is_empty(),
2160        "Failed to fulfill requested test Python versions: {selected_pythons:?}"
2161    );
2162
2163    Ok(selected_pythons)
2164}
2165
2166#[derive(Debug, Copy, Clone)]
2167pub enum WindowsFilters {
2168    Platform,
2169    Universal,
2170}
2171
2172/// Helper method to apply filters to a string. Useful when `!uv_snapshot` cannot be used.
2173pub fn apply_filters<T: AsRef<str>>(mut snapshot: String, filters: impl AsRef<[(T, T)]>) -> String {
2174    for (matcher, replacement) in filters.as_ref() {
2175        // TODO(konstin): Cache regex compilation
2176        let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?");
2177        if re.is_match(&snapshot) {
2178            snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string();
2179        }
2180    }
2181    snapshot
2182}
2183
2184/// Execute the command and format its output status, stdout and stderr into a snapshot string.
2185///
2186/// This function is derived from `insta_cmd`s `spawn_with_info`.
2187#[expect(clippy::print_stderr)]
2188pub fn run_and_format<T: AsRef<str>>(
2189    command: impl BorrowMut<Command>,
2190    filters: impl AsRef<[(T, T)]>,
2191    function_name: &str,
2192    windows_filters: Option<WindowsFilters>,
2193    input: Option<&str>,
2194) -> (String, Output) {
2195    let (snapshot, output) =
2196        run_and_format_silent(command, filters, function_name, windows_filters, input);
2197    eprintln!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Unfiltered output ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2198    eprintln!(
2199        "----- stdout -----\n{}\n----- stderr -----\n{}",
2200        String::from_utf8_lossy(&output.stdout),
2201        String::from_utf8_lossy(&output.stderr),
2202    );
2203    eprintln!("────────────────────────────────────────────────────────────────────────────────\n");
2204    (snapshot, output)
2205}
2206
2207/// Execute the command and format its output without printing the unfiltered output.
2208#[doc(hidden)]
2209pub fn run_and_format_silent<T: AsRef<str>>(
2210    mut command: impl BorrowMut<Command>,
2211    filters: impl AsRef<[(T, T)]>,
2212    function_name: &str,
2213    windows_filters: Option<WindowsFilters>,
2214    input: Option<&str>,
2215) -> (String, Output) {
2216    let program = command
2217        .borrow_mut()
2218        .get_program()
2219        .to_string_lossy()
2220        .to_string();
2221
2222    // Support profiling test run commands with traces.
2223    if let Ok(root) = env::var(EnvVars::TRACING_DURATIONS_TEST_ROOT) {
2224        // We only want to fail if the variable is set at runtime.
2225        #[expect(clippy::assertions_on_constants)]
2226        {
2227            assert!(
2228                cfg!(feature = "tracing-durations-export"),
2229                "You need to enable the tracing-durations-export feature to use `TRACING_DURATIONS_TEST_ROOT`"
2230            );
2231        }
2232        command.borrow_mut().env(
2233            EnvVars::TRACING_DURATIONS_FILE,
2234            Path::new(&root).join(function_name).with_extension("jsonl"),
2235        );
2236    }
2237
2238    let output = if let Some(input) = input {
2239        let mut child = command
2240            .borrow_mut()
2241            .stdin(Stdio::piped())
2242            .stdout(Stdio::piped())
2243            .stderr(Stdio::piped())
2244            .spawn()
2245            .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"));
2246        child
2247            .stdin
2248            .as_mut()
2249            .expect("Failed to open stdin")
2250            .write_all(input.as_bytes())
2251            .expect("Failed to write to stdin");
2252
2253        child
2254            .wait_with_output()
2255            .unwrap_or_else(|err| panic!("Failed to read output from {program}: {err}"))
2256    } else {
2257        command
2258            .borrow_mut()
2259            .output()
2260            .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"))
2261    };
2262
2263    let mut snapshot = apply_filters(
2264        format!(
2265            "success: {:?}\nexit_code: {}\n----- stdout -----\n{}\n----- stderr -----\n{}",
2266            output.status.success(),
2267            output.status.code().unwrap_or(!0),
2268            String::from_utf8_lossy(&output.stdout),
2269            String::from_utf8_lossy(&output.stderr),
2270        ),
2271        filters,
2272    );
2273
2274    // This is a heuristic filter meant to try and make *most* of our tests
2275    // pass whether it's on Windows or Unix. In particular, there are some very
2276    // common Windows-only dependencies that, when removed from a resolution,
2277    // cause the set of dependencies to be the same across platforms.
2278    if cfg!(windows) {
2279        if let Some(windows_filters) = windows_filters {
2280            // The optional leading +/-/~ is for install logs, the optional next line is for lockfiles
2281            let windows_only_deps = [
2282                (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2283                (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2284                (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2285                (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2286            ];
2287            let mut removed_packages = 0;
2288            for windows_only_dep in windows_only_deps {
2289                // TODO(konstin): Cache regex compilation
2290                let re = Regex::new(windows_only_dep).unwrap();
2291                if re.is_match(&snapshot) {
2292                    snapshot = re.replace(&snapshot, "").to_string();
2293                    removed_packages += 1;
2294                }
2295            }
2296            if removed_packages > 0 {
2297                for i in 1..20 {
2298                    for verb in match windows_filters {
2299                        WindowsFilters::Platform => [
2300                            "Resolved",
2301                            "Prepared",
2302                            "Installed",
2303                            "Checked",
2304                            "Uninstalled",
2305                        ]
2306                        .iter(),
2307                        WindowsFilters::Universal => {
2308                            ["Prepared", "Installed", "Checked", "Uninstalled"].iter()
2309                        }
2310                    } {
2311                        snapshot = snapshot.replace(
2312                            &format!("{verb} {} packages", i + removed_packages),
2313                            &format!("{verb} {} package{}", i, if i > 1 { "s" } else { "" }),
2314                        );
2315                    }
2316                }
2317            }
2318        }
2319    }
2320
2321    (snapshot, output)
2322}
2323
2324/// Recursively copy a directory and its contents, skipping gitignored files.
2325pub fn copy_dir_ignore(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {
2326    for entry in ignore::Walk::new(&src) {
2327        let entry = entry?;
2328        let relative = entry.path().strip_prefix(&src)?;
2329        let ty = entry.file_type().unwrap();
2330        if ty.is_dir() {
2331            fs_err::create_dir(dst.as_ref().join(relative))?;
2332        } else {
2333            fs_err::copy(entry.path(), dst.as_ref().join(relative))?;
2334        }
2335    }
2336    Ok(())
2337}
2338
2339/// Create a stub package `name` in `dir` with the given `pyproject.toml` body.
2340pub fn make_project(dir: &Path, name: &str, body: &str) -> anyhow::Result<()> {
2341    let pyproject_toml = formatdoc! {r#"
2342        [project]
2343        name = "{name}"
2344        version = "0.1.0"
2345        requires-python = ">=3.11,<3.13"
2346        {body}
2347
2348        [build-system]
2349        requires = ["uv_build>=0.9.0,<10000"]
2350        build-backend = "uv_build"
2351        "#
2352    };
2353    fs_err::create_dir_all(dir)?;
2354    fs_err::write(dir.join("pyproject.toml"), pyproject_toml)?;
2355    fs_err::create_dir_all(dir.join("src").join(name))?;
2356    fs_err::write(dir.join("src").join(name).join("__init__.py"), "")?;
2357    Ok(())
2358}
2359
2360// This is a fine-grained token that only has read-only access to the `uv-private-pypackage` repository
2361pub const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
2362    "Z2l0aHViCg==",
2363    "cGF0Cg==",
2364    "MTFBQlVDUjZBMERMUTQ3aVphN3hPdV9qQmhTMkZUeHZ4ZE13OHczakxuZndsV2ZlZjc2cE53eHBWS2tiRUFwdnpmUk8zV0dDSUhicDFsT01aago=",
2365];
2366
2367// This is a fine-grained token that only has read-only access to the `uv-private-pypackage-2` repository
2368#[cfg(not(windows))]
2369pub const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[
2370    "Z2l0aHViCg==",
2371    "cGF0Cg==",
2372    "MTFBQlVDUjZBMDJTOFYwMTM4YmQ0bV9uTXpueWhxZDBrcllROTQ5SERTeTI0dENKZ2lmdzIybDFSR2s1SE04QW8xTUVYQ1I0Q1YxYUdPRGpvZQo=",
2373];
2374
2375pub const READ_ONLY_GITHUB_SSH_DEPLOY_KEY: &str = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNBeTF1SnNZK1JXcWp1NkdIY3Z6a3AwS21yWDEwdmo3RUZqTkpNTkRqSGZPZ0FBQUpqWUpwVnAyQ2FWCmFRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQXkxdUpzWStSV3FqdTZHSGN2emtwMEttclgxMHZqN0VGak5KTU5EakhmT2cKQUFBRUMwbzBnd1BxbGl6TFBJOEFXWDVaS2dVZHJyQ2ptMDhIQm9FenB4VDg3MXBqTFc0bXhqNUZhcU83b1lkeS9PU25RcQphdGZYUytQc1FXTTBrdzBPTWQ4NkFBQUFFR3R2Ym5OMGFVQmhjM1J5WVd3dWMyZ0JBZ01FQlE9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K";
2376
2377/// Decode a split, base64 encoded authentication token.
2378/// We split and encode the token to bypass revoke by GitHub's secret scanning
2379pub fn decode_token(content: &[&str]) -> String {
2380    content
2381        .iter()
2382        .map(|part| base64.decode(part).unwrap())
2383        .map(|decoded| {
2384            std::str::from_utf8(decoded.as_slice())
2385                .unwrap()
2386                .trim_end()
2387                .to_string()
2388        })
2389        .join("_")
2390}
2391
2392/// Simulates `reqwest::blocking::get` but returns bytes directly, and disables
2393/// certificate verification, passing through the `BaseClient`
2394#[tokio::main(flavor = "current_thread")]
2395pub async fn download_to_disk(url: &str, path: &Path) {
2396    let trusted_hosts: Vec<_> = env::var(EnvVars::UV_INSECURE_HOST)
2397        .unwrap_or_default()
2398        .split(' ')
2399        .map(|h| uv_configuration::TrustedHost::from_str(h).unwrap())
2400        .collect();
2401
2402    let client = uv_client::BaseClientBuilder::default()
2403        .allow_insecure_host(trusted_hosts)
2404        .build()
2405        .expect("failed to build base client");
2406    let url = url.parse().unwrap();
2407    let response = client
2408        .for_host(&url)
2409        .get(reqwest::Url::from(url))
2410        .send()
2411        .await
2412        .unwrap();
2413
2414    let mut file = fs_err::tokio::File::create(path).await.unwrap();
2415    let mut stream = response.bytes_stream();
2416    while let Some(chunk) = stream.next().await {
2417        file.write_all(&chunk.unwrap()).await.unwrap();
2418    }
2419    file.sync_all().await.unwrap();
2420}
2421
2422/// A guard that sets a directory to read-only and restores original permissions when dropped.
2423///
2424/// This is useful for tests that need to make a directory read-only and ensure
2425/// the permissions are restored even if the test panics.
2426#[cfg(unix)]
2427pub struct ReadOnlyDirectoryGuard {
2428    path: PathBuf,
2429    original_mode: u32,
2430}
2431
2432#[cfg(unix)]
2433impl ReadOnlyDirectoryGuard {
2434    /// Sets the directory to read-only (removes write permission) and returns a guard
2435    /// that will restore the original permissions when dropped.
2436    pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
2437        use std::os::unix::fs::PermissionsExt;
2438        let path = path.into();
2439        let metadata = fs_err::metadata(&path)?;
2440        let original_mode = metadata.permissions().mode();
2441        // Remove write permissions (keep read and execute)
2442        let readonly_mode = original_mode & !0o222;
2443        fs_err::set_permissions(&path, std::fs::Permissions::from_mode(readonly_mode))?;
2444        Ok(Self {
2445            path,
2446            original_mode,
2447        })
2448    }
2449}
2450
2451#[cfg(unix)]
2452impl Drop for ReadOnlyDirectoryGuard {
2453    fn drop(&mut self) {
2454        use std::os::unix::fs::PermissionsExt;
2455        let _ = fs_err::set_permissions(
2456            &self.path,
2457            std::fs::Permissions::from_mode(self.original_mode),
2458        );
2459    }
2460}
2461
2462/// Utility macro to return the name of the current function.
2463///
2464/// https://stackoverflow.com/a/40234666/3549270
2465#[doc(hidden)]
2466#[macro_export]
2467macro_rules! function_name {
2468    () => {{
2469        fn f() {}
2470        fn type_name_of_val<T>(_: T) -> &'static str {
2471            std::any::type_name::<T>()
2472        }
2473        let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
2474        while let Some(rest) = name.strip_suffix("::{{closure}}") {
2475            name = rest;
2476        }
2477        name
2478    }};
2479}
2480
2481/// Run [`assert_cmd_snapshot!`], with default filters or with custom filters.
2482///
2483/// By default, the filters will search for the generally windows-only deps colorama and tzdata,
2484/// filter them out and decrease the package counts by one for each match.
2485#[macro_export]
2486macro_rules! uv_snapshot {
2487    ($spawnable:expr, @$snapshot:literal) => {{
2488        uv_snapshot!($crate::INSTA_FILTERS.to_vec(), $spawnable, @$snapshot)
2489    }};
2490    ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2491        // Take a reference for backwards compatibility with the vec-expecting insta filters.
2492        let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), None);
2493        ::insta::assert_snapshot!(snapshot, @$snapshot);
2494        output
2495    }};
2496    ($filters:expr, $spawnable:expr, input=$input:expr, @$snapshot:literal) => {{
2497        // Take a reference for backwards compatibility with the vec-expecting insta filters.
2498        let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), Some($input));
2499        ::insta::assert_snapshot!(snapshot, @$snapshot);
2500        output
2501    }};
2502    ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{
2503        // Take a reference for backwards compatibility with the vec-expecting insta filters.
2504        let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), None, None);
2505        ::insta::assert_snapshot!(snapshot, @$snapshot);
2506        output
2507    }};
2508    ($filters:expr, universal_windows_filters=true, $spawnable:expr, @$snapshot:literal) => {{
2509        // Take a reference for backwards compatibility with the vec-expecting insta filters.
2510        let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Universal), None);
2511        ::insta::assert_snapshot!(snapshot, @$snapshot);
2512        output
2513    }};
2514}