1#![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
41static TEST_TIMESTAMP: &str = "2024-03-25T00:00:00Z";
43
44pub const DEFAULT_PYTHON_VERSION: &str = "3.12";
45
46const LATEST_PYTHON_3_15: &str = "3.15.0b4";
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#[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#[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#[macro_export]
91macro_rules! get_bin {
92 () => {
93 std::path::PathBuf::from(env!("CARGO_BIN_EXE_uv"))
94 };
95}
96
97#[doc(hidden)] pub const INSTA_FILTERS: &[(&str, &str)] = &[
99 (r"--cache-dir [^\s]+", "--cache-dir [CACHE_DIR]"),
100 (r"(\s|\()(\d+m )?(\d+\.)?\d+(ms|s)", "$1[TIME]"),
102 (r"(\s|\()(\d+\.)?\d+([KM]i)?B", "$1[SIZE]"),
104 (r"tv_sec: \d+", "tv_sec: [TIME]"),
106 (r"tv_nsec: \d+", "tv_nsec: [TIME]"),
107 (r"\\([\w\d]|\.)", "/$1"),
109 (r"uv\.exe", "uv"),
110 (
112 r"uv(-.*)? \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?( \([^)]*\))?",
113 r"uv [VERSION] ([COMMIT] DATE)",
114 ),
115 (r"([^\s])[ \t]+(\r?\n)", "$1$2"),
117 (r"DEBUG Loaded \d+ certificate\(s\) from [^\n]+\n", ""),
119];
120
121pub 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 python_version: Option<PythonVersion>,
140
141 pub python_versions: Vec<(PythonVersion, PathBuf)>,
143
144 uv_bin: PathBuf,
146
147 filters: Vec<(String, String)>,
149
150 extra_env: Vec<(OsString, OsString)>,
152
153 #[allow(dead_code)]
154 _root: tempfile::TempDir,
155
156 #[allow(dead_code)]
159 _extra_tempdirs: Vec<tempfile::TempDir>,
160}
161
162impl TestContext {
163 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 #[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 #[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 #[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 #[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 #[must_use]
225 pub fn with_filtered_cache_size(mut self) -> Self {
226 self.filters
228 .push((r"(?m)^\d+\n".to_string(), "[SIZE]\n".to_string()));
229 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 #[must_use]
239 pub fn with_filtered_centralized_environment_hashes(mut self) -> Self {
240 self.filters.push((
241 r"`([\w.\[\]-]+)-[a-f0-9]{16}`".to_string(),
242 "`$1-[HASH]`".to_string(),
243 ));
244 self
245 }
246
247 #[must_use]
249 pub fn with_filtered_missing_file_error(mut self) -> Self {
250 self.filters.push((
253 r"[^:\n]* \(os error 2\)".to_string(),
254 " [OS ERROR 2]".to_string(),
255 ));
256 self.filters.push((
260 r"[^:\n]* \(os error 3\)".to_string(),
261 " [OS ERROR 2]".to_string(),
262 ));
263 self
264 }
265
266 #[must_use]
269 pub fn with_filtered_exe_suffix(mut self) -> Self {
270 self.filters
271 .push((regex::escape(env::consts::EXE_SUFFIX), String::new()));
272 self
273 }
274
275 #[must_use]
277 pub fn with_filtered_python_sources(mut self) -> Self {
278 self.filters.push((
279 "virtual environments, managed installations, or search path".to_string(),
280 "[PYTHON SOURCES]".to_string(),
281 ));
282 self.filters.push((
283 "virtual environments, managed installations, search path, or registry".to_string(),
284 "[PYTHON SOURCES]".to_string(),
285 ));
286 self.filters.push((
287 "virtual environments, search path, or registry".to_string(),
288 "[PYTHON SOURCES]".to_string(),
289 ));
290 self.filters.push((
291 "virtual environments, registry, or search path".to_string(),
292 "[PYTHON SOURCES]".to_string(),
293 ));
294 self.filters.push((
295 "virtual environments or search path".to_string(),
296 "[PYTHON SOURCES]".to_string(),
297 ));
298 self.filters.push((
299 "managed installations or search path".to_string(),
300 "[PYTHON SOURCES]".to_string(),
301 ));
302 self.filters.push((
303 "managed installations, search path, or registry".to_string(),
304 "[PYTHON SOURCES]".to_string(),
305 ));
306 self.filters.push((
307 "search path or registry".to_string(),
308 "[PYTHON SOURCES]".to_string(),
309 ));
310 self.filters.push((
311 "registry or search path".to_string(),
312 "[PYTHON SOURCES]".to_string(),
313 ));
314 self.filters
315 .push(("search path".to_string(), "[PYTHON SOURCES]".to_string()));
316 self
317 }
318
319 #[must_use]
322 pub fn with_filtered_python_names(mut self) -> Self {
323 for name in ["python", "pypy"] {
324 let suffix = if cfg!(windows) {
327 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
331 format!(r"(\d\.\d+|\d)?{exe_suffix}")
332 } else {
333 if name == "python" {
335 r"(\d\.\d+|\d)?(t|d|td)?".to_string()
337 } else {
338 r"(\d\.\d+|\d)(t|d|td)?".to_string()
340 }
341 };
342
343 self.filters.push((
344 format!(r"[\\/]{name}{suffix}"),
347 format!("/[{}]", name.to_uppercase()),
348 ));
349 }
350
351 self
352 }
353
354 #[must_use]
357 pub fn with_filtered_virtualenv_bin(mut self) -> Self {
358 self.filters.push((
359 format!(
360 r"[\\/]{}[\\/]",
361 venv_bin_path(PathBuf::new()).to_string_lossy()
362 ),
363 "/[BIN]/".to_string(),
364 ));
365 self.filters.push((
366 format!(r"[\\/]{}", venv_bin_path(PathBuf::new()).to_string_lossy()),
367 "/[BIN]".to_string(),
368 ));
369 self
370 }
371
372 #[must_use]
376 pub fn with_filtered_python_install_bin(mut self) -> Self {
377 let suffix = if cfg!(windows) {
380 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
381 format!(r"(\d\.\d+|\d)?{exe_suffix}")
383 } else {
384 r"\d\.\d+|\d".to_string()
386 };
387
388 if cfg!(unix) {
389 self.filters.push((
390 format!(r"[\\/]bin/python({suffix})"),
391 "/[INSTALL-BIN]/python$1".to_string(),
392 ));
393 self.filters.push((
394 format!(r"[\\/]bin/pypy({suffix})"),
395 "/[INSTALL-BIN]/pypy$1".to_string(),
396 ));
397 } else {
398 self.filters.push((
399 format!(r"[\\/]python({suffix})"),
400 "/[INSTALL-BIN]/python$1".to_string(),
401 ));
402 self.filters.push((
403 format!(r"[\\/]pypy({suffix})"),
404 "/[INSTALL-BIN]/pypy$1".to_string(),
405 ));
406 }
407 self
408 }
409
410 #[must_use]
415 pub fn with_pyvenv_cfg_filters(mut self) -> Self {
416 let added_filters = [
417 (r"home = .+".to_string(), "home = [PYTHON_HOME]".to_string()),
418 (
419 r"uv = \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?".to_string(),
420 "uv = [UV_VERSION]".to_string(),
421 ),
422 ];
423 for filter in added_filters {
424 self.filters.insert(0, filter);
425 }
426 self
427 }
428
429 #[must_use]
432 pub fn with_filtered_python_symlinks(mut self) -> Self {
433 for (version, executable) in &self.python_versions {
434 if fs_err::symlink_metadata(executable).unwrap().is_symlink() {
435 self.filters.extend(
436 Self::path_patterns(executable.read_link().unwrap())
437 .into_iter()
438 .map(|pattern| (format! {" -> {pattern}"}, String::new())),
439 );
440 }
441 self.filters.push((
443 regex::escape(&format!(" -> [PYTHON-{version}]")),
444 String::new(),
445 ));
446 }
447 self
448 }
449
450 #[must_use]
452 pub fn with_filtered_path(mut self, path: &Path, name: &str) -> Self {
453 for pattern in Self::path_patterns(path)
457 .into_iter()
458 .map(|pattern| (pattern, format!("[{name}]/")))
459 {
460 self.filters.insert(0, pattern);
461 }
462 self
463 }
464
465 #[inline]
473 #[must_use]
474 pub fn with_filtered_link_mode_warning(mut self) -> Self {
475 let pattern = "warning: Failed to hardlink files; .*\n.*\n.*\n";
476 self.filters.push((pattern.to_string(), String::new()));
477 self
478 }
479
480 #[inline]
482 #[must_use]
483 pub fn with_filtered_not_executable(mut self) -> Self {
484 let pattern = if cfg!(unix) {
485 r"Permission denied \(os error 13\)"
486 } else {
487 r"\%1 is not a valid Win32 application. \(os error 193\)"
488 };
489 self.filters
490 .push((pattern.to_string(), "[PERMISSION DENIED]".to_string()));
491 self
492 }
493
494 #[must_use]
496 pub fn with_filtered_python_keys(mut self) -> Self {
497 let platform_re = r"(?x)
499 ( # We capture the group before the platform
500 (?:cpython|pypy|graalpy)# Python implementation
501 -
502 \d+\.\d+ # Major and minor version
503 (?: # The patch version is handled separately
504 \.
505 (?:
506 \[X\] # A previously filtered patch version [X]
507 | # OR
508 \[LATEST\] # A previously filtered latest patch version [LATEST]
509 | # OR
510 \d+ # An actual patch version
511 )
512 )? # (we allow the patch version to be missing entirely, e.g., in a request)
513 (?:(?:a|b|rc)[0-9]+)? # Pre-release version component, e.g., `a6` or `rc2`
514 (?:[td])? # A short variant, such as `t` (for freethreaded) or `d` (for debug)
515 (?:(\+[a-z]+)+)? # A long variant, such as `+freethreaded` or `+freethreaded+debug`
516 )
517 -
518 [a-z0-9]+ # Operating system (e.g., 'macos')
519 -
520 [a-z0-9_]+ # Architecture (e.g., 'aarch64')
521 -
522 [a-z]+ # Libc (e.g., 'none')
523";
524 self.filters
525 .push((platform_re.to_string(), "$1-[PLATFORM]".to_string()));
526 self
527 }
528
529 #[must_use]
531 pub fn with_filtered_latest_python_versions(mut self) -> Self {
532 for (minor, patch) in [
535 ("3.15", LATEST_PYTHON_3_15.strip_prefix("3.15.").unwrap()),
536 ("3.14", LATEST_PYTHON_3_14.strip_prefix("3.14.").unwrap()),
537 ("3.13", LATEST_PYTHON_3_13.strip_prefix("3.13.").unwrap()),
538 ("3.12", LATEST_PYTHON_3_12.strip_prefix("3.12.").unwrap()),
539 ("3.11", LATEST_PYTHON_3_11.strip_prefix("3.11.").unwrap()),
540 ("3.10", LATEST_PYTHON_3_10.strip_prefix("3.10.").unwrap()),
541 ] {
542 let pattern = format!(r"(\b){minor}\.{patch}(\b)");
544 let replacement = format!("${{1}}{minor}.[LATEST]${{2}}");
545 self.filters.push((pattern, replacement));
546 }
547 self
548 }
549
550 #[must_use]
552 #[cfg(windows)]
553 pub fn with_filtered_windows_temp_dir(mut self) -> Self {
554 let pattern = regex::escape(
555 &self
556 .temp_dir
557 .simplified_display()
558 .to_string()
559 .replace('/', "\\"),
560 );
561 self.filters.push((pattern, "[TEMP_DIR]".to_string()));
562 self
563 }
564
565 #[must_use]
567 pub fn with_filtered_compiled_file_count(mut self) -> Self {
568 self.filters.push((
569 r"compiled \d+ files".to_string(),
570 "compiled [COUNT] files".to_string(),
571 ));
572 self
573 }
574
575 #[must_use]
577 pub fn with_filtered_current_version(mut self) -> Self {
578 self.filters.push((
579 regex::escape(&format!("v{}", env!("CARGO_PKG_VERSION"))),
580 "v[CURRENT_VERSION]".to_string(),
581 ));
582 self
583 }
584
585 #[must_use]
587 pub fn with_cyclonedx_filters(mut self) -> Self {
588 self.filters.push((
589 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(),
590 "[SERIAL_NUMBER]".to_string(),
591 ));
592 self.filters.push((
593 r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#
594 .to_string(),
595 r#""timestamp": "[TIMESTAMP]""#.to_string(),
596 ));
597 self.filters.push((
598 r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#
599 .to_string(),
600 r#""name": "uv",
601 "version": "[VERSION]""#
602 .to_string(),
603 ));
604 self
605 }
606
607 #[must_use]
609 pub fn with_collapsed_whitespace(mut self) -> Self {
610 self.filters.push((r"[ \t]+".to_string(), " ".to_string()));
611 self
612 }
613
614 #[must_use]
616 pub fn with_python_download_cache(mut self) -> Self {
617 self.extra_env.push((
618 EnvVars::UV_PYTHON_CACHE_DIR.into(),
619 env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).unwrap_or_else(|| {
621 uv_cache::Cache::from_settings(false, None)
622 .unwrap()
623 .bucket(CacheBucket::Python)
624 .into()
625 }),
626 ));
627 self
628 }
629
630 #[must_use]
631 pub fn with_empty_python_install_mirror(mut self) -> Self {
632 self.extra_env.push((
633 EnvVars::UV_PYTHON_INSTALL_MIRROR.into(),
634 String::new().into(),
635 ));
636 self
637 }
638
639 #[must_use]
641 pub fn with_managed_python_dirs(mut self) -> Self {
642 let managed = self.temp_dir.join("managed");
643
644 self.extra_env.push((
645 EnvVars::UV_PYTHON_BIN_DIR.into(),
646 self.bin_dir.as_os_str().to_owned(),
647 ));
648 self.extra_env
649 .push((EnvVars::UV_PYTHON_INSTALL_DIR.into(), managed.into()));
650 self.extra_env
651 .push((EnvVars::UV_PYTHON_DOWNLOADS.into(), "automatic".into()));
652
653 self
654 }
655
656 #[must_use]
657 pub fn with_versions_as_managed(mut self, versions: &[&str]) -> Self {
658 self.extra_env.push((
659 EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED.into(),
660 versions.iter().join(" ").into(),
661 ));
662
663 self
664 }
665
666 #[must_use]
668 pub fn with_filter(mut self, filter: (impl Into<String>, impl Into<String>)) -> Self {
669 self.filters.push((filter.0.into(), filter.1.into()));
670 self
671 }
672
673 #[must_use]
675 pub fn with_unset_git_credential_helper(self) -> Self {
676 let git_config = self.home_dir.child(".gitconfig");
677 git_config
678 .write_str(indoc! {r"
679 [credential]
680 helper =
681 "})
682 .expect("Failed to unset git credential helper");
683
684 self
685 }
686
687 #[must_use]
689 #[cfg(windows)]
690 pub fn clear_filters(mut self) -> Self {
691 self.filters.clear();
692 self
693 }
694
695 pub fn with_cache_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
700 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
701 return Ok(None);
702 };
703 self.with_cache_on_fs(&dir, "COW_FS").map(Some)
704 }
705
706 pub fn with_cache_on_alt_fs(self) -> anyhow::Result<Option<Self>> {
711 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_ALT_FS).ok() else {
712 return Ok(None);
713 };
714 self.with_cache_on_fs(&dir, "ALT_FS").map(Some)
715 }
716
717 pub fn with_cache_on_lowlinks_fs(self) -> anyhow::Result<Option<Self>> {
722 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_LOWLINKS_FS).ok() else {
723 return Ok(None);
724 };
725 self.with_cache_on_fs(&dir, "LOWLINKS_FS").map(Some)
726 }
727
728 pub fn with_cache_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
733 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
734 return Ok(None);
735 };
736 self.with_cache_on_fs(&dir, "NOCOW_FS").map(Some)
737 }
738
739 pub fn with_working_dir_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
746 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
747 return Ok(None);
748 };
749 self.with_working_dir_on_fs(&dir, "COW_FS").map(Some)
750 }
751
752 pub fn with_working_dir_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
759 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
760 return Ok(None);
761 };
762 self.with_working_dir_on_fs(&dir, "NOCOW_FS").map(Some)
763 }
764
765 fn with_cache_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
766 fs_err::create_dir_all(dir)?;
767 let tmp = tempfile::TempDir::new_in(dir)?;
768 self.cache_dir = ChildPath::new(tmp.path()).child("cache");
769 fs_err::create_dir_all(&self.cache_dir)?;
770 let replacement = format!("[{name}]/[CACHE_DIR]/");
771 self.filters.extend(
772 Self::path_patterns(&self.cache_dir)
773 .into_iter()
774 .map(|pattern| (pattern, replacement.clone())),
775 );
776 self._extra_tempdirs.push(tmp);
777 Ok(self)
778 }
779
780 fn with_working_dir_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
781 fs_err::create_dir_all(dir)?;
782 let tmp = tempfile::TempDir::new_in(dir)?;
783 self.temp_dir = ChildPath::new(tmp.path()).child("temp");
784 fs_err::create_dir_all(&self.temp_dir)?;
785 let canonical_temp_dir = self.temp_dir.canonicalize()?;
788 self.venv = ChildPath::new(canonical_temp_dir.join(".venv"));
789 let temp_replacement = format!("[{name}]/[TEMP_DIR]/");
790 self.filters.extend(
791 Self::path_patterns(&self.temp_dir)
792 .into_iter()
793 .map(|pattern| (pattern, temp_replacement.clone())),
794 );
795 let venv_replacement = format!("[{name}]/[VENV]/");
796 self.filters.extend(
797 Self::path_patterns(&self.venv)
798 .into_iter()
799 .map(|pattern| (pattern, venv_replacement.clone())),
800 );
801 self._extra_tempdirs.push(tmp);
802 Ok(self)
803 }
804
805 pub fn test_bucket_dir() -> PathBuf {
814 std::env::temp_dir()
815 .simple_canonicalize()
816 .expect("failed to canonicalize temp dir")
817 .join("uv")
818 .join("tests")
819 }
820
821 pub fn new_with_versions_and_bin(python_versions: &[&str], uv_bin: PathBuf) -> Self {
828 let bucket = Self::test_bucket_dir();
829 fs_err::create_dir_all(&bucket).expect("Failed to create test bucket");
830
831 let root = tempfile::TempDir::new_in(bucket).expect("Failed to create test root directory");
832
833 fs_err::create_dir_all(root.path().join(".git"))
836 .expect("Failed to create `.git` placeholder in test root directory");
837
838 let temp_dir = ChildPath::new(root.path()).child("temp");
839 fs_err::create_dir_all(&temp_dir).expect("Failed to create test working directory");
840
841 let cache_dir = ChildPath::new(root.path()).child("cache");
842 fs_err::create_dir_all(&cache_dir).expect("Failed to create test cache directory");
843
844 let python_dir = ChildPath::new(root.path()).child("python");
845 fs_err::create_dir_all(&python_dir).expect("Failed to create test Python directory");
846
847 let bin_dir = ChildPath::new(root.path()).child("bin");
848 fs_err::create_dir_all(&bin_dir).expect("Failed to create test bin directory");
849
850 if cfg!(not(feature = "git")) {
852 Self::disallow_git_cli(&bin_dir).expect("Failed to setup disallowed `git` command");
853 }
854
855 let home_dir = ChildPath::new(root.path()).child("home");
856 fs_err::create_dir_all(&home_dir).expect("Failed to create test home directory");
857
858 let user_config_dir = if cfg!(windows) {
859 ChildPath::new(home_dir.path())
860 } else {
861 ChildPath::new(home_dir.path()).child(".config")
862 };
863
864 let canonical_temp_dir = temp_dir.canonicalize().unwrap();
866 let venv = ChildPath::new(canonical_temp_dir.join(".venv"));
867
868 let python_version = python_versions
869 .first()
870 .map(|version| PythonVersion::from_str(version).unwrap());
871
872 let site_packages = python_version
873 .as_ref()
874 .map(|version| site_packages_path(&venv, &format!("python{version}")));
875
876 let workspace_root = Path::new(&env::var(EnvVars::CARGO_MANIFEST_DIR).unwrap())
879 .parent()
880 .expect("CARGO_MANIFEST_DIR should be nested in workspace")
881 .parent()
882 .expect("CARGO_MANIFEST_DIR should be doubly nested in workspace")
883 .to_path_buf();
884
885 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
886
887 let python_versions: Vec<_> = python_versions
888 .iter()
889 .map(|version| PythonVersion::from_str(version).unwrap())
890 .zip(
891 python_installations_for_versions(&temp_dir, python_versions, &download_list)
892 .expect("Failed to find test Python versions"),
893 )
894 .collect();
895
896 if cfg!(unix) {
899 for (version, executable) in &python_versions {
900 let parent = python_dir.child(version.to_string());
901 parent.create_dir_all().unwrap();
902 parent.child("python3").symlink_to_file(executable).unwrap();
903 }
904 }
905
906 let mut filters = Vec::new();
907
908 filters.extend(
909 Self::path_patterns(&uv_bin)
910 .into_iter()
911 .map(|pattern| (pattern, "[UV]".to_string())),
912 );
913
914 if cfg!(windows) {
916 filters.push((" --link-mode <LINK_MODE>".to_string(), String::new()));
917 filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new()));
918 filters.push((r"exit code: ".to_string(), "exit status: ".to_string()));
920 }
921
922 for (version, executable) in &python_versions {
923 filters.extend(
925 Self::path_patterns(executable)
926 .into_iter()
927 .map(|pattern| (pattern, format!("[PYTHON-{version}]"))),
928 );
929
930 filters.extend(
932 Self::path_patterns(python_dir.join(version.to_string()))
933 .into_iter()
934 .map(|pattern| {
935 (
936 format!("{pattern}[a-zA-Z0-9]*"),
937 format!("[PYTHON-{version}]"),
938 )
939 }),
940 );
941
942 if version.patch().is_none() {
945 filters.push((
946 format!(r"({})\.\d+", regex::escape(version.to_string().as_str())),
947 "$1.[X]".to_string(),
948 ));
949 }
950 }
951
952 filters.extend(
953 Self::path_patterns(&bin_dir)
954 .into_iter()
955 .map(|pattern| (pattern, "[BIN]/".to_string())),
956 );
957 filters.extend(
958 Self::path_patterns(&cache_dir)
959 .into_iter()
960 .map(|pattern| (pattern, "[CACHE_DIR]/".to_string())),
961 );
962 if let Some(ref site_packages) = site_packages {
963 filters.extend(
964 Self::path_patterns(site_packages)
965 .into_iter()
966 .map(|pattern| (pattern, "[SITE_PACKAGES]/".to_string())),
967 );
968 }
969 filters.extend(
970 Self::path_patterns(&venv)
971 .into_iter()
972 .map(|pattern| (pattern, "[VENV]/".to_string())),
973 );
974
975 if let Some(site_packages) = site_packages {
977 filters.push((
978 Self::path_pattern(
979 site_packages
980 .strip_prefix(&canonical_temp_dir)
981 .expect("The test site-packages directory is always in the tempdir"),
982 ),
983 "[SITE_PACKAGES]/".to_string(),
984 ));
985 }
986
987 filters.push((
989 r"[\\/]lib[\\/]python\d+\.\d+[\\/]".to_string(),
990 "/[PYTHON-LIB]/".to_string(),
991 ));
992 filters.push((r"[\\/]Lib[\\/]".to_string(), "/[PYTHON-LIB]/".to_string()));
993
994 filters.extend(
995 Self::path_patterns(&temp_dir)
996 .into_iter()
997 .map(|pattern| (pattern, "[TEMP_DIR]/".to_string())),
998 );
999 filters.extend(
1000 Self::path_patterns(&python_dir)
1001 .into_iter()
1002 .map(|pattern| (pattern, "[PYTHON_DIR]/".to_string())),
1003 );
1004 let mut uv_user_config_dir = PathBuf::from(user_config_dir.path());
1005 uv_user_config_dir.push("uv");
1006 filters.extend(
1007 Self::path_patterns(&uv_user_config_dir)
1008 .into_iter()
1009 .map(|pattern| (pattern, "[UV_USER_CONFIG_DIR]/".to_string())),
1010 );
1011 filters.extend(
1012 Self::path_patterns(&user_config_dir)
1013 .into_iter()
1014 .map(|pattern| (pattern, "[USER_CONFIG_DIR]/".to_string())),
1015 );
1016 filters.extend(
1017 Self::path_patterns(&home_dir)
1018 .into_iter()
1019 .map(|pattern| (pattern, "[HOME]/".to_string())),
1020 );
1021 filters.extend(
1022 Self::path_patterns(&workspace_root)
1023 .into_iter()
1024 .map(|pattern| (pattern, "[WORKSPACE]/".to_string())),
1025 );
1026
1027 filters.push((
1029 r"Activate with: (.*)\\Scripts\\activate".to_string(),
1030 "Activate with: source $1/[BIN]/activate".to_string(),
1031 ));
1032 filters.push((
1033 r"Activate with: Scripts\\activate".to_string(),
1034 "Activate with: source [BIN]/activate".to_string(),
1035 ));
1036 filters.push((
1037 r"Activate with: source (.*/|)bin/activate(?:\.\w+)?".to_string(),
1038 "Activate with: source $1[BIN]/activate".to_string(),
1039 ));
1040
1041 filters.push((
1044 r#"(\\|/)\.tmp[^\\/\s"'`]*"#.to_string(),
1045 "/[TMP]".to_string(),
1046 ));
1047
1048 filters.push((r"file:///".to_string(), "file://".to_string()));
1050
1051 filters.push((r"\\\\\?\\".to_string(), String::new()));
1053
1054 filters.push((r"127\.0\.0\.1:\d*".to_string(), "[LOCALHOST]".to_string()));
1056 filters.push((
1058 format!(
1059 r#"requires = \["uv_build>={},<[0-9.]+"\]"#,
1060 uv_version::version()
1061 ),
1062 r#"requires = ["uv_build>=[CURRENT_VERSION],<[NEXT_BREAKING]"]"#.to_string(),
1063 ));
1064 filters.push((
1066 r"environments-v(\d+)[\\/]([\w.\[\]-]+)-[a-f0-9]{16}".to_string(),
1067 "environments-v$1/$2-[HASH]".to_string(),
1068 ));
1069 filters.push((
1071 r"archive-v(\d+)[\\/][A-Za-z0-9\-\_]+".to_string(),
1072 "archive-v$1/[HASH]".to_string(),
1073 ));
1074
1075 Self {
1076 root: ChildPath::new(root.path()),
1077 temp_dir,
1078 cache_dir,
1079 python_dir,
1080 home_dir,
1081 user_config_dir,
1082 bin_dir,
1083 venv,
1084 workspace_root,
1085 python_version,
1086 python_versions,
1087 uv_bin,
1088 filters,
1089 extra_env: vec![],
1090 _root: root,
1091 _extra_tempdirs: vec![],
1092 }
1093 }
1094
1095 pub fn command(&self) -> Command {
1097 let mut command = self.new_command();
1098 self.add_shared_options(&mut command, true);
1099 command
1100 }
1101
1102 pub fn disallow_git_cli(bin_dir: &Path) -> std::io::Result<()> {
1103 let contents = r"#!/bin/sh
1104 echo 'error: `git` operations are not allowed — are you missing a cfg for the `git` feature?' >&2
1105 exit 127";
1106 let git = bin_dir.join(format!("git{}", env::consts::EXE_SUFFIX));
1107 fs_err::write(&git, contents)?;
1108
1109 #[cfg(unix)]
1110 {
1111 use std::os::unix::fs::PermissionsExt;
1112 let mut perms = fs_err::metadata(&git)?.permissions();
1113 perms.set_mode(0o755);
1114 fs_err::set_permissions(&git, perms)?;
1115 }
1116
1117 Ok(())
1118 }
1119
1120 #[must_use]
1125 pub fn with_git_lfs_config(mut self) -> Self {
1126 let git_lfs_config = self.root.child(".gitconfig");
1127 git_lfs_config
1128 .write_str(indoc! {r#"
1129 [filter "lfs"]
1130 clean = git-lfs clean -- %f
1131 smudge = git-lfs smudge -- %f
1132 process = git-lfs filter-process
1133 required = true
1134 "#})
1135 .expect("Failed to setup `git-lfs` filters");
1136
1137 self.extra_env.push((
1140 EnvVars::GIT_CONFIG_GLOBAL.into(),
1141 git_lfs_config.as_os_str().into(),
1142 ));
1143 self
1144 }
1145
1146 pub fn add_shared_options(&self, command: &mut Command, activate_venv: bool) {
1158 self.add_shared_args(command);
1159 self.add_shared_env(command, activate_venv);
1160 }
1161
1162 fn add_shared_args(&self, command: &mut Command) {
1164 command.arg("--cache-dir").arg(self.cache_dir.path());
1165 }
1166
1167 pub fn add_shared_env(&self, command: &mut Command, activate_venv: bool) {
1169 let path = env::join_paths(std::iter::once(self.bin_dir.to_path_buf()).chain(
1171 env::split_paths(&env::var(EnvVars::PATH).unwrap_or_default()),
1172 ))
1173 .unwrap();
1174
1175 if cfg!(not(windows)) {
1178 command.env(EnvVars::SHELL, "bash");
1179 }
1180
1181 command
1182 .env_remove(EnvVars::VIRTUAL_ENV)
1184 .env(EnvVars::UV_NO_WRAP, "1")
1186 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1188 .env(EnvVars::COLUMNS, "100")
1191 .env(EnvVars::PATH, path)
1192 .env(EnvVars::HOME, self.home_dir.as_os_str())
1193 .env(EnvVars::APPDATA, self.home_dir.as_os_str())
1194 .env(EnvVars::USERPROFILE, self.home_dir.as_os_str())
1195 .env(
1196 EnvVars::XDG_CONFIG_DIRS,
1197 self.home_dir.join("config").as_os_str(),
1198 )
1199 .env(
1200 EnvVars::XDG_DATA_HOME,
1201 self.home_dir.join("data").as_os_str(),
1202 )
1203 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1204 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "")
1205 .env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
1207 .env(EnvVars::UV_PYTHON_SEARCH_PATH, self.python_path())
1208 .env(EnvVars::UV_EXCLUDE_NEWER, TEST_TIMESTAMP)
1209 .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, TEST_TIMESTAMP)
1210 .env(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF, TEST_TIMESTAMP)
1211 .env(EnvVars::UV_PYTHON_NO_REGISTRY, "1")
1214 .env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "0")
1215 .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1")
1218 .env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path())
1232 .current_dir(self.temp_dir.path());
1233
1234 for (key, value) in &self.extra_env {
1235 command.env(key, value);
1236 }
1237
1238 if activate_venv {
1239 command.env(EnvVars::VIRTUAL_ENV, self.venv.as_os_str());
1240 }
1241
1242 if cfg!(unix) {
1243 command.env(EnvVars::LC_ALL, "C");
1245 }
1246 }
1247
1248 pub fn pip_compile(&self) -> Command {
1250 let mut command = self.new_command();
1251 command.arg("pip").arg("compile");
1252 self.add_shared_options(&mut command, true);
1253 command
1254 }
1255
1256 pub fn pip_sync(&self) -> Command {
1258 let mut command = self.new_command();
1259 command.arg("pip").arg("sync");
1260 self.add_shared_options(&mut command, true);
1261 command
1262 }
1263
1264 pub fn pip_show(&self) -> Command {
1265 let mut command = self.new_command();
1266 command.arg("pip").arg("show");
1267 self.add_shared_options(&mut command, true);
1268 command
1269 }
1270
1271 pub fn pip_freeze(&self) -> Command {
1273 let mut command = self.new_command();
1274 command.arg("pip").arg("freeze");
1275 self.add_shared_options(&mut command, true);
1276 command
1277 }
1278
1279 pub fn pip_check(&self) -> Command {
1281 let mut command = self.new_command();
1282 command.arg("pip").arg("check");
1283 self.add_shared_options(&mut command, true);
1284 command
1285 }
1286
1287 pub fn pip_list(&self) -> Command {
1288 let mut command = self.new_command();
1289 command.arg("pip").arg("list");
1290 self.add_shared_options(&mut command, true);
1291 command
1292 }
1293
1294 pub fn venv(&self) -> Command {
1296 let mut command = self.new_command();
1297 command.arg("venv");
1298 self.add_shared_options(&mut command, false);
1299 command
1300 }
1301
1302 pub fn pip_install(&self) -> Command {
1304 let mut command = self.new_command();
1305 command.arg("pip").arg("install");
1306 self.add_shared_options(&mut command, true);
1307 command
1308 }
1309
1310 pub fn pip_uninstall(&self) -> Command {
1312 let mut command = self.new_command();
1313 command.arg("pip").arg("uninstall");
1314 self.add_shared_options(&mut command, true);
1315 command
1316 }
1317
1318 pub fn pip_tree(&self) -> Command {
1320 let mut command = self.new_command();
1321 command.arg("pip").arg("tree");
1322 self.add_shared_options(&mut command, true);
1323 command
1324 }
1325
1326 pub fn pip_debug(&self) -> Command {
1328 let mut command = self.new_command();
1329 command.arg("pip").arg("debug");
1330 self.add_shared_options(&mut command, true);
1331 command
1332 }
1333
1334 pub fn help(&self) -> Command {
1336 let mut command = self.new_command();
1337 command.arg("help");
1338 self.add_shared_env(&mut command, false);
1339 command
1340 }
1341
1342 pub fn init(&self) -> Command {
1345 let mut command = self.new_command();
1346 command.arg("init");
1347 self.add_shared_options(&mut command, false);
1348 command
1349 }
1350
1351 pub fn sync(&self) -> Command {
1353 let mut command = self.new_command();
1354 command.arg("sync");
1355 self.add_shared_options(&mut command, false);
1356 command
1357 }
1358
1359 pub fn lock(&self) -> Command {
1361 let mut command = self.new_command();
1362 command.arg("lock");
1363 self.add_shared_options(&mut command, false);
1364 command
1365 }
1366
1367 pub fn upgrade(&self) -> Command {
1369 let mut command = self.new_command();
1370 command.arg("upgrade");
1371 self.add_shared_options(&mut command, false);
1372 command
1373 }
1374
1375 pub fn audit(&self) -> Command {
1377 let mut command = self.new_command();
1378 command.arg("audit");
1379 self.add_shared_options(&mut command, false);
1380 command
1381 }
1382
1383 pub fn workspace_metadata(&self) -> Command {
1385 let mut command = self.new_command();
1386 command.arg("workspace").arg("metadata");
1387 self.add_shared_options(&mut command, false);
1388 command
1389 }
1390
1391 pub fn workspace_dir(&self) -> Command {
1393 let mut command = self.new_command();
1394 command.arg("workspace").arg("dir");
1395 self.add_shared_options(&mut command, false);
1396 command
1397 }
1398
1399 pub fn workspace_list(&self) -> Command {
1401 let mut command = self.new_command();
1402 command.arg("workspace").arg("list");
1403 self.add_shared_options(&mut command, false);
1404 command
1405 }
1406
1407 pub fn export(&self) -> Command {
1409 let mut command = self.new_command();
1410 command.arg("export");
1411 self.add_shared_options(&mut command, false);
1412 command
1413 }
1414
1415 pub fn format(&self) -> Command {
1417 let mut command = self.new_command();
1418 command.arg("format");
1419 self.add_shared_options(&mut command, false);
1420 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1422 command
1423 }
1424
1425 pub fn check(&self) -> Command {
1427 let mut command = self.new_command();
1428 command.arg("check");
1429 self.add_shared_options(&mut command, false);
1430 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1432 command
1433 }
1434
1435 pub fn build(&self) -> Command {
1437 let mut command = self.new_command();
1438 command.arg("build");
1439 self.add_shared_options(&mut command, false);
1440 command
1441 }
1442
1443 pub fn version(&self) -> Command {
1444 let mut command = self.new_command();
1445 command.arg("version");
1446 self.add_shared_options(&mut command, false);
1447 command
1448 }
1449
1450 pub fn self_version(&self) -> Command {
1451 let mut command = self.new_command();
1452 command.arg("self").arg("version");
1453 self.add_shared_options(&mut command, false);
1454 command
1455 }
1456
1457 pub fn self_update(&self) -> Command {
1458 let mut command = self.new_command();
1459 command.arg("self").arg("update");
1460 self.add_shared_options(&mut command, false);
1461 command
1462 }
1463
1464 pub fn publish(&self) -> Command {
1466 let mut command = self.new_command();
1467 command.arg("publish");
1468 self.add_shared_options(&mut command, false);
1469 command
1470 }
1471
1472 pub fn python_find(&self) -> Command {
1474 let mut command = self.new_command();
1475 command
1476 .arg("python")
1477 .arg("find")
1478 .env(EnvVars::UV_PREVIEW, "1")
1479 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1480 self.add_shared_options(&mut command, false);
1481 command
1482 }
1483
1484 pub fn python_list(&self) -> Command {
1486 let mut command = self.new_command();
1487 command
1488 .arg("python")
1489 .arg("list")
1490 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1491 self.add_shared_options(&mut command, false);
1492 command
1493 }
1494
1495 pub fn python_install(&self) -> Command {
1497 let mut command = self.new_command();
1498 command.arg("python").arg("install");
1499 self.add_shared_options(&mut command, true);
1500 command
1501 }
1502
1503 pub fn python_uninstall(&self) -> Command {
1505 let mut command = self.new_command();
1506 command.arg("python").arg("uninstall");
1507 self.add_shared_options(&mut command, true);
1508 command
1509 }
1510
1511 pub fn python_upgrade(&self) -> Command {
1513 let mut command = self.new_command();
1514 command.arg("python").arg("upgrade");
1515 self.add_shared_options(&mut command, true);
1516 command
1517 }
1518
1519 pub fn python_pin(&self) -> Command {
1521 let mut command = self.new_command();
1522 command.arg("python").arg("pin");
1523 self.add_shared_options(&mut command, true);
1524 command
1525 }
1526
1527 pub fn python_dir(&self) -> Command {
1529 let mut command = self.new_command();
1530 command.arg("python").arg("dir");
1531 self.add_shared_options(&mut command, true);
1532 command
1533 }
1534
1535 pub fn run(&self) -> Command {
1537 let mut command = self.new_command();
1538 command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1");
1539 self.add_shared_options(&mut command, true);
1540 command
1541 }
1542
1543 pub fn tool_run(&self) -> Command {
1545 let mut command = self.new_command();
1546 command
1547 .arg("tool")
1548 .arg("run")
1549 .env(EnvVars::UV_SHOW_RESOLUTION, "1");
1550 self.add_shared_options(&mut command, false);
1551 command
1552 }
1553
1554 pub fn tool_upgrade(&self) -> Command {
1556 let mut command = self.new_command();
1557 command.arg("tool").arg("upgrade");
1558 self.add_shared_options(&mut command, false);
1559 command
1560 }
1561
1562 pub fn tool_install(&self) -> Command {
1564 let mut command = self.new_command();
1565 command.arg("tool").arg("install");
1566 self.add_shared_options(&mut command, false);
1567 command
1568 }
1569
1570 pub fn tool_list(&self) -> Command {
1572 let mut command = self.new_command();
1573 command.arg("tool").arg("list");
1574 self.add_shared_options(&mut command, false);
1575 command
1576 }
1577
1578 pub fn tool_dir(&self) -> Command {
1580 let mut command = self.new_command();
1581 command.arg("tool").arg("dir");
1582 self.add_shared_options(&mut command, false);
1583 command
1584 }
1585
1586 pub fn tool_uninstall(&self) -> Command {
1588 let mut command = self.new_command();
1589 command.arg("tool").arg("uninstall");
1590 self.add_shared_options(&mut command, false);
1591 command
1592 }
1593
1594 pub fn add(&self) -> Command {
1596 let mut command = self.new_command();
1597 command.arg("add");
1598 self.add_shared_options(&mut command, false);
1599 command
1600 }
1601
1602 pub fn remove(&self) -> Command {
1604 let mut command = self.new_command();
1605 command.arg("remove");
1606 self.add_shared_options(&mut command, false);
1607 command
1608 }
1609
1610 pub fn tree(&self) -> Command {
1612 let mut command = self.new_command();
1613 command.arg("tree");
1614 self.add_shared_options(&mut command, false);
1615 command
1616 }
1617
1618 pub fn clean(&self) -> Command {
1620 let mut command = self.new_command();
1621 command.arg("cache").arg("clean");
1622 self.add_shared_options(&mut command, false);
1623 command
1624 }
1625
1626 pub fn prune(&self) -> Command {
1628 let mut command = self.new_command();
1629 command.arg("cache").arg("prune");
1630 self.add_shared_options(&mut command, false);
1631 command
1632 }
1633
1634 pub fn cache_size(&self) -> Command {
1636 let mut command = self.new_command();
1637 command.arg("cache").arg("size");
1638 self.add_shared_options(&mut command, false);
1639 command
1640 }
1641
1642 pub fn build_backend(&self) -> Command {
1646 let mut command = self.new_command();
1647 command.arg("build-backend");
1648 self.add_shared_options(&mut command, false);
1649 command
1650 }
1651
1652 pub fn interpreter(&self) -> PathBuf {
1656 let venv = &self.venv;
1657 if cfg!(unix) {
1658 venv.join("bin").join("python")
1659 } else if cfg!(windows) {
1660 venv.join("Scripts").join("python.exe")
1661 } else {
1662 unimplemented!("Only Windows and Unix are supported")
1663 }
1664 }
1665
1666 pub fn python_command(&self) -> Command {
1667 let mut interpreter = self.interpreter();
1668
1669 if !interpreter.exists() {
1671 interpreter.clone_from(
1672 &self
1673 .python_versions
1674 .first()
1675 .expect("At least one Python version is required")
1676 .1,
1677 );
1678 }
1679
1680 let mut command = Self::new_command_with(&interpreter);
1681 command
1682 .arg("-B")
1685 .env(EnvVars::PYTHONUTF8, "1");
1687
1688 self.add_shared_env(&mut command, false);
1689
1690 command
1691 }
1692
1693 pub fn auth_login(&self) -> Command {
1695 let mut command = self.new_command();
1696 command.arg("auth").arg("login");
1697 self.add_shared_options(&mut command, false);
1698 command
1699 }
1700
1701 pub fn auth_logout(&self) -> Command {
1703 let mut command = self.new_command();
1704 command.arg("auth").arg("logout");
1705 self.add_shared_options(&mut command, false);
1706 command
1707 }
1708
1709 pub fn auth_helper(&self) -> Command {
1711 let mut command = self.new_command();
1712 command.arg("auth").arg("helper");
1713 self.add_shared_options(&mut command, false);
1714 command
1715 }
1716
1717 pub fn auth_token(&self) -> Command {
1719 let mut command = self.new_command();
1720 command.arg("auth").arg("token");
1721 self.add_shared_options(&mut command, false);
1722 command
1723 }
1724
1725 #[must_use]
1729 pub fn with_real_home(mut self) -> Self {
1730 if let Some(home) = env::var_os(EnvVars::HOME) {
1731 self.extra_env
1732 .push((EnvVars::HOME.to_string().into(), home));
1733 }
1734 self.extra_env.push((
1737 EnvVars::XDG_CONFIG_HOME.into(),
1738 self.user_config_dir.as_os_str().into(),
1739 ));
1740 self
1741 }
1742
1743 pub fn assert_command(&self, command: &str) -> Assert {
1745 self.python_command()
1746 .arg("-c")
1747 .arg(command)
1748 .current_dir(&self.temp_dir)
1749 .assert()
1750 }
1751
1752 pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
1754 self.python_command()
1755 .arg(file.as_ref())
1756 .current_dir(&self.temp_dir)
1757 .assert()
1758 }
1759
1760 pub fn assert_installed(&self, package: &'static str, version: &'static str) {
1762 self.assert_command(
1763 format!("import {package} as package; print(package.__version__, end='')").as_str(),
1764 )
1765 .success()
1766 .stdout(version);
1767 }
1768
1769 pub fn assert_not_installed(&self, package: &'static str) {
1771 self.assert_command(format!("import {package}").as_str())
1772 .failure();
1773 }
1774
1775 pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
1777 let mut patterns = Vec::new();
1778
1779 if path.as_ref().exists() {
1781 patterns.push(Self::path_pattern(
1782 path.as_ref()
1783 .canonicalize()
1784 .expect("Failed to create canonical path"),
1785 ));
1786 }
1787
1788 patterns.push(Self::path_pattern(path));
1790
1791 patterns
1792 }
1793
1794 fn path_pattern(path: impl AsRef<Path>) -> String {
1796 format!(
1797 r"{}\\?/?",
1799 regex::escape(&path.as_ref().simplified_display().to_string())
1800 .replace(r"\\", r"(\\|\/)")
1803 )
1804 }
1805
1806 pub fn python_path(&self) -> OsString {
1807 if cfg!(unix) {
1808 env::join_paths(
1810 self.python_versions
1811 .iter()
1812 .map(|(version, _)| self.python_dir.join(version.to_string())),
1813 )
1814 .unwrap()
1815 } else {
1816 env::join_paths(
1818 self.python_versions
1819 .iter()
1820 .map(|(_, executable)| executable.parent().unwrap().to_path_buf()),
1821 )
1822 .unwrap()
1823 }
1824 }
1825
1826 pub fn filters(&self) -> Vec<(&str, &str)> {
1828 self.filters
1831 .iter()
1832 .map(|(p, r)| (p.as_str(), r.as_str()))
1833 .chain(INSTA_FILTERS.iter().copied())
1834 .collect()
1835 }
1836
1837 #[cfg(windows)]
1839 pub fn filters_without_standard_filters(&self) -> Vec<(&str, &str)> {
1840 self.filters
1841 .iter()
1842 .map(|(p, r)| (p.as_str(), r.as_str()))
1843 .collect()
1844 }
1845
1846 pub fn python_kind(&self) -> &'static str {
1848 "python"
1849 }
1850
1851 pub fn site_packages(&self) -> PathBuf {
1853 site_packages_path(
1854 &self.venv,
1855 &format!(
1856 "{}{}",
1857 self.python_kind(),
1858 self.python_version.as_ref().expect(
1859 "A Python version must be provided to retrieve the test site packages path"
1860 )
1861 ),
1862 )
1863 }
1864
1865 pub fn reset_venv(&self) {
1867 self.create_venv();
1868 }
1869
1870 fn create_venv(&self) {
1872 let executable = get_python(
1873 self.python_version
1874 .as_ref()
1875 .expect("A Python version must be provided to create a test virtual environment"),
1876 );
1877 create_venv_from_executable(&self.venv, &self.cache_dir, &executable, &self.uv_bin);
1878 }
1879
1880 pub fn copy_ecosystem_project(&self, name: &str) {
1891 let project_dir = PathBuf::from(format!("../../test/ecosystem/{name}"));
1892 self.temp_dir.copy_from(project_dir, &["*"]).unwrap();
1893 if let Err(err) = fs_err::remove_file(self.temp_dir.join("uv.lock")) {
1895 assert_eq!(
1896 err.kind(),
1897 io::ErrorKind::NotFound,
1898 "Failed to remove uv.lock: {err}"
1899 );
1900 }
1901 }
1902
1903 pub fn diff_lock(&self, change: impl Fn(&Self) -> Command) -> String {
1912 static TRIM_TRAILING_WHITESPACE: std::sync::LazyLock<Regex> =
1913 std::sync::LazyLock::new(|| Regex::new(r"(?m)^\s+$").unwrap());
1914
1915 let lock_path = ChildPath::new(self.temp_dir.join("uv.lock"));
1916 let old_lock = fs_err::read_to_string(&lock_path).unwrap();
1917 let (snapshot, output) = run_and_format(
1918 change(self),
1919 self.filters(),
1920 "diff_lock",
1921 Some(WindowsFilters::Platform),
1922 None,
1923 );
1924 assert!(output.status.success(), "{snapshot}");
1925 let new_lock = fs_err::read_to_string(&lock_path).unwrap();
1926 diff_snapshot(&old_lock, &new_lock, 10)
1927 }
1928
1929 pub fn read(&self, file: impl AsRef<Path>) -> String {
1931 fs_err::read_to_string(self.temp_dir.join(&file))
1932 .unwrap_or_else(|_| panic!("Missing file: `{}`", file.user_display()))
1933 }
1934
1935 fn new_command(&self) -> Command {
1938 Self::new_command_with(&self.uv_bin)
1939 }
1940
1941 fn new_command_with(bin: &Path) -> Command {
1947 let mut command = Command::new(bin);
1948
1949 let passthrough = [
1950 EnvVars::PATH,
1952 EnvVars::RUST_LOG,
1954 EnvVars::RUST_BACKTRACE,
1955 EnvVars::SYSTEMDRIVE,
1957 EnvVars::RUST_MIN_STACK,
1959 EnvVars::UV_STACK_SIZE,
1960 EnvVars::ALL_PROXY,
1962 EnvVars::HTTPS_PROXY,
1963 EnvVars::HTTP_PROXY,
1964 EnvVars::NO_PROXY,
1965 EnvVars::SSL_CERT_DIR,
1966 EnvVars::SSL_CERT_FILE,
1967 EnvVars::UV_NATIVE_TLS,
1968 EnvVars::UV_SYSTEM_CERTS,
1969 ];
1970
1971 for env_var in EnvVars::all_names()
1972 .iter()
1973 .filter(|name| !passthrough.contains(name))
1974 {
1975 command.env_remove(env_var);
1976 }
1977
1978 command
1979 }
1980}
1981
1982pub fn diff_snapshot(old: &str, new: &str, context_radius: usize) -> String {
1985 static TRIM_TRAILING_WHITESPACE: std::sync::LazyLock<Regex> =
1986 std::sync::LazyLock::new(|| Regex::new(r"(?m)^\s+$").unwrap());
1987
1988 let diff = similar::TextDiff::from_lines(old, new);
1989 let unified = diff
1990 .unified_diff()
1991 .context_radius(context_radius)
1992 .header("old", "new")
1993 .to_string();
1994 TRIM_TRAILING_WHITESPACE
1998 .replace_all(&unified, "")
1999 .into_owned()
2000}
2001
2002#[macro_export]
2006macro_rules! diff_uv_snapshot {
2007 ($filters:expr, $old:expr, $spawnable:expr, @$snapshot:literal) => {{
2008 let new = $crate::capture_uv_snapshot!($filters, $spawnable);
2009 let snapshot = $crate::diff_snapshot($old, &new, 3);
2010 let mut settings = ::insta::Settings::clone_current();
2011 let description = match settings.description() {
2013 Some(description) => format!("{description}\n\nUnfiltered diff:\n{snapshot}"),
2014 None => format!("Unfiltered diff:\n{snapshot}"),
2015 };
2016 settings.set_description(description);
2017 settings.add_filter(r"^--- old\n\+\+\+ new\n", "");
2018 settings.add_filter(r"(?m)^@@.*$", "...");
2019 settings.add_filter(r"\n$", "\n...\n");
2020 settings.bind(|| {
2021 ::insta::assert_snapshot!(snapshot, @$snapshot);
2022 });
2023 new
2024 }};
2025}
2026
2027#[macro_export]
2029macro_rules! capture_uv_snapshot {
2030 ($filters:expr, $spawnable:expr) => {{
2031 let (snapshot, _) = $crate::run_and_format_silent(
2033 $spawnable,
2034 &$filters,
2035 $crate::function_name!(),
2036 Some($crate::WindowsFilters::Platform),
2037 None,
2038 );
2039 snapshot
2040 }};
2041 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2042 let (snapshot, _) = $crate::run_and_format(
2043 $spawnable,
2044 &$filters,
2045 $crate::function_name!(),
2046 Some($crate::WindowsFilters::Platform),
2047 None,
2048 );
2049 ::insta::assert_snapshot!(snapshot, @$snapshot);
2050 snapshot
2051 }};
2052}
2053
2054pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
2055 if cfg!(unix) {
2056 venv.join("lib").join(python).join("site-packages")
2057 } else if cfg!(windows) {
2058 venv.join("Lib").join("site-packages")
2059 } else {
2060 unimplemented!("Only Windows and Unix are supported")
2061 }
2062}
2063
2064pub fn venv_bin_path(venv: impl AsRef<Path>) -> PathBuf {
2065 if cfg!(unix) {
2066 venv.as_ref().join("bin")
2067 } else if cfg!(windows) {
2068 venv.as_ref().join("Scripts")
2069 } else {
2070 unimplemented!("Only Windows and Unix are supported")
2071 }
2072}
2073
2074fn get_python(version: &PythonVersion) -> PathBuf {
2076 ManagedPythonInstallations::from_settings(None)
2077 .map(|installed_pythons| {
2078 installed_pythons
2079 .find_version(version)
2080 .expect("Tests are run on a supported platform")
2081 .next()
2082 .as_ref()
2083 .map(|python| python.executable(false))
2084 })
2085 .unwrap_or_default()
2088 .unwrap_or(PathBuf::from(version.to_string()))
2089}
2090
2091fn create_venv_from_executable<P: AsRef<Path>>(
2093 path: P,
2094 cache_dir: &ChildPath,
2095 python: &Path,
2096 uv_bin: &Path,
2097) {
2098 TestContext::new_command_with(uv_bin)
2099 .arg("venv")
2100 .arg(path.as_ref().as_os_str())
2101 .arg("--clear")
2102 .arg("--cache-dir")
2103 .arg(cache_dir.path())
2104 .arg("--python")
2105 .arg(python)
2106 .current_dir(path.as_ref().parent().unwrap())
2107 .assert()
2108 .success();
2109 ChildPath::new(path.as_ref()).assert(predicate::path::is_dir());
2110}
2111
2112pub fn python_path_with_versions(
2116 temp_dir: &ChildPath,
2117 python_versions: &[&str],
2118) -> anyhow::Result<OsString> {
2119 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
2120 Ok(env::join_paths(
2121 python_installations_for_versions(temp_dir, python_versions, &download_list)?
2122 .into_iter()
2123 .map(|path| path.parent().unwrap().to_path_buf()),
2124 )?)
2125}
2126
2127fn python_installations_for_versions(
2131 temp_dir: &ChildPath,
2132 python_versions: &[&str],
2133 download_list: &ManagedPythonDownloadList,
2134) -> anyhow::Result<Vec<PathBuf>> {
2135 let cache = Cache::from_path(temp_dir.child("cache").to_path_buf())
2136 .init_no_wait()?
2137 .expect("No cache contention when setting up Python in tests");
2138 let _preview = uv_preview::test::with_features(&[]);
2139 let selected_pythons = python_versions
2140 .iter()
2141 .map(|python_version| {
2142 if let Ok(python) = PythonInstallation::find(
2143 &PythonRequest::parse(python_version),
2144 EnvironmentPreference::OnlySystem,
2145 PythonPreference::Managed,
2146 download_list,
2147 &cache,
2148 ) {
2149 python.into_interpreter().sys_executable().to_owned()
2150 } else {
2151 panic!("Could not find Python {python_version} for test\nTry `cargo run python install` first, or refer to CONTRIBUTING.md");
2152 }
2153 })
2154 .collect::<Vec<_>>();
2155
2156 assert!(
2157 python_versions.is_empty() || !selected_pythons.is_empty(),
2158 "Failed to fulfill requested test Python versions: {selected_pythons:?}"
2159 );
2160
2161 Ok(selected_pythons)
2162}
2163
2164#[derive(Debug, Copy, Clone)]
2165pub enum WindowsFilters {
2166 Platform,
2167 Universal,
2168}
2169
2170pub fn apply_filters<T: AsRef<str>>(mut snapshot: String, filters: impl AsRef<[(T, T)]>) -> String {
2172 for (matcher, replacement) in filters.as_ref() {
2173 let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?");
2175 if re.is_match(&snapshot) {
2176 snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string();
2177 }
2178 }
2179 snapshot
2180}
2181
2182#[expect(clippy::print_stderr)]
2186pub fn run_and_format<T: AsRef<str>>(
2187 command: impl BorrowMut<Command>,
2188 filters: impl AsRef<[(T, T)]>,
2189 function_name: &str,
2190 windows_filters: Option<WindowsFilters>,
2191 input: Option<&str>,
2192) -> (String, Output) {
2193 let (snapshot, output) =
2194 run_and_format_silent(command, filters, function_name, windows_filters, input);
2195 eprintln!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Unfiltered output ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2196 eprintln!(
2197 "----- stdout -----\n{}\n----- stderr -----\n{}",
2198 String::from_utf8_lossy(&output.stdout),
2199 String::from_utf8_lossy(&output.stderr),
2200 );
2201 eprintln!("────────────────────────────────────────────────────────────────────────────────\n");
2202 (snapshot, output)
2203}
2204
2205#[doc(hidden)]
2207pub fn run_and_format_silent<T: AsRef<str>>(
2208 mut command: impl BorrowMut<Command>,
2209 filters: impl AsRef<[(T, T)]>,
2210 function_name: &str,
2211 windows_filters: Option<WindowsFilters>,
2212 input: Option<&str>,
2213) -> (String, Output) {
2214 let program = command
2215 .borrow_mut()
2216 .get_program()
2217 .to_string_lossy()
2218 .to_string();
2219
2220 if let Ok(root) = env::var(EnvVars::TRACING_DURATIONS_TEST_ROOT) {
2222 #[expect(clippy::assertions_on_constants)]
2224 {
2225 assert!(
2226 cfg!(feature = "tracing-durations-export"),
2227 "You need to enable the tracing-durations-export feature to use `TRACING_DURATIONS_TEST_ROOT`"
2228 );
2229 }
2230 command.borrow_mut().env(
2231 EnvVars::TRACING_DURATIONS_FILE,
2232 Path::new(&root).join(function_name).with_extension("jsonl"),
2233 );
2234 }
2235
2236 let output = if let Some(input) = input {
2237 let mut child = command
2238 .borrow_mut()
2239 .stdin(Stdio::piped())
2240 .stdout(Stdio::piped())
2241 .stderr(Stdio::piped())
2242 .spawn()
2243 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"));
2244 child
2245 .stdin
2246 .as_mut()
2247 .expect("Failed to open stdin")
2248 .write_all(input.as_bytes())
2249 .expect("Failed to write to stdin");
2250
2251 child
2252 .wait_with_output()
2253 .unwrap_or_else(|err| panic!("Failed to read output from {program}: {err}"))
2254 } else {
2255 command
2256 .borrow_mut()
2257 .output()
2258 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"))
2259 };
2260
2261 let mut snapshot = apply_filters(
2262 format!(
2263 "success: {:?}\nexit_code: {}\n----- stdout -----\n{}\n----- stderr -----\n{}",
2264 output.status.success(),
2265 output.status.code().unwrap_or(!0),
2266 String::from_utf8_lossy(&output.stdout),
2267 String::from_utf8_lossy(&output.stderr),
2268 ),
2269 filters,
2270 );
2271
2272 if cfg!(windows) {
2277 if let Some(windows_filters) = windows_filters {
2278 let windows_only_deps = [
2280 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2281 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2282 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2283 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2284 ];
2285 let mut removed_packages = 0;
2286 for windows_only_dep in windows_only_deps {
2287 let re = Regex::new(windows_only_dep).unwrap();
2289 if re.is_match(&snapshot) {
2290 snapshot = re.replace(&snapshot, "").to_string();
2291 removed_packages += 1;
2292 }
2293 }
2294 if removed_packages > 0 {
2295 for i in 1..20 {
2296 for verb in match windows_filters {
2297 WindowsFilters::Platform => [
2298 "Resolved",
2299 "Prepared",
2300 "Installed",
2301 "Checked",
2302 "Uninstalled",
2303 ]
2304 .iter(),
2305 WindowsFilters::Universal => {
2306 ["Prepared", "Installed", "Checked", "Uninstalled"].iter()
2307 }
2308 } {
2309 snapshot = snapshot.replace(
2310 &format!("{verb} {} packages", i + removed_packages),
2311 &format!("{verb} {} package{}", i, if i > 1 { "s" } else { "" }),
2312 );
2313 }
2314 }
2315 }
2316 }
2317 }
2318
2319 (snapshot, output)
2320}
2321
2322pub fn copy_dir_ignore(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {
2324 for entry in ignore::Walk::new(&src) {
2325 let entry = entry?;
2326 let relative = entry.path().strip_prefix(&src)?;
2327 let ty = entry.file_type().unwrap();
2328 if ty.is_dir() {
2329 fs_err::create_dir(dst.as_ref().join(relative))?;
2330 } else {
2331 fs_err::copy(entry.path(), dst.as_ref().join(relative))?;
2332 }
2333 }
2334 Ok(())
2335}
2336
2337pub fn make_project(dir: &Path, name: &str, body: &str) -> anyhow::Result<()> {
2339 let pyproject_toml = formatdoc! {r#"
2340 [project]
2341 name = "{name}"
2342 version = "0.1.0"
2343 requires-python = ">=3.11,<3.13"
2344 {body}
2345
2346 [build-system]
2347 requires = ["uv_build>=0.9.0,<10000"]
2348 build-backend = "uv_build"
2349 "#
2350 };
2351 fs_err::create_dir_all(dir)?;
2352 fs_err::write(dir.join("pyproject.toml"), pyproject_toml)?;
2353 fs_err::create_dir_all(dir.join("src").join(name))?;
2354 fs_err::write(dir.join("src").join(name).join("__init__.py"), "")?;
2355 Ok(())
2356}
2357
2358pub const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
2360 "Z2l0aHViCg==",
2361 "cGF0Cg==",
2362 "MTFBQlVDUjZBMERMUTQ3aVphN3hPdV9qQmhTMkZUeHZ4ZE13OHczakxuZndsV2ZlZjc2cE53eHBWS2tiRUFwdnpmUk8zV0dDSUhicDFsT01aago=",
2363];
2364
2365#[cfg(not(windows))]
2367pub const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[
2368 "Z2l0aHViCg==",
2369 "cGF0Cg==",
2370 "MTFBQlVDUjZBMDJTOFYwMTM4YmQ0bV9uTXpueWhxZDBrcllROTQ5SERTeTI0dENKZ2lmdzIybDFSR2s1SE04QW8xTUVYQ1I0Q1YxYUdPRGpvZQo=",
2371];
2372
2373pub const READ_ONLY_GITHUB_SSH_DEPLOY_KEY: &str = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNBeTF1SnNZK1JXcWp1NkdIY3Z6a3AwS21yWDEwdmo3RUZqTkpNTkRqSGZPZ0FBQUpqWUpwVnAyQ2FWCmFRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQXkxdUpzWStSV3FqdTZHSGN2emtwMEttclgxMHZqN0VGak5KTU5EakhmT2cKQUFBRUMwbzBnd1BxbGl6TFBJOEFXWDVaS2dVZHJyQ2ptMDhIQm9FenB4VDg3MXBqTFc0bXhqNUZhcU83b1lkeS9PU25RcQphdGZYUytQc1FXTTBrdzBPTWQ4NkFBQUFFR3R2Ym5OMGFVQmhjM1J5WVd3dWMyZ0JBZ01FQlE9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K";
2374
2375pub fn decode_token(content: &[&str]) -> String {
2378 content
2379 .iter()
2380 .map(|part| base64.decode(part).unwrap())
2381 .map(|decoded| {
2382 std::str::from_utf8(decoded.as_slice())
2383 .unwrap()
2384 .trim_end()
2385 .to_string()
2386 })
2387 .join("_")
2388}
2389
2390#[tokio::main(flavor = "current_thread")]
2393pub async fn download_to_disk(url: &str, path: &Path) {
2394 let trusted_hosts: Vec<_> = env::var(EnvVars::UV_INSECURE_HOST)
2395 .unwrap_or_default()
2396 .split(' ')
2397 .map(|h| uv_configuration::TrustedHost::from_str(h).unwrap())
2398 .collect();
2399
2400 let client = uv_client::BaseClientBuilder::default()
2401 .allow_insecure_host(trusted_hosts)
2402 .build()
2403 .expect("failed to build base client");
2404 let url = url.parse().unwrap();
2405 let response = client
2406 .for_host(&url)
2407 .get(reqwest::Url::from(url))
2408 .send()
2409 .await
2410 .unwrap();
2411
2412 let mut file = fs_err::tokio::File::create(path).await.unwrap();
2413 let mut stream = response.bytes_stream();
2414 while let Some(chunk) = stream.next().await {
2415 file.write_all(&chunk.unwrap()).await.unwrap();
2416 }
2417 file.sync_all().await.unwrap();
2418}
2419
2420#[cfg(unix)]
2425pub struct ReadOnlyDirectoryGuard {
2426 path: PathBuf,
2427 original_mode: u32,
2428}
2429
2430#[cfg(unix)]
2431impl ReadOnlyDirectoryGuard {
2432 pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
2435 use std::os::unix::fs::PermissionsExt;
2436 let path = path.into();
2437 let metadata = fs_err::metadata(&path)?;
2438 let original_mode = metadata.permissions().mode();
2439 let readonly_mode = original_mode & !0o222;
2441 fs_err::set_permissions(&path, std::fs::Permissions::from_mode(readonly_mode))?;
2442 Ok(Self {
2443 path,
2444 original_mode,
2445 })
2446 }
2447}
2448
2449#[cfg(unix)]
2450impl Drop for ReadOnlyDirectoryGuard {
2451 fn drop(&mut self) {
2452 use std::os::unix::fs::PermissionsExt;
2453 let _ = fs_err::set_permissions(
2454 &self.path,
2455 std::fs::Permissions::from_mode(self.original_mode),
2456 );
2457 }
2458}
2459
2460#[doc(hidden)]
2464#[macro_export]
2465macro_rules! function_name {
2466 () => {{
2467 fn f() {}
2468 fn type_name_of_val<T>(_: T) -> &'static str {
2469 std::any::type_name::<T>()
2470 }
2471 let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
2472 while let Some(rest) = name.strip_suffix("::{{closure}}") {
2473 name = rest;
2474 }
2475 name
2476 }};
2477}
2478
2479#[macro_export]
2484macro_rules! uv_snapshot {
2485 ($spawnable:expr, @$snapshot:literal) => {{
2486 uv_snapshot!($crate::INSTA_FILTERS.to_vec(), $spawnable, @$snapshot)
2487 }};
2488 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2489 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), None);
2491 ::insta::assert_snapshot!(snapshot, @$snapshot);
2492 output
2493 }};
2494 ($filters:expr, $spawnable:expr, input=$input:expr, @$snapshot:literal) => {{
2495 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), Some($input));
2497 ::insta::assert_snapshot!(snapshot, @$snapshot);
2498 output
2499 }};
2500 ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{
2501 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), None, None);
2503 ::insta::assert_snapshot!(snapshot, @$snapshot);
2504 output
2505 }};
2506 ($filters:expr, universal_windows_filters=true, $spawnable:expr, @$snapshot:literal) => {{
2507 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Universal), None);
2509 ::insta::assert_snapshot!(snapshot, @$snapshot);
2510 output
2511 }};
2512}