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