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.0b3";
48const LATEST_PYTHON_3_14: &str = "3.14.6";
49const LATEST_PYTHON_3_13: &str = "3.13.14";
50pub const LATEST_PYTHON_3_12: &str = "3.12.13";
51const LATEST_PYTHON_3_11: &str = "3.11.15";
52const LATEST_PYTHON_3_10: &str = "3.10.20";
53
54#[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]
416 pub fn with_pyvenv_cfg_filters(mut self) -> Self {
417 let added_filters = [
418 (r"home = .+".to_string(), "home = [PYTHON_HOME]".to_string()),
419 (
420 r"uv = \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?".to_string(),
421 "uv = [UV_VERSION]".to_string(),
422 ),
423 (
424 r"extends-environment = .+".to_string(),
425 "extends-environment = [PARENT_VENV]".to_string(),
426 ),
427 ];
428 for filter in added_filters {
429 self.filters.insert(0, filter);
430 }
431 self
432 }
433
434 #[must_use]
437 pub fn with_filtered_python_symlinks(mut self) -> Self {
438 for (version, executable) in &self.python_versions {
439 if fs_err::symlink_metadata(executable).unwrap().is_symlink() {
440 self.filters.extend(
441 Self::path_patterns(executable.read_link().unwrap())
442 .into_iter()
443 .map(|pattern| (format! {" -> {pattern}"}, String::new())),
444 );
445 }
446 self.filters.push((
448 regex::escape(&format!(" -> [PYTHON-{version}]")),
449 String::new(),
450 ));
451 }
452 self
453 }
454
455 #[must_use]
457 pub fn with_filtered_path(mut self, path: &Path, name: &str) -> Self {
458 for pattern in Self::path_patterns(path)
462 .into_iter()
463 .map(|pattern| (pattern, format!("[{name}]/")))
464 {
465 self.filters.insert(0, pattern);
466 }
467 self
468 }
469
470 #[inline]
478 #[must_use]
479 pub fn with_filtered_link_mode_warning(mut self) -> Self {
480 let pattern = "warning: Failed to hardlink files; .*\n.*\n.*\n";
481 self.filters.push((pattern.to_string(), String::new()));
482 self
483 }
484
485 #[inline]
487 #[must_use]
488 pub fn with_filtered_not_executable(mut self) -> Self {
489 let pattern = if cfg!(unix) {
490 r"Permission denied \(os error 13\)"
491 } else {
492 r"\%1 is not a valid Win32 application. \(os error 193\)"
493 };
494 self.filters
495 .push((pattern.to_string(), "[PERMISSION DENIED]".to_string()));
496 self
497 }
498
499 #[must_use]
501 pub fn with_filtered_python_keys(mut self) -> Self {
502 let platform_re = r"(?x)
504 ( # We capture the group before the platform
505 (?:cpython|pypy|graalpy)# Python implementation
506 -
507 \d+\.\d+ # Major and minor version
508 (?: # The patch version is handled separately
509 \.
510 (?:
511 \[X\] # A previously filtered patch version [X]
512 | # OR
513 \[LATEST\] # A previously filtered latest patch version [LATEST]
514 | # OR
515 \d+ # An actual patch version
516 )
517 )? # (we allow the patch version to be missing entirely, e.g., in a request)
518 (?:(?:a|b|rc)[0-9]+)? # Pre-release version component, e.g., `a6` or `rc2`
519 (?:[td])? # A short variant, such as `t` (for freethreaded) or `d` (for debug)
520 (?:(\+[a-z]+)+)? # A long variant, such as `+freethreaded` or `+freethreaded+debug`
521 )
522 -
523 [a-z0-9]+ # Operating system (e.g., 'macos')
524 -
525 [a-z0-9_]+ # Architecture (e.g., 'aarch64')
526 -
527 [a-z]+ # Libc (e.g., 'none')
528";
529 self.filters
530 .push((platform_re.to_string(), "$1-[PLATFORM]".to_string()));
531 self
532 }
533
534 #[must_use]
536 pub fn with_filtered_latest_python_versions(mut self) -> Self {
537 for (minor, patch) in [
540 ("3.15", LATEST_PYTHON_3_15.strip_prefix("3.15.").unwrap()),
541 ("3.14", LATEST_PYTHON_3_14.strip_prefix("3.14.").unwrap()),
542 ("3.13", LATEST_PYTHON_3_13.strip_prefix("3.13.").unwrap()),
543 ("3.12", LATEST_PYTHON_3_12.strip_prefix("3.12.").unwrap()),
544 ("3.11", LATEST_PYTHON_3_11.strip_prefix("3.11.").unwrap()),
545 ("3.10", LATEST_PYTHON_3_10.strip_prefix("3.10.").unwrap()),
546 ] {
547 let pattern = format!(r"(\b){minor}\.{patch}(\b)");
549 let replacement = format!("${{1}}{minor}.[LATEST]${{2}}");
550 self.filters.push((pattern, replacement));
551 }
552 self
553 }
554
555 #[must_use]
557 #[cfg(windows)]
558 pub fn with_filtered_windows_temp_dir(mut self) -> Self {
559 let pattern = regex::escape(
560 &self
561 .temp_dir
562 .simplified_display()
563 .to_string()
564 .replace('/', "\\"),
565 );
566 self.filters.push((pattern, "[TEMP_DIR]".to_string()));
567 self
568 }
569
570 #[must_use]
572 pub fn with_filtered_compiled_file_count(mut self) -> Self {
573 self.filters.push((
574 r"compiled \d+ files".to_string(),
575 "compiled [COUNT] files".to_string(),
576 ));
577 self
578 }
579
580 #[must_use]
582 pub fn with_filtered_current_version(mut self) -> Self {
583 self.filters.push((
584 regex::escape(&format!("v{}", env!("CARGO_PKG_VERSION"))),
585 "v[CURRENT_VERSION]".to_string(),
586 ));
587 self
588 }
589
590 #[must_use]
592 pub fn with_cyclonedx_filters(mut self) -> Self {
593 self.filters.push((
594 r"urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}".to_string(),
595 "[SERIAL_NUMBER]".to_string(),
596 ));
597 self.filters.push((
598 r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#
599 .to_string(),
600 r#""timestamp": "[TIMESTAMP]""#.to_string(),
601 ));
602 self.filters.push((
603 r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#
604 .to_string(),
605 r#""name": "uv",
606 "version": "[VERSION]""#
607 .to_string(),
608 ));
609 self
610 }
611
612 #[must_use]
614 pub fn with_collapsed_whitespace(mut self) -> Self {
615 self.filters.push((r"[ \t]+".to_string(), " ".to_string()));
616 self
617 }
618
619 #[must_use]
621 pub fn with_python_download_cache(mut self) -> Self {
622 self.extra_env.push((
623 EnvVars::UV_PYTHON_CACHE_DIR.into(),
624 env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).unwrap_or_else(|| {
626 uv_cache::Cache::from_settings(false, None)
627 .unwrap()
628 .bucket(CacheBucket::Python)
629 .into()
630 }),
631 ));
632 self
633 }
634
635 #[must_use]
636 pub fn with_empty_python_install_mirror(mut self) -> Self {
637 self.extra_env.push((
638 EnvVars::UV_PYTHON_INSTALL_MIRROR.into(),
639 String::new().into(),
640 ));
641 self
642 }
643
644 #[must_use]
646 pub fn with_managed_python_dirs(mut self) -> Self {
647 let managed = self.temp_dir.join("managed");
648
649 self.extra_env.push((
650 EnvVars::UV_PYTHON_BIN_DIR.into(),
651 self.bin_dir.as_os_str().to_owned(),
652 ));
653 self.extra_env
654 .push((EnvVars::UV_PYTHON_INSTALL_DIR.into(), managed.into()));
655 self.extra_env
656 .push((EnvVars::UV_PYTHON_DOWNLOADS.into(), "automatic".into()));
657
658 self
659 }
660
661 #[must_use]
662 pub fn with_versions_as_managed(mut self, versions: &[&str]) -> Self {
663 self.extra_env.push((
664 EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED.into(),
665 versions.iter().join(" ").into(),
666 ));
667
668 self
669 }
670
671 #[must_use]
673 pub fn with_filter(mut self, filter: (impl Into<String>, impl Into<String>)) -> Self {
674 self.filters.push((filter.0.into(), filter.1.into()));
675 self
676 }
677
678 #[must_use]
680 pub fn with_unset_git_credential_helper(self) -> Self {
681 let git_config = self.home_dir.child(".gitconfig");
682 git_config
683 .write_str(indoc! {r"
684 [credential]
685 helper =
686 "})
687 .expect("Failed to unset git credential helper");
688
689 self
690 }
691
692 #[must_use]
694 #[cfg(windows)]
695 pub fn clear_filters(mut self) -> Self {
696 self.filters.clear();
697 self
698 }
699
700 pub fn with_cache_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
705 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
706 return Ok(None);
707 };
708 self.with_cache_on_fs(&dir, "COW_FS").map(Some)
709 }
710
711 pub fn with_cache_on_alt_fs(self) -> anyhow::Result<Option<Self>> {
716 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_ALT_FS).ok() else {
717 return Ok(None);
718 };
719 self.with_cache_on_fs(&dir, "ALT_FS").map(Some)
720 }
721
722 pub fn with_cache_on_lowlinks_fs(self) -> anyhow::Result<Option<Self>> {
727 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_LOWLINKS_FS).ok() else {
728 return Ok(None);
729 };
730 self.with_cache_on_fs(&dir, "LOWLINKS_FS").map(Some)
731 }
732
733 pub fn with_cache_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
738 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
739 return Ok(None);
740 };
741 self.with_cache_on_fs(&dir, "NOCOW_FS").map(Some)
742 }
743
744 pub fn with_working_dir_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
751 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
752 return Ok(None);
753 };
754 self.with_working_dir_on_fs(&dir, "COW_FS").map(Some)
755 }
756
757 pub fn with_working_dir_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
764 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
765 return Ok(None);
766 };
767 self.with_working_dir_on_fs(&dir, "NOCOW_FS").map(Some)
768 }
769
770 fn with_cache_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
771 fs_err::create_dir_all(dir)?;
772 let tmp = tempfile::TempDir::new_in(dir)?;
773 self.cache_dir = ChildPath::new(tmp.path()).child("cache");
774 fs_err::create_dir_all(&self.cache_dir)?;
775 let replacement = format!("[{name}]/[CACHE_DIR]/");
776 self.filters.extend(
777 Self::path_patterns(&self.cache_dir)
778 .into_iter()
779 .map(|pattern| (pattern, replacement.clone())),
780 );
781 self._extra_tempdirs.push(tmp);
782 Ok(self)
783 }
784
785 fn with_working_dir_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
786 fs_err::create_dir_all(dir)?;
787 let tmp = tempfile::TempDir::new_in(dir)?;
788 self.temp_dir = ChildPath::new(tmp.path()).child("temp");
789 fs_err::create_dir_all(&self.temp_dir)?;
790 let canonical_temp_dir = self.temp_dir.canonicalize()?;
793 self.venv = ChildPath::new(canonical_temp_dir.join(".venv"));
794 let temp_replacement = format!("[{name}]/[TEMP_DIR]/");
795 self.filters.extend(
796 Self::path_patterns(&self.temp_dir)
797 .into_iter()
798 .map(|pattern| (pattern, temp_replacement.clone())),
799 );
800 let venv_replacement = format!("[{name}]/[VENV]/");
801 self.filters.extend(
802 Self::path_patterns(&self.venv)
803 .into_iter()
804 .map(|pattern| (pattern, venv_replacement.clone())),
805 );
806 self._extra_tempdirs.push(tmp);
807 Ok(self)
808 }
809
810 pub fn test_bucket_dir() -> PathBuf {
819 std::env::temp_dir()
820 .simple_canonicalize()
821 .expect("failed to canonicalize temp dir")
822 .join("uv")
823 .join("tests")
824 }
825
826 pub fn new_with_versions_and_bin(python_versions: &[&str], uv_bin: PathBuf) -> Self {
833 let bucket = Self::test_bucket_dir();
834 fs_err::create_dir_all(&bucket).expect("Failed to create test bucket");
835
836 let root = tempfile::TempDir::new_in(bucket).expect("Failed to create test root directory");
837
838 fs_err::create_dir_all(root.path().join(".git"))
841 .expect("Failed to create `.git` placeholder in test root directory");
842
843 let temp_dir = ChildPath::new(root.path()).child("temp");
844 fs_err::create_dir_all(&temp_dir).expect("Failed to create test working directory");
845
846 let cache_dir = ChildPath::new(root.path()).child("cache");
847 fs_err::create_dir_all(&cache_dir).expect("Failed to create test cache directory");
848
849 let python_dir = ChildPath::new(root.path()).child("python");
850 fs_err::create_dir_all(&python_dir).expect("Failed to create test Python directory");
851
852 let bin_dir = ChildPath::new(root.path()).child("bin");
853 fs_err::create_dir_all(&bin_dir).expect("Failed to create test bin directory");
854
855 if cfg!(not(feature = "git")) {
857 Self::disallow_git_cli(&bin_dir).expect("Failed to setup disallowed `git` command");
858 }
859
860 let home_dir = ChildPath::new(root.path()).child("home");
861 fs_err::create_dir_all(&home_dir).expect("Failed to create test home directory");
862
863 let user_config_dir = if cfg!(windows) {
864 ChildPath::new(home_dir.path())
865 } else {
866 ChildPath::new(home_dir.path()).child(".config")
867 };
868
869 let canonical_temp_dir = temp_dir.canonicalize().unwrap();
871 let venv = ChildPath::new(canonical_temp_dir.join(".venv"));
872
873 let python_version = python_versions
874 .first()
875 .map(|version| PythonVersion::from_str(version).unwrap());
876
877 let site_packages = python_version
878 .as_ref()
879 .map(|version| site_packages_path(&venv, &format!("python{version}")));
880
881 let workspace_root = Path::new(&env::var(EnvVars::CARGO_MANIFEST_DIR).unwrap())
884 .parent()
885 .expect("CARGO_MANIFEST_DIR should be nested in workspace")
886 .parent()
887 .expect("CARGO_MANIFEST_DIR should be doubly nested in workspace")
888 .to_path_buf();
889
890 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
891
892 let python_versions: Vec<_> = python_versions
893 .iter()
894 .map(|version| PythonVersion::from_str(version).unwrap())
895 .zip(
896 python_installations_for_versions(&temp_dir, python_versions, &download_list)
897 .expect("Failed to find test Python versions"),
898 )
899 .collect();
900
901 if cfg!(unix) {
904 for (version, executable) in &python_versions {
905 let parent = python_dir.child(version.to_string());
906 parent.create_dir_all().unwrap();
907 parent.child("python3").symlink_to_file(executable).unwrap();
908 }
909 }
910
911 let mut filters = Vec::new();
912
913 filters.extend(
914 Self::path_patterns(&uv_bin)
915 .into_iter()
916 .map(|pattern| (pattern, "[UV]".to_string())),
917 );
918
919 if cfg!(windows) {
921 filters.push((" --link-mode <LINK_MODE>".to_string(), String::new()));
922 filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new()));
923 filters.push((r"exit code: ".to_string(), "exit status: ".to_string()));
925 }
926
927 for (version, executable) in &python_versions {
928 filters.extend(
930 Self::path_patterns(executable)
931 .into_iter()
932 .map(|pattern| (pattern, format!("[PYTHON-{version}]"))),
933 );
934
935 filters.extend(
937 Self::path_patterns(python_dir.join(version.to_string()))
938 .into_iter()
939 .map(|pattern| {
940 (
941 format!("{pattern}[a-zA-Z0-9]*"),
942 format!("[PYTHON-{version}]"),
943 )
944 }),
945 );
946
947 if version.patch().is_none() {
950 filters.push((
951 format!(r"({})\.\d+", regex::escape(version.to_string().as_str())),
952 "$1.[X]".to_string(),
953 ));
954 }
955 }
956
957 filters.extend(
958 Self::path_patterns(&bin_dir)
959 .into_iter()
960 .map(|pattern| (pattern, "[BIN]/".to_string())),
961 );
962 filters.extend(
963 Self::path_patterns(&cache_dir)
964 .into_iter()
965 .map(|pattern| (pattern, "[CACHE_DIR]/".to_string())),
966 );
967 if let Some(ref site_packages) = site_packages {
968 filters.extend(
969 Self::path_patterns(site_packages)
970 .into_iter()
971 .map(|pattern| (pattern, "[SITE_PACKAGES]/".to_string())),
972 );
973 }
974 filters.extend(
975 Self::path_patterns(&venv)
976 .into_iter()
977 .map(|pattern| (pattern, "[VENV]/".to_string())),
978 );
979
980 if let Some(site_packages) = site_packages {
982 filters.push((
983 Self::path_pattern(
984 site_packages
985 .strip_prefix(&canonical_temp_dir)
986 .expect("The test site-packages directory is always in the tempdir"),
987 ),
988 "[SITE_PACKAGES]/".to_string(),
989 ));
990 }
991
992 filters.push((
994 r"[\\/]lib[\\/]python\d+\.\d+[\\/]".to_string(),
995 "/[PYTHON-LIB]/".to_string(),
996 ));
997 filters.push((r"[\\/]Lib[\\/]".to_string(), "/[PYTHON-LIB]/".to_string()));
998
999 filters.extend(
1000 Self::path_patterns(&temp_dir)
1001 .into_iter()
1002 .map(|pattern| (pattern, "[TEMP_DIR]/".to_string())),
1003 );
1004 filters.extend(
1005 Self::path_patterns(&python_dir)
1006 .into_iter()
1007 .map(|pattern| (pattern, "[PYTHON_DIR]/".to_string())),
1008 );
1009 let mut uv_user_config_dir = PathBuf::from(user_config_dir.path());
1010 uv_user_config_dir.push("uv");
1011 filters.extend(
1012 Self::path_patterns(&uv_user_config_dir)
1013 .into_iter()
1014 .map(|pattern| (pattern, "[UV_USER_CONFIG_DIR]/".to_string())),
1015 );
1016 filters.extend(
1017 Self::path_patterns(&user_config_dir)
1018 .into_iter()
1019 .map(|pattern| (pattern, "[USER_CONFIG_DIR]/".to_string())),
1020 );
1021 filters.extend(
1022 Self::path_patterns(&home_dir)
1023 .into_iter()
1024 .map(|pattern| (pattern, "[HOME]/".to_string())),
1025 );
1026 filters.extend(
1027 Self::path_patterns(&workspace_root)
1028 .into_iter()
1029 .map(|pattern| (pattern, "[WORKSPACE]/".to_string())),
1030 );
1031
1032 filters.push((
1034 r"Activate with: (.*)\\Scripts\\activate".to_string(),
1035 "Activate with: source $1/[BIN]/activate".to_string(),
1036 ));
1037 filters.push((
1038 r"Activate with: Scripts\\activate".to_string(),
1039 "Activate with: source [BIN]/activate".to_string(),
1040 ));
1041 filters.push((
1042 r"Activate with: source (.*/|)bin/activate(?:\.\w+)?".to_string(),
1043 "Activate with: source $1[BIN]/activate".to_string(),
1044 ));
1045
1046 filters.push((r"(\\|\/)\.tmp.*(\\|\/)".to_string(), "/[TMP]/".to_string()));
1049
1050 filters.push((r"file:///".to_string(), "file://".to_string()));
1052
1053 filters.push((r"\\\\\?\\".to_string(), String::new()));
1055
1056 filters.push((r"127\.0\.0\.1:\d*".to_string(), "[LOCALHOST]".to_string()));
1058 filters.push((
1060 format!(
1061 r#"requires = \["uv_build>={},<[0-9.]+"\]"#,
1062 uv_version::version()
1063 ),
1064 r#"requires = ["uv_build>=[CURRENT_VERSION],<[NEXT_BREAKING]"]"#.to_string(),
1065 ));
1066 filters.push((
1068 r"environments-v(\d+)[\\/]([\w.\[\]-]+)-[a-f0-9]{16}".to_string(),
1069 "environments-v$1/$2-[HASH]".to_string(),
1070 ));
1071 filters.push((
1073 r"archive-v(\d+)[\\/][A-Za-z0-9\-\_]+".to_string(),
1074 "archive-v$1/[HASH]".to_string(),
1075 ));
1076
1077 Self {
1078 root: ChildPath::new(root.path()),
1079 temp_dir,
1080 cache_dir,
1081 python_dir,
1082 home_dir,
1083 user_config_dir,
1084 bin_dir,
1085 venv,
1086 workspace_root,
1087 python_version,
1088 python_versions,
1089 uv_bin,
1090 filters,
1091 extra_env: vec![],
1092 _root: root,
1093 _extra_tempdirs: vec![],
1094 }
1095 }
1096
1097 pub fn command(&self) -> Command {
1099 let mut command = self.new_command();
1100 self.add_shared_options(&mut command, true);
1101 command
1102 }
1103
1104 pub fn disallow_git_cli(bin_dir: &Path) -> std::io::Result<()> {
1105 let contents = r"#!/bin/sh
1106 echo 'error: `git` operations are not allowed — are you missing a cfg for the `git` feature?' >&2
1107 exit 127";
1108 let git = bin_dir.join(format!("git{}", env::consts::EXE_SUFFIX));
1109 fs_err::write(&git, contents)?;
1110
1111 #[cfg(unix)]
1112 {
1113 use std::os::unix::fs::PermissionsExt;
1114 let mut perms = fs_err::metadata(&git)?.permissions();
1115 perms.set_mode(0o755);
1116 fs_err::set_permissions(&git, perms)?;
1117 }
1118
1119 Ok(())
1120 }
1121
1122 #[must_use]
1127 pub fn with_git_lfs_config(mut self) -> Self {
1128 let git_lfs_config = self.root.child(".gitconfig");
1129 git_lfs_config
1130 .write_str(indoc! {r#"
1131 [filter "lfs"]
1132 clean = git-lfs clean -- %f
1133 smudge = git-lfs smudge -- %f
1134 process = git-lfs filter-process
1135 required = true
1136 "#})
1137 .expect("Failed to setup `git-lfs` filters");
1138
1139 self.extra_env.push((
1142 EnvVars::GIT_CONFIG_GLOBAL.into(),
1143 git_lfs_config.as_os_str().into(),
1144 ));
1145 self
1146 }
1147
1148 pub fn add_shared_options(&self, command: &mut Command, activate_venv: bool) {
1160 self.add_shared_args(command);
1161 self.add_shared_env(command, activate_venv);
1162 }
1163
1164 fn add_shared_args(&self, command: &mut Command) {
1166 command.arg("--cache-dir").arg(self.cache_dir.path());
1167 }
1168
1169 pub fn add_shared_env(&self, command: &mut Command, activate_venv: bool) {
1171 let path = env::join_paths(std::iter::once(self.bin_dir.to_path_buf()).chain(
1173 env::split_paths(&env::var(EnvVars::PATH).unwrap_or_default()),
1174 ))
1175 .unwrap();
1176
1177 if cfg!(not(windows)) {
1180 command.env(EnvVars::SHELL, "bash");
1181 }
1182
1183 command
1184 .env_remove(EnvVars::VIRTUAL_ENV)
1186 .env(EnvVars::UV_NO_WRAP, "1")
1188 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1190 .env(EnvVars::COLUMNS, "100")
1193 .env(EnvVars::PATH, path)
1194 .env(EnvVars::HOME, self.home_dir.as_os_str())
1195 .env(EnvVars::APPDATA, self.home_dir.as_os_str())
1196 .env(EnvVars::USERPROFILE, self.home_dir.as_os_str())
1197 .env(
1198 EnvVars::XDG_CONFIG_DIRS,
1199 self.home_dir.join("config").as_os_str(),
1200 )
1201 .env(
1202 EnvVars::XDG_DATA_HOME,
1203 self.home_dir.join("data").as_os_str(),
1204 )
1205 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1206 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "")
1207 .env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
1209 .env(EnvVars::UV_PYTHON_SEARCH_PATH, self.python_path())
1210 .env(EnvVars::UV_EXCLUDE_NEWER, TEST_TIMESTAMP)
1211 .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, TEST_TIMESTAMP)
1212 .env(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF, TEST_TIMESTAMP)
1213 .env(EnvVars::UV_PYTHON_NO_REGISTRY, "1")
1216 .env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "0")
1217 .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1")
1220 .env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path())
1234 .current_dir(self.temp_dir.path());
1235
1236 for (key, value) in &self.extra_env {
1237 command.env(key, value);
1238 }
1239
1240 if activate_venv {
1241 command.env(EnvVars::VIRTUAL_ENV, self.venv.as_os_str());
1242 }
1243
1244 if cfg!(unix) {
1245 command.env(EnvVars::LC_ALL, "C");
1247 }
1248 }
1249
1250 pub fn pip_compile(&self) -> Command {
1252 let mut command = self.new_command();
1253 command.arg("pip").arg("compile");
1254 self.add_shared_options(&mut command, true);
1255 command
1256 }
1257
1258 pub fn pip_sync(&self) -> Command {
1260 let mut command = self.new_command();
1261 command.arg("pip").arg("sync");
1262 self.add_shared_options(&mut command, true);
1263 command
1264 }
1265
1266 pub fn pip_show(&self) -> Command {
1267 let mut command = self.new_command();
1268 command.arg("pip").arg("show");
1269 self.add_shared_options(&mut command, true);
1270 command
1271 }
1272
1273 pub fn pip_freeze(&self) -> Command {
1275 let mut command = self.new_command();
1276 command.arg("pip").arg("freeze");
1277 self.add_shared_options(&mut command, true);
1278 command
1279 }
1280
1281 pub fn pip_check(&self) -> Command {
1283 let mut command = self.new_command();
1284 command.arg("pip").arg("check");
1285 self.add_shared_options(&mut command, true);
1286 command
1287 }
1288
1289 pub fn pip_list(&self) -> Command {
1290 let mut command = self.new_command();
1291 command.arg("pip").arg("list");
1292 self.add_shared_options(&mut command, true);
1293 command
1294 }
1295
1296 pub fn venv(&self) -> Command {
1298 let mut command = self.new_command();
1299 command.arg("venv");
1300 self.add_shared_options(&mut command, false);
1301 command
1302 }
1303
1304 pub fn pip_install(&self) -> Command {
1306 let mut command = self.new_command();
1307 command.arg("pip").arg("install");
1308 self.add_shared_options(&mut command, true);
1309 command
1310 }
1311
1312 pub fn pip_uninstall(&self) -> Command {
1314 let mut command = self.new_command();
1315 command.arg("pip").arg("uninstall");
1316 self.add_shared_options(&mut command, true);
1317 command
1318 }
1319
1320 pub fn pip_tree(&self) -> Command {
1322 let mut command = self.new_command();
1323 command.arg("pip").arg("tree");
1324 self.add_shared_options(&mut command, true);
1325 command
1326 }
1327
1328 pub fn pip_debug(&self) -> Command {
1330 let mut command = self.new_command();
1331 command.arg("pip").arg("debug");
1332 self.add_shared_options(&mut command, true);
1333 command
1334 }
1335
1336 pub fn help(&self) -> Command {
1338 let mut command = self.new_command();
1339 command.arg("help");
1340 self.add_shared_env(&mut command, false);
1341 command
1342 }
1343
1344 pub fn init(&self) -> Command {
1347 let mut command = self.new_command();
1348 command.arg("init");
1349 self.add_shared_options(&mut command, false);
1350 command
1351 }
1352
1353 pub fn sync(&self) -> Command {
1355 let mut command = self.new_command();
1356 command.arg("sync");
1357 self.add_shared_options(&mut command, false);
1358 command
1359 }
1360
1361 pub fn lock(&self) -> Command {
1363 let mut command = self.new_command();
1364 command.arg("lock");
1365 self.add_shared_options(&mut command, false);
1366 command
1367 }
1368
1369 pub fn upgrade(&self) -> Command {
1371 let mut command = self.new_command();
1372 command.arg("upgrade");
1373 self.add_shared_options(&mut command, false);
1374 command
1375 }
1376
1377 pub fn audit(&self) -> Command {
1379 let mut command = self.new_command();
1380 command.arg("audit");
1381 self.add_shared_options(&mut command, false);
1382 command
1383 }
1384
1385 pub fn workspace_metadata(&self) -> Command {
1387 let mut command = self.new_command();
1388 command.arg("workspace").arg("metadata");
1389 self.add_shared_options(&mut command, false);
1390 command
1391 }
1392
1393 pub fn workspace_dir(&self) -> Command {
1395 let mut command = self.new_command();
1396 command.arg("workspace").arg("dir");
1397 self.add_shared_options(&mut command, false);
1398 command
1399 }
1400
1401 pub fn workspace_list(&self) -> Command {
1403 let mut command = self.new_command();
1404 command.arg("workspace").arg("list");
1405 self.add_shared_options(&mut command, false);
1406 command
1407 }
1408
1409 pub fn export(&self) -> Command {
1411 let mut command = self.new_command();
1412 command.arg("export");
1413 self.add_shared_options(&mut command, false);
1414 command
1415 }
1416
1417 pub fn format(&self) -> Command {
1419 let mut command = self.new_command();
1420 command.arg("format");
1421 self.add_shared_options(&mut command, false);
1422 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1424 command
1425 }
1426
1427 pub fn check(&self) -> Command {
1429 let mut command = self.new_command();
1430 command.arg("check");
1431 self.add_shared_options(&mut command, false);
1432 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1434 command
1435 }
1436
1437 pub fn build(&self) -> Command {
1439 let mut command = self.new_command();
1440 command.arg("build");
1441 self.add_shared_options(&mut command, false);
1442 command
1443 }
1444
1445 pub fn version(&self) -> Command {
1446 let mut command = self.new_command();
1447 command.arg("version");
1448 self.add_shared_options(&mut command, false);
1449 command
1450 }
1451
1452 pub fn self_version(&self) -> Command {
1453 let mut command = self.new_command();
1454 command.arg("self").arg("version");
1455 self.add_shared_options(&mut command, false);
1456 command
1457 }
1458
1459 pub fn self_update(&self) -> Command {
1460 let mut command = self.new_command();
1461 command.arg("self").arg("update");
1462 self.add_shared_options(&mut command, false);
1463 command
1464 }
1465
1466 pub fn publish(&self) -> Command {
1468 let mut command = self.new_command();
1469 command.arg("publish");
1470 self.add_shared_options(&mut command, false);
1471 command
1472 }
1473
1474 pub fn python_find(&self) -> Command {
1476 let mut command = self.new_command();
1477 command
1478 .arg("python")
1479 .arg("find")
1480 .env(EnvVars::UV_PREVIEW, "1")
1481 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1482 self.add_shared_options(&mut command, false);
1483 command
1484 }
1485
1486 pub fn python_list(&self) -> Command {
1488 let mut command = self.new_command();
1489 command
1490 .arg("python")
1491 .arg("list")
1492 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1493 self.add_shared_options(&mut command, false);
1494 command
1495 }
1496
1497 pub fn python_install(&self) -> Command {
1499 let mut command = self.new_command();
1500 command.arg("python").arg("install");
1501 self.add_shared_options(&mut command, true);
1502 command
1503 }
1504
1505 pub fn python_uninstall(&self) -> Command {
1507 let mut command = self.new_command();
1508 command.arg("python").arg("uninstall");
1509 self.add_shared_options(&mut command, true);
1510 command
1511 }
1512
1513 pub fn python_upgrade(&self) -> Command {
1515 let mut command = self.new_command();
1516 command.arg("python").arg("upgrade");
1517 self.add_shared_options(&mut command, true);
1518 command
1519 }
1520
1521 pub fn python_pin(&self) -> Command {
1523 let mut command = self.new_command();
1524 command.arg("python").arg("pin");
1525 self.add_shared_options(&mut command, true);
1526 command
1527 }
1528
1529 pub fn python_dir(&self) -> Command {
1531 let mut command = self.new_command();
1532 command.arg("python").arg("dir");
1533 self.add_shared_options(&mut command, true);
1534 command
1535 }
1536
1537 pub fn run(&self) -> Command {
1539 let mut command = self.new_command();
1540 command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1");
1541 self.add_shared_options(&mut command, true);
1542 command
1543 }
1544
1545 pub fn tool_run(&self) -> Command {
1547 let mut command = self.new_command();
1548 command
1549 .arg("tool")
1550 .arg("run")
1551 .env(EnvVars::UV_SHOW_RESOLUTION, "1");
1552 self.add_shared_options(&mut command, false);
1553 command
1554 }
1555
1556 pub fn tool_upgrade(&self) -> Command {
1558 let mut command = self.new_command();
1559 command.arg("tool").arg("upgrade");
1560 self.add_shared_options(&mut command, false);
1561 command
1562 }
1563
1564 pub fn tool_install(&self) -> Command {
1566 let mut command = self.new_command();
1567 command.arg("tool").arg("install");
1568 self.add_shared_options(&mut command, false);
1569 command
1570 }
1571
1572 pub fn tool_list(&self) -> Command {
1574 let mut command = self.new_command();
1575 command.arg("tool").arg("list");
1576 self.add_shared_options(&mut command, false);
1577 command
1578 }
1579
1580 pub fn tool_dir(&self) -> Command {
1582 let mut command = self.new_command();
1583 command.arg("tool").arg("dir");
1584 self.add_shared_options(&mut command, false);
1585 command
1586 }
1587
1588 pub fn tool_uninstall(&self) -> Command {
1590 let mut command = self.new_command();
1591 command.arg("tool").arg("uninstall");
1592 self.add_shared_options(&mut command, false);
1593 command
1594 }
1595
1596 pub fn add(&self) -> Command {
1598 let mut command = self.new_command();
1599 command.arg("add");
1600 self.add_shared_options(&mut command, false);
1601 command
1602 }
1603
1604 pub fn remove(&self) -> Command {
1606 let mut command = self.new_command();
1607 command.arg("remove");
1608 self.add_shared_options(&mut command, false);
1609 command
1610 }
1611
1612 pub fn tree(&self) -> Command {
1614 let mut command = self.new_command();
1615 command.arg("tree");
1616 self.add_shared_options(&mut command, false);
1617 command
1618 }
1619
1620 pub fn clean(&self) -> Command {
1622 let mut command = self.new_command();
1623 command.arg("cache").arg("clean");
1624 self.add_shared_options(&mut command, false);
1625 command
1626 }
1627
1628 pub fn prune(&self) -> Command {
1630 let mut command = self.new_command();
1631 command.arg("cache").arg("prune");
1632 self.add_shared_options(&mut command, false);
1633 command
1634 }
1635
1636 pub fn cache_size(&self) -> Command {
1638 let mut command = self.new_command();
1639 command.arg("cache").arg("size");
1640 self.add_shared_options(&mut command, false);
1641 command
1642 }
1643
1644 pub fn build_backend(&self) -> Command {
1648 let mut command = self.new_command();
1649 command.arg("build-backend");
1650 self.add_shared_options(&mut command, false);
1651 command
1652 }
1653
1654 pub fn interpreter(&self) -> PathBuf {
1658 let venv = &self.venv;
1659 if cfg!(unix) {
1660 venv.join("bin").join("python")
1661 } else if cfg!(windows) {
1662 venv.join("Scripts").join("python.exe")
1663 } else {
1664 unimplemented!("Only Windows and Unix are supported")
1665 }
1666 }
1667
1668 pub fn python_command(&self) -> Command {
1669 let mut interpreter = self.interpreter();
1670
1671 if !interpreter.exists() {
1673 interpreter.clone_from(
1674 &self
1675 .python_versions
1676 .first()
1677 .expect("At least one Python version is required")
1678 .1,
1679 );
1680 }
1681
1682 let mut command = Self::new_command_with(&interpreter);
1683 command
1684 .arg("-B")
1687 .env(EnvVars::PYTHONUTF8, "1");
1689
1690 self.add_shared_env(&mut command, false);
1691
1692 command
1693 }
1694
1695 pub fn auth_login(&self) -> Command {
1697 let mut command = self.new_command();
1698 command.arg("auth").arg("login");
1699 self.add_shared_options(&mut command, false);
1700 command
1701 }
1702
1703 pub fn auth_logout(&self) -> Command {
1705 let mut command = self.new_command();
1706 command.arg("auth").arg("logout");
1707 self.add_shared_options(&mut command, false);
1708 command
1709 }
1710
1711 pub fn auth_helper(&self) -> Command {
1713 let mut command = self.new_command();
1714 command.arg("auth").arg("helper");
1715 self.add_shared_options(&mut command, false);
1716 command
1717 }
1718
1719 pub fn auth_token(&self) -> Command {
1721 let mut command = self.new_command();
1722 command.arg("auth").arg("token");
1723 self.add_shared_options(&mut command, false);
1724 command
1725 }
1726
1727 #[must_use]
1731 pub fn with_real_home(mut self) -> Self {
1732 if let Some(home) = env::var_os(EnvVars::HOME) {
1733 self.extra_env
1734 .push((EnvVars::HOME.to_string().into(), home));
1735 }
1736 self.extra_env.push((
1739 EnvVars::XDG_CONFIG_HOME.into(),
1740 self.user_config_dir.as_os_str().into(),
1741 ));
1742 self
1743 }
1744
1745 pub fn assert_command(&self, command: &str) -> Assert {
1747 self.python_command()
1748 .arg("-c")
1749 .arg(command)
1750 .current_dir(&self.temp_dir)
1751 .assert()
1752 }
1753
1754 pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
1756 self.python_command()
1757 .arg(file.as_ref())
1758 .current_dir(&self.temp_dir)
1759 .assert()
1760 }
1761
1762 pub fn assert_installed(&self, package: &'static str, version: &'static str) {
1764 self.assert_command(
1765 format!("import {package} as package; print(package.__version__, end='')").as_str(),
1766 )
1767 .success()
1768 .stdout(version);
1769 }
1770
1771 pub fn assert_not_installed(&self, package: &'static str) {
1773 self.assert_command(format!("import {package}").as_str())
1774 .failure();
1775 }
1776
1777 pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
1779 let mut patterns = Vec::new();
1780
1781 if path.as_ref().exists() {
1783 patterns.push(Self::path_pattern(
1784 path.as_ref()
1785 .canonicalize()
1786 .expect("Failed to create canonical path"),
1787 ));
1788 }
1789
1790 patterns.push(Self::path_pattern(path));
1792
1793 patterns
1794 }
1795
1796 fn path_pattern(path: impl AsRef<Path>) -> String {
1798 format!(
1799 r"{}\\?/?",
1801 regex::escape(&path.as_ref().simplified_display().to_string())
1802 .replace(r"\\", r"(\\|\/)")
1805 )
1806 }
1807
1808 pub fn python_path(&self) -> OsString {
1809 if cfg!(unix) {
1810 env::join_paths(
1812 self.python_versions
1813 .iter()
1814 .map(|(version, _)| self.python_dir.join(version.to_string())),
1815 )
1816 .unwrap()
1817 } else {
1818 env::join_paths(
1820 self.python_versions
1821 .iter()
1822 .map(|(_, executable)| executable.parent().unwrap().to_path_buf()),
1823 )
1824 .unwrap()
1825 }
1826 }
1827
1828 pub fn filters(&self) -> Vec<(&str, &str)> {
1830 self.filters
1833 .iter()
1834 .map(|(p, r)| (p.as_str(), r.as_str()))
1835 .chain(INSTA_FILTERS.iter().copied())
1836 .collect()
1837 }
1838
1839 #[cfg(windows)]
1841 pub fn filters_without_standard_filters(&self) -> Vec<(&str, &str)> {
1842 self.filters
1843 .iter()
1844 .map(|(p, r)| (p.as_str(), r.as_str()))
1845 .collect()
1846 }
1847
1848 pub fn python_kind(&self) -> &'static str {
1850 "python"
1851 }
1852
1853 pub fn site_packages(&self) -> PathBuf {
1855 site_packages_path(
1856 &self.venv,
1857 &format!(
1858 "{}{}",
1859 self.python_kind(),
1860 self.python_version.as_ref().expect(
1861 "A Python version must be provided to retrieve the test site packages path"
1862 )
1863 ),
1864 )
1865 }
1866
1867 pub fn reset_venv(&self) {
1869 self.create_venv();
1870 }
1871
1872 fn create_venv(&self) {
1874 let executable = get_python(
1875 self.python_version
1876 .as_ref()
1877 .expect("A Python version must be provided to create a test virtual environment"),
1878 );
1879 create_venv_from_executable(&self.venv, &self.cache_dir, &executable, &self.uv_bin);
1880 }
1881
1882 pub fn copy_ecosystem_project(&self, name: &str) {
1893 let project_dir = PathBuf::from(format!("../../test/ecosystem/{name}"));
1894 self.temp_dir.copy_from(project_dir, &["*"]).unwrap();
1895 if let Err(err) = fs_err::remove_file(self.temp_dir.join("uv.lock")) {
1897 assert_eq!(
1898 err.kind(),
1899 io::ErrorKind::NotFound,
1900 "Failed to remove uv.lock: {err}"
1901 );
1902 }
1903 }
1904
1905 pub fn diff_lock(&self, change: impl Fn(&Self) -> Command) -> String {
1914 static TRIM_TRAILING_WHITESPACE: std::sync::LazyLock<Regex> =
1915 std::sync::LazyLock::new(|| Regex::new(r"(?m)^\s+$").unwrap());
1916
1917 let lock_path = ChildPath::new(self.temp_dir.join("uv.lock"));
1918 let old_lock = fs_err::read_to_string(&lock_path).unwrap();
1919 let (snapshot, output) = run_and_format(
1920 change(self),
1921 self.filters(),
1922 "diff_lock",
1923 Some(WindowsFilters::Platform),
1924 None,
1925 );
1926 assert!(output.status.success(), "{snapshot}");
1927 let new_lock = fs_err::read_to_string(&lock_path).unwrap();
1928 diff_snapshot(&old_lock, &new_lock, 10)
1929 }
1930
1931 pub fn read(&self, file: impl AsRef<Path>) -> String {
1933 fs_err::read_to_string(self.temp_dir.join(&file))
1934 .unwrap_or_else(|_| panic!("Missing file: `{}`", file.user_display()))
1935 }
1936
1937 fn new_command(&self) -> Command {
1940 Self::new_command_with(&self.uv_bin)
1941 }
1942
1943 fn new_command_with(bin: &Path) -> Command {
1949 let mut command = Command::new(bin);
1950
1951 let passthrough = [
1952 EnvVars::PATH,
1954 EnvVars::RUST_LOG,
1956 EnvVars::RUST_BACKTRACE,
1957 EnvVars::SYSTEMDRIVE,
1959 EnvVars::RUST_MIN_STACK,
1961 EnvVars::UV_STACK_SIZE,
1962 EnvVars::ALL_PROXY,
1964 EnvVars::HTTPS_PROXY,
1965 EnvVars::HTTP_PROXY,
1966 EnvVars::NO_PROXY,
1967 EnvVars::SSL_CERT_DIR,
1968 EnvVars::SSL_CERT_FILE,
1969 EnvVars::UV_NATIVE_TLS,
1970 EnvVars::UV_SYSTEM_CERTS,
1971 ];
1972
1973 for env_var in EnvVars::all_names()
1974 .iter()
1975 .filter(|name| !passthrough.contains(name))
1976 {
1977 command.env_remove(env_var);
1978 }
1979
1980 command
1981 }
1982}
1983
1984pub fn diff_snapshot(old: &str, new: &str, context_radius: usize) -> String {
1987 static TRIM_TRAILING_WHITESPACE: std::sync::LazyLock<Regex> =
1988 std::sync::LazyLock::new(|| Regex::new(r"(?m)^\s+$").unwrap());
1989
1990 let diff = similar::TextDiff::from_lines(old, new);
1991 let unified = diff
1992 .unified_diff()
1993 .context_radius(context_radius)
1994 .header("old", "new")
1995 .to_string();
1996 TRIM_TRAILING_WHITESPACE
2000 .replace_all(&unified, "")
2001 .into_owned()
2002}
2003
2004#[macro_export]
2008macro_rules! diff_uv_snapshot {
2009 ($filters:expr, $old:expr, $spawnable:expr, @$snapshot:literal) => {{
2010 let new = $crate::capture_uv_snapshot!($filters, $spawnable);
2011 let snapshot = $crate::diff_snapshot($old, &new, 3);
2012 let mut settings = ::insta::Settings::clone_current();
2013 let description = match settings.description() {
2015 Some(description) => format!("{description}\n\nUnfiltered diff:\n{snapshot}"),
2016 None => format!("Unfiltered diff:\n{snapshot}"),
2017 };
2018 settings.set_description(description);
2019 settings.add_filter(r"^--- old\n\+\+\+ new\n", "");
2020 settings.add_filter(r"(?m)^@@.*$", "...");
2021 settings.add_filter(r"\n$", "\n...\n");
2022 settings.bind(|| {
2023 ::insta::assert_snapshot!(snapshot, @$snapshot);
2024 });
2025 new
2026 }};
2027}
2028
2029#[macro_export]
2031macro_rules! capture_uv_snapshot {
2032 ($filters:expr, $spawnable:expr) => {{
2033 let (snapshot, _) = $crate::run_and_format_silent(
2035 $spawnable,
2036 &$filters,
2037 $crate::function_name!(),
2038 Some($crate::WindowsFilters::Platform),
2039 None,
2040 );
2041 snapshot
2042 }};
2043 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2044 let (snapshot, _) = $crate::run_and_format(
2045 $spawnable,
2046 &$filters,
2047 $crate::function_name!(),
2048 Some($crate::WindowsFilters::Platform),
2049 None,
2050 );
2051 ::insta::assert_snapshot!(snapshot, @$snapshot);
2052 snapshot
2053 }};
2054}
2055
2056pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
2057 if cfg!(unix) {
2058 venv.join("lib").join(python).join("site-packages")
2059 } else if cfg!(windows) {
2060 venv.join("Lib").join("site-packages")
2061 } else {
2062 unimplemented!("Only Windows and Unix are supported")
2063 }
2064}
2065
2066pub fn venv_bin_path(venv: impl AsRef<Path>) -> PathBuf {
2067 if cfg!(unix) {
2068 venv.as_ref().join("bin")
2069 } else if cfg!(windows) {
2070 venv.as_ref().join("Scripts")
2071 } else {
2072 unimplemented!("Only Windows and Unix are supported")
2073 }
2074}
2075
2076fn get_python(version: &PythonVersion) -> PathBuf {
2078 ManagedPythonInstallations::from_settings(None)
2079 .map(|installed_pythons| {
2080 installed_pythons
2081 .find_version(version)
2082 .expect("Tests are run on a supported platform")
2083 .next()
2084 .as_ref()
2085 .map(|python| python.executable(false))
2086 })
2087 .unwrap_or_default()
2090 .unwrap_or(PathBuf::from(version.to_string()))
2091}
2092
2093fn create_venv_from_executable<P: AsRef<Path>>(
2095 path: P,
2096 cache_dir: &ChildPath,
2097 python: &Path,
2098 uv_bin: &Path,
2099) {
2100 TestContext::new_command_with(uv_bin)
2101 .arg("venv")
2102 .arg(path.as_ref().as_os_str())
2103 .arg("--clear")
2104 .arg("--cache-dir")
2105 .arg(cache_dir.path())
2106 .arg("--python")
2107 .arg(python)
2108 .current_dir(path.as_ref().parent().unwrap())
2109 .assert()
2110 .success();
2111 ChildPath::new(path.as_ref()).assert(predicate::path::is_dir());
2112}
2113
2114pub fn python_path_with_versions(
2118 temp_dir: &ChildPath,
2119 python_versions: &[&str],
2120) -> anyhow::Result<OsString> {
2121 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
2122 Ok(env::join_paths(
2123 python_installations_for_versions(temp_dir, python_versions, &download_list)?
2124 .into_iter()
2125 .map(|path| path.parent().unwrap().to_path_buf()),
2126 )?)
2127}
2128
2129fn python_installations_for_versions(
2133 temp_dir: &ChildPath,
2134 python_versions: &[&str],
2135 download_list: &ManagedPythonDownloadList,
2136) -> anyhow::Result<Vec<PathBuf>> {
2137 let cache = Cache::from_path(temp_dir.child("cache").to_path_buf())
2138 .init_no_wait()?
2139 .expect("No cache contention when setting up Python in tests");
2140 let _preview = uv_preview::test::with_features(&[]);
2141 let selected_pythons = python_versions
2142 .iter()
2143 .map(|python_version| {
2144 if let Ok(python) = PythonInstallation::find(
2145 &PythonRequest::parse(python_version),
2146 EnvironmentPreference::OnlySystem,
2147 PythonPreference::Managed,
2148 download_list,
2149 &cache,
2150 ) {
2151 python.into_interpreter().sys_executable().to_owned()
2152 } else {
2153 panic!("Could not find Python {python_version} for test\nTry `cargo run python install` first, or refer to CONTRIBUTING.md");
2154 }
2155 })
2156 .collect::<Vec<_>>();
2157
2158 assert!(
2159 python_versions.is_empty() || !selected_pythons.is_empty(),
2160 "Failed to fulfill requested test Python versions: {selected_pythons:?}"
2161 );
2162
2163 Ok(selected_pythons)
2164}
2165
2166#[derive(Debug, Copy, Clone)]
2167pub enum WindowsFilters {
2168 Platform,
2169 Universal,
2170}
2171
2172pub fn apply_filters<T: AsRef<str>>(mut snapshot: String, filters: impl AsRef<[(T, T)]>) -> String {
2174 for (matcher, replacement) in filters.as_ref() {
2175 let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?");
2177 if re.is_match(&snapshot) {
2178 snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string();
2179 }
2180 }
2181 snapshot
2182}
2183
2184#[expect(clippy::print_stderr)]
2188pub fn run_and_format<T: AsRef<str>>(
2189 command: impl BorrowMut<Command>,
2190 filters: impl AsRef<[(T, T)]>,
2191 function_name: &str,
2192 windows_filters: Option<WindowsFilters>,
2193 input: Option<&str>,
2194) -> (String, Output) {
2195 let (snapshot, output) =
2196 run_and_format_silent(command, filters, function_name, windows_filters, input);
2197 eprintln!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Unfiltered output ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2198 eprintln!(
2199 "----- stdout -----\n{}\n----- stderr -----\n{}",
2200 String::from_utf8_lossy(&output.stdout),
2201 String::from_utf8_lossy(&output.stderr),
2202 );
2203 eprintln!("────────────────────────────────────────────────────────────────────────────────\n");
2204 (snapshot, output)
2205}
2206
2207#[doc(hidden)]
2209pub fn run_and_format_silent<T: AsRef<str>>(
2210 mut command: impl BorrowMut<Command>,
2211 filters: impl AsRef<[(T, T)]>,
2212 function_name: &str,
2213 windows_filters: Option<WindowsFilters>,
2214 input: Option<&str>,
2215) -> (String, Output) {
2216 let program = command
2217 .borrow_mut()
2218 .get_program()
2219 .to_string_lossy()
2220 .to_string();
2221
2222 if let Ok(root) = env::var(EnvVars::TRACING_DURATIONS_TEST_ROOT) {
2224 #[expect(clippy::assertions_on_constants)]
2226 {
2227 assert!(
2228 cfg!(feature = "tracing-durations-export"),
2229 "You need to enable the tracing-durations-export feature to use `TRACING_DURATIONS_TEST_ROOT`"
2230 );
2231 }
2232 command.borrow_mut().env(
2233 EnvVars::TRACING_DURATIONS_FILE,
2234 Path::new(&root).join(function_name).with_extension("jsonl"),
2235 );
2236 }
2237
2238 let output = if let Some(input) = input {
2239 let mut child = command
2240 .borrow_mut()
2241 .stdin(Stdio::piped())
2242 .stdout(Stdio::piped())
2243 .stderr(Stdio::piped())
2244 .spawn()
2245 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"));
2246 child
2247 .stdin
2248 .as_mut()
2249 .expect("Failed to open stdin")
2250 .write_all(input.as_bytes())
2251 .expect("Failed to write to stdin");
2252
2253 child
2254 .wait_with_output()
2255 .unwrap_or_else(|err| panic!("Failed to read output from {program}: {err}"))
2256 } else {
2257 command
2258 .borrow_mut()
2259 .output()
2260 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"))
2261 };
2262
2263 let mut snapshot = apply_filters(
2264 format!(
2265 "success: {:?}\nexit_code: {}\n----- stdout -----\n{}\n----- stderr -----\n{}",
2266 output.status.success(),
2267 output.status.code().unwrap_or(!0),
2268 String::from_utf8_lossy(&output.stdout),
2269 String::from_utf8_lossy(&output.stderr),
2270 ),
2271 filters,
2272 );
2273
2274 if cfg!(windows) {
2279 if let Some(windows_filters) = windows_filters {
2280 let windows_only_deps = [
2282 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2283 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2284 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2285 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2286 ];
2287 let mut removed_packages = 0;
2288 for windows_only_dep in windows_only_deps {
2289 let re = Regex::new(windows_only_dep).unwrap();
2291 if re.is_match(&snapshot) {
2292 snapshot = re.replace(&snapshot, "").to_string();
2293 removed_packages += 1;
2294 }
2295 }
2296 if removed_packages > 0 {
2297 for i in 1..20 {
2298 for verb in match windows_filters {
2299 WindowsFilters::Platform => [
2300 "Resolved",
2301 "Prepared",
2302 "Installed",
2303 "Checked",
2304 "Uninstalled",
2305 ]
2306 .iter(),
2307 WindowsFilters::Universal => {
2308 ["Prepared", "Installed", "Checked", "Uninstalled"].iter()
2309 }
2310 } {
2311 snapshot = snapshot.replace(
2312 &format!("{verb} {} packages", i + removed_packages),
2313 &format!("{verb} {} package{}", i, if i > 1 { "s" } else { "" }),
2314 );
2315 }
2316 }
2317 }
2318 }
2319 }
2320
2321 (snapshot, output)
2322}
2323
2324pub fn copy_dir_ignore(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {
2326 for entry in ignore::Walk::new(&src) {
2327 let entry = entry?;
2328 let relative = entry.path().strip_prefix(&src)?;
2329 let ty = entry.file_type().unwrap();
2330 if ty.is_dir() {
2331 fs_err::create_dir(dst.as_ref().join(relative))?;
2332 } else {
2333 fs_err::copy(entry.path(), dst.as_ref().join(relative))?;
2334 }
2335 }
2336 Ok(())
2337}
2338
2339pub fn make_project(dir: &Path, name: &str, body: &str) -> anyhow::Result<()> {
2341 let pyproject_toml = formatdoc! {r#"
2342 [project]
2343 name = "{name}"
2344 version = "0.1.0"
2345 requires-python = ">=3.11,<3.13"
2346 {body}
2347
2348 [build-system]
2349 requires = ["uv_build>=0.9.0,<10000"]
2350 build-backend = "uv_build"
2351 "#
2352 };
2353 fs_err::create_dir_all(dir)?;
2354 fs_err::write(dir.join("pyproject.toml"), pyproject_toml)?;
2355 fs_err::create_dir_all(dir.join("src").join(name))?;
2356 fs_err::write(dir.join("src").join(name).join("__init__.py"), "")?;
2357 Ok(())
2358}
2359
2360pub const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
2362 "Z2l0aHViCg==",
2363 "cGF0Cg==",
2364 "MTFBQlVDUjZBMERMUTQ3aVphN3hPdV9qQmhTMkZUeHZ4ZE13OHczakxuZndsV2ZlZjc2cE53eHBWS2tiRUFwdnpmUk8zV0dDSUhicDFsT01aago=",
2365];
2366
2367#[cfg(not(windows))]
2369pub const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[
2370 "Z2l0aHViCg==",
2371 "cGF0Cg==",
2372 "MTFBQlVDUjZBMDJTOFYwMTM4YmQ0bV9uTXpueWhxZDBrcllROTQ5SERTeTI0dENKZ2lmdzIybDFSR2s1SE04QW8xTUVYQ1I0Q1YxYUdPRGpvZQo=",
2373];
2374
2375pub const READ_ONLY_GITHUB_SSH_DEPLOY_KEY: &str = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNBeTF1SnNZK1JXcWp1NkdIY3Z6a3AwS21yWDEwdmo3RUZqTkpNTkRqSGZPZ0FBQUpqWUpwVnAyQ2FWCmFRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQXkxdUpzWStSV3FqdTZHSGN2emtwMEttclgxMHZqN0VGak5KTU5EakhmT2cKQUFBRUMwbzBnd1BxbGl6TFBJOEFXWDVaS2dVZHJyQ2ptMDhIQm9FenB4VDg3MXBqTFc0bXhqNUZhcU83b1lkeS9PU25RcQphdGZYUytQc1FXTTBrdzBPTWQ4NkFBQUFFR3R2Ym5OMGFVQmhjM1J5WVd3dWMyZ0JBZ01FQlE9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K";
2376
2377pub fn decode_token(content: &[&str]) -> String {
2380 content
2381 .iter()
2382 .map(|part| base64.decode(part).unwrap())
2383 .map(|decoded| {
2384 std::str::from_utf8(decoded.as_slice())
2385 .unwrap()
2386 .trim_end()
2387 .to_string()
2388 })
2389 .join("_")
2390}
2391
2392#[tokio::main(flavor = "current_thread")]
2395pub async fn download_to_disk(url: &str, path: &Path) {
2396 let trusted_hosts: Vec<_> = env::var(EnvVars::UV_INSECURE_HOST)
2397 .unwrap_or_default()
2398 .split(' ')
2399 .map(|h| uv_configuration::TrustedHost::from_str(h).unwrap())
2400 .collect();
2401
2402 let client = uv_client::BaseClientBuilder::default()
2403 .allow_insecure_host(trusted_hosts)
2404 .build()
2405 .expect("failed to build base client");
2406 let url = url.parse().unwrap();
2407 let response = client
2408 .for_host(&url)
2409 .get(reqwest::Url::from(url))
2410 .send()
2411 .await
2412 .unwrap();
2413
2414 let mut file = fs_err::tokio::File::create(path).await.unwrap();
2415 let mut stream = response.bytes_stream();
2416 while let Some(chunk) = stream.next().await {
2417 file.write_all(&chunk.unwrap()).await.unwrap();
2418 }
2419 file.sync_all().await.unwrap();
2420}
2421
2422#[cfg(unix)]
2427pub struct ReadOnlyDirectoryGuard {
2428 path: PathBuf,
2429 original_mode: u32,
2430}
2431
2432#[cfg(unix)]
2433impl ReadOnlyDirectoryGuard {
2434 pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
2437 use std::os::unix::fs::PermissionsExt;
2438 let path = path.into();
2439 let metadata = fs_err::metadata(&path)?;
2440 let original_mode = metadata.permissions().mode();
2441 let readonly_mode = original_mode & !0o222;
2443 fs_err::set_permissions(&path, std::fs::Permissions::from_mode(readonly_mode))?;
2444 Ok(Self {
2445 path,
2446 original_mode,
2447 })
2448 }
2449}
2450
2451#[cfg(unix)]
2452impl Drop for ReadOnlyDirectoryGuard {
2453 fn drop(&mut self) {
2454 use std::os::unix::fs::PermissionsExt;
2455 let _ = fs_err::set_permissions(
2456 &self.path,
2457 std::fs::Permissions::from_mode(self.original_mode),
2458 );
2459 }
2460}
2461
2462#[doc(hidden)]
2466#[macro_export]
2467macro_rules! function_name {
2468 () => {{
2469 fn f() {}
2470 fn type_name_of_val<T>(_: T) -> &'static str {
2471 std::any::type_name::<T>()
2472 }
2473 let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
2474 while let Some(rest) = name.strip_suffix("::{{closure}}") {
2475 name = rest;
2476 }
2477 name
2478 }};
2479}
2480
2481#[macro_export]
2486macro_rules! uv_snapshot {
2487 ($spawnable:expr, @$snapshot:literal) => {{
2488 uv_snapshot!($crate::INSTA_FILTERS.to_vec(), $spawnable, @$snapshot)
2489 }};
2490 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2491 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), None);
2493 ::insta::assert_snapshot!(snapshot, @$snapshot);
2494 output
2495 }};
2496 ($filters:expr, $spawnable:expr, input=$input:expr, @$snapshot:literal) => {{
2497 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), Some($input));
2499 ::insta::assert_snapshot!(snapshot, @$snapshot);
2500 output
2501 }};
2502 ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{
2503 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), None, None);
2505 ::insta::assert_snapshot!(snapshot, @$snapshot);
2506 output
2507 }};
2508 ($filters:expr, universal_windows_filters=true, $spawnable:expr, @$snapshot:literal) => {{
2509 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Universal), None);
2511 ::insta::assert_snapshot!(snapshot, @$snapshot);
2512 output
2513 }};
2514}