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, 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 (
119 r"(?ms)^([ \t]*custom_certificates: )(?:None|Some\(\n.*?^[ \t]*\),\n[ \t]*\)),",
120 "${1}[CERTIFICATES],",
121 ),
122 (r"DEBUG Loaded \d+ certificate\(s\) from [^\n]+\n", ""),
124];
125
126pub struct TestContext {
133 pub root: ChildPath,
134 pub temp_dir: ChildPath,
135 pub cache_dir: ChildPath,
136 python_dir: ChildPath,
137 pub home_dir: ChildPath,
138 pub user_config_dir: ChildPath,
139 pub bin_dir: ChildPath,
140 pub venv: ChildPath,
141 pub workspace_root: PathBuf,
142
143 python_version: Option<PythonVersion>,
145
146 pub python_versions: Vec<(PythonVersion, PathBuf)>,
148
149 uv_bin: PathBuf,
151
152 filters: Vec<(String, String)>,
154
155 extra_env: Vec<(OsString, OsString)>,
157
158 #[allow(dead_code)]
159 _root: tempfile::TempDir,
160
161 #[allow(dead_code)]
164 _extra_tempdirs: Vec<tempfile::TempDir>,
165}
166
167impl TestContext {
168 pub fn new_with_bin(python_version: &str, uv_bin: PathBuf) -> Self {
172 let new = Self::new_with_versions_and_bin(&[python_version], uv_bin);
173 new.create_venv();
174 new
175 }
176
177 #[must_use]
179 pub fn with_exclude_newer(mut self, exclude_newer: &str) -> Self {
180 self.extra_env
181 .push((EnvVars::UV_EXCLUDE_NEWER.into(), exclude_newer.into()));
182 self
183 }
184
185 #[must_use]
187 pub fn with_http_timeout(mut self, http_timeout: &str) -> Self {
188 self.extra_env
189 .push((EnvVars::UV_HTTP_TIMEOUT.into(), http_timeout.into()));
190 self
191 }
192
193 #[must_use]
195 pub fn with_concurrent_installs(mut self, concurrent_installs: &str) -> Self {
196 self.extra_env.push((
197 EnvVars::UV_CONCURRENT_INSTALLS.into(),
198 concurrent_installs.into(),
199 ));
200 self
201 }
202
203 #[must_use]
208 pub fn with_filtered_counts(mut self) -> Self {
209 for verb in &[
210 "Resolved",
211 "Prepared",
212 "Installed",
213 "Uninstalled",
214 "Checked",
215 ] {
216 self.filters.push((
217 format!("{verb} \\d+ packages?"),
218 format!("{verb} [N] packages"),
219 ));
220 }
221 self.filters.push((
222 "Removed \\d+ files?".to_string(),
223 "Removed [N] files".to_string(),
224 ));
225 self
226 }
227
228 #[must_use]
230 pub fn with_filtered_cache_size(mut self) -> Self {
231 self.filters
233 .push((r"(?m)^\d+\n".to_string(), "[SIZE]\n".to_string()));
234 self.filters.push((
236 r"(?m)^\d+(\.\d+)? [KMGT]i?B\n".to_string(),
237 "[SIZE]\n".to_string(),
238 ));
239 self
240 }
241
242 #[must_use]
244 pub fn with_filtered_centralized_environment_hashes(mut self) -> Self {
245 self.filters.push((
246 r"`([\w.\[\]-]+)-[a-f0-9]{16}`".to_string(),
247 "`$1-[HASH]`".to_string(),
248 ));
249 self
250 }
251
252 #[must_use]
254 pub fn with_filtered_missing_file_error(mut self) -> Self {
255 self.filters.push((
258 r"[^:\n]* \(os error 2\)".to_string(),
259 " [OS ERROR 2]".to_string(),
260 ));
261 self.filters.push((
265 r"[^:\n]* \(os error 3\)".to_string(),
266 " [OS ERROR 2]".to_string(),
267 ));
268 self
269 }
270
271 #[must_use]
274 pub fn with_filtered_exe_suffix(mut self) -> Self {
275 self.filters
276 .push((regex::escape(env::consts::EXE_SUFFIX), String::new()));
277 self
278 }
279
280 #[must_use]
282 pub fn with_filtered_python_sources(mut self) -> Self {
283 self.filters.push((
284 "virtual environments, managed installations, or search path".to_string(),
285 "[PYTHON SOURCES]".to_string(),
286 ));
287 self.filters.push((
288 "virtual environments, managed installations, search path, or registry".to_string(),
289 "[PYTHON SOURCES]".to_string(),
290 ));
291 self.filters.push((
292 "virtual environments, search path, or registry".to_string(),
293 "[PYTHON SOURCES]".to_string(),
294 ));
295 self.filters.push((
296 "virtual environments, registry, or search path".to_string(),
297 "[PYTHON SOURCES]".to_string(),
298 ));
299 self.filters.push((
300 "virtual environments or search path".to_string(),
301 "[PYTHON SOURCES]".to_string(),
302 ));
303 self.filters.push((
304 "managed installations or search path".to_string(),
305 "[PYTHON SOURCES]".to_string(),
306 ));
307 self.filters.push((
308 "managed installations, search path, or registry".to_string(),
309 "[PYTHON SOURCES]".to_string(),
310 ));
311 self.filters.push((
312 "search path or registry".to_string(),
313 "[PYTHON SOURCES]".to_string(),
314 ));
315 self.filters.push((
316 "registry or search path".to_string(),
317 "[PYTHON SOURCES]".to_string(),
318 ));
319 self.filters
320 .push(("search path".to_string(), "[PYTHON SOURCES]".to_string()));
321 self
322 }
323
324 #[must_use]
327 pub fn with_filtered_python_names(mut self) -> Self {
328 for name in ["python", "pypy"] {
329 let suffix = if cfg!(windows) {
332 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
336 format!(r"(\d\.\d+|\d)?{exe_suffix}")
337 } else {
338 if name == "python" {
340 r"(\d\.\d+|\d)?(t|d|td)?".to_string()
342 } else {
343 r"(\d\.\d+|\d)(t|d|td)?".to_string()
345 }
346 };
347
348 self.filters.push((
349 format!(r"[\\/]{name}{suffix}"),
352 format!("/[{}]", name.to_uppercase()),
353 ));
354 }
355
356 self
357 }
358
359 #[must_use]
362 pub fn with_filtered_virtualenv_bin(mut self) -> Self {
363 self.filters.push((
364 format!(
365 r"[\\/]{}[\\/]",
366 venv_bin_path(PathBuf::new()).to_string_lossy()
367 ),
368 "/[BIN]/".to_string(),
369 ));
370 self.filters.push((
371 format!(r"[\\/]{}", venv_bin_path(PathBuf::new()).to_string_lossy()),
372 "/[BIN]".to_string(),
373 ));
374 self
375 }
376
377 #[must_use]
381 pub fn with_filtered_python_install_bin(mut self) -> Self {
382 let suffix = if cfg!(windows) {
385 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
386 format!(r"(\d\.\d+|\d)?{exe_suffix}")
388 } else {
389 r"\d\.\d+|\d".to_string()
391 };
392
393 if cfg!(unix) {
394 self.filters.push((
395 format!(r"[\\/]bin/python({suffix})"),
396 "/[INSTALL-BIN]/python$1".to_string(),
397 ));
398 self.filters.push((
399 format!(r"[\\/]bin/pypy({suffix})"),
400 "/[INSTALL-BIN]/pypy$1".to_string(),
401 ));
402 } else {
403 self.filters.push((
404 format!(r"[\\/]python({suffix})"),
405 "/[INSTALL-BIN]/python$1".to_string(),
406 ));
407 self.filters.push((
408 format!(r"[\\/]pypy({suffix})"),
409 "/[INSTALL-BIN]/pypy$1".to_string(),
410 ));
411 }
412 self
413 }
414
415 #[must_use]
420 pub fn with_pyvenv_cfg_filters(mut self) -> Self {
421 let added_filters = [
422 (r"home = .+".to_string(), "home = [PYTHON_HOME]".to_string()),
423 (
424 r"uv = \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?".to_string(),
425 "uv = [UV_VERSION]".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((
1049 r#"(\\|/)\.tmp[^\\/\s"'`]*"#.to_string(),
1050 "/[TMP]".to_string(),
1051 ));
1052
1053 filters.push((r"file:///".to_string(), "file://".to_string()));
1055
1056 filters.push((r"\\\\\?\\".to_string(), String::new()));
1058
1059 filters.push((r"127\.0\.0\.1:\d*".to_string(), "[LOCALHOST]".to_string()));
1061 filters.push((
1063 format!(
1064 r#"requires = \["uv_build>={},<[0-9.]+"\]"#,
1065 uv_version::version()
1066 ),
1067 r#"requires = ["uv_build>=[CURRENT_VERSION],<[NEXT_BREAKING]"]"#.to_string(),
1068 ));
1069 filters.push((
1071 r"environments-v(\d+)[\\/]([\w.\[\]-]+)-[a-f0-9]{16}".to_string(),
1072 "environments-v$1/$2-[HASH]".to_string(),
1073 ));
1074 filters.push((
1076 r"archive-v(\d+)[\\/][A-Za-z0-9\-\_]+".to_string(),
1077 "archive-v$1/[HASH]".to_string(),
1078 ));
1079
1080 Self {
1081 root: ChildPath::new(root.path()),
1082 temp_dir,
1083 cache_dir,
1084 python_dir,
1085 home_dir,
1086 user_config_dir,
1087 bin_dir,
1088 venv,
1089 workspace_root,
1090 python_version,
1091 python_versions,
1092 uv_bin,
1093 filters,
1094 extra_env: vec![],
1095 _root: root,
1096 _extra_tempdirs: vec![],
1097 }
1098 }
1099
1100 pub fn command(&self) -> Command {
1102 let mut command = self.new_command();
1103 self.add_shared_options(&mut command, true);
1104 command
1105 }
1106
1107 pub fn disallow_git_cli(bin_dir: &Path) -> std::io::Result<()> {
1108 let contents = r"#!/bin/sh
1109 echo 'error: `git` operations are not allowed — are you missing a cfg for the `git` feature?' >&2
1110 exit 127";
1111 let git = bin_dir.join(format!("git{}", env::consts::EXE_SUFFIX));
1112 fs_err::write(&git, contents)?;
1113
1114 #[cfg(unix)]
1115 {
1116 use std::os::unix::fs::PermissionsExt;
1117 let mut perms = fs_err::metadata(&git)?.permissions();
1118 perms.set_mode(0o755);
1119 fs_err::set_permissions(&git, perms)?;
1120 }
1121
1122 Ok(())
1123 }
1124
1125 #[must_use]
1130 pub fn with_git_lfs_config(mut self) -> Self {
1131 let git_lfs_config = self.root.child(".gitconfig");
1132 git_lfs_config
1133 .write_str(indoc! {r#"
1134 [filter "lfs"]
1135 clean = git-lfs clean -- %f
1136 smudge = git-lfs smudge -- %f
1137 process = git-lfs filter-process
1138 required = true
1139 "#})
1140 .expect("Failed to setup `git-lfs` filters");
1141
1142 self.extra_env.push((
1145 EnvVars::GIT_CONFIG_GLOBAL.into(),
1146 git_lfs_config.as_os_str().into(),
1147 ));
1148 self
1149 }
1150
1151 pub fn add_shared_options(&self, command: &mut Command, activate_venv: bool) {
1163 self.add_shared_args(command);
1164 self.add_shared_env(command, activate_venv);
1165 }
1166
1167 fn add_shared_args(&self, command: &mut Command) {
1169 command.arg("--cache-dir").arg(self.cache_dir.path());
1170 }
1171
1172 pub fn add_shared_env(&self, command: &mut Command, activate_venv: bool) {
1174 let path = env::join_paths(std::iter::once(self.bin_dir.to_path_buf()).chain(
1176 env::split_paths(&env::var(EnvVars::PATH).unwrap_or_default()),
1177 ))
1178 .unwrap();
1179
1180 if cfg!(not(windows)) {
1183 command.env(EnvVars::SHELL, "bash");
1184 }
1185
1186 command
1187 .env_remove(EnvVars::VIRTUAL_ENV)
1189 .env(EnvVars::UV_NO_WRAP, "1")
1191 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1193 .env(EnvVars::COLUMNS, "100")
1196 .env(EnvVars::PATH, path)
1197 .env(EnvVars::HOME, self.home_dir.as_os_str())
1198 .env(EnvVars::APPDATA, self.home_dir.as_os_str())
1199 .env(EnvVars::USERPROFILE, self.home_dir.as_os_str())
1200 .env(
1201 EnvVars::XDG_CONFIG_DIRS,
1202 self.home_dir.join("config").as_os_str(),
1203 )
1204 .env(
1205 EnvVars::XDG_DATA_HOME,
1206 self.home_dir.join("data").as_os_str(),
1207 )
1208 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1209 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "")
1210 .env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
1212 .env(EnvVars::UV_PYTHON_SEARCH_PATH, self.python_path())
1213 .env(EnvVars::UV_EXCLUDE_NEWER, TEST_TIMESTAMP)
1214 .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, TEST_TIMESTAMP)
1215 .env(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF, TEST_TIMESTAMP)
1216 .env(EnvVars::UV_PYTHON_NO_REGISTRY, "1")
1219 .env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "0")
1220 .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1")
1223 .env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path())
1237 .current_dir(self.temp_dir.path());
1238
1239 for (key, value) in &self.extra_env {
1240 command.env(key, value);
1241 }
1242
1243 if activate_venv {
1244 command.env(EnvVars::VIRTUAL_ENV, self.venv.as_os_str());
1245 }
1246
1247 if cfg!(unix) {
1248 command.env(EnvVars::LC_ALL, "C");
1250 }
1251 }
1252
1253 pub fn pip_compile(&self) -> Command {
1255 let mut command = self.new_command();
1256 command.arg("pip").arg("compile");
1257 self.add_shared_options(&mut command, true);
1258 command
1259 }
1260
1261 pub fn pip_sync(&self) -> Command {
1263 let mut command = self.new_command();
1264 command.arg("pip").arg("sync");
1265 self.add_shared_options(&mut command, true);
1266 command
1267 }
1268
1269 pub fn pip_show(&self) -> Command {
1270 let mut command = self.new_command();
1271 command.arg("pip").arg("show");
1272 self.add_shared_options(&mut command, true);
1273 command
1274 }
1275
1276 pub fn pip_freeze(&self) -> Command {
1278 let mut command = self.new_command();
1279 command.arg("pip").arg("freeze");
1280 self.add_shared_options(&mut command, true);
1281 command
1282 }
1283
1284 pub fn pip_check(&self) -> Command {
1286 let mut command = self.new_command();
1287 command.arg("pip").arg("check");
1288 self.add_shared_options(&mut command, true);
1289 command
1290 }
1291
1292 pub fn pip_list(&self) -> Command {
1293 let mut command = self.new_command();
1294 command.arg("pip").arg("list");
1295 self.add_shared_options(&mut command, true);
1296 command
1297 }
1298
1299 pub fn venv(&self) -> Command {
1301 let mut command = self.new_command();
1302 command.arg("venv");
1303 self.add_shared_options(&mut command, false);
1304 command
1305 }
1306
1307 pub fn pip_install(&self) -> Command {
1309 let mut command = self.new_command();
1310 command.arg("pip").arg("install");
1311 self.add_shared_options(&mut command, true);
1312 command
1313 }
1314
1315 pub fn pip_uninstall(&self) -> Command {
1317 let mut command = self.new_command();
1318 command.arg("pip").arg("uninstall");
1319 self.add_shared_options(&mut command, true);
1320 command
1321 }
1322
1323 pub fn pip_tree(&self) -> Command {
1325 let mut command = self.new_command();
1326 command.arg("pip").arg("tree");
1327 self.add_shared_options(&mut command, true);
1328 command
1329 }
1330
1331 pub fn pip_debug(&self) -> Command {
1333 let mut command = self.new_command();
1334 command.arg("pip").arg("debug");
1335 self.add_shared_options(&mut command, true);
1336 command
1337 }
1338
1339 pub fn help(&self) -> Command {
1341 let mut command = self.new_command();
1342 command.arg("help");
1343 self.add_shared_env(&mut command, false);
1344 command
1345 }
1346
1347 pub fn init(&self) -> Command {
1350 let mut command = self.new_command();
1351 command.arg("init");
1352 self.add_shared_options(&mut command, false);
1353 command
1354 }
1355
1356 pub fn sync(&self) -> Command {
1358 let mut command = self.new_command();
1359 command.arg("sync");
1360 self.add_shared_options(&mut command, false);
1361 command
1362 }
1363
1364 pub fn lock(&self) -> Command {
1366 let mut command = self.new_command();
1367 command.arg("lock");
1368 self.add_shared_options(&mut command, false);
1369 command
1370 }
1371
1372 pub fn upgrade(&self) -> Command {
1374 let mut command = self.new_command();
1375 command.arg("upgrade");
1376 self.add_shared_options(&mut command, false);
1377 command
1378 }
1379
1380 pub fn audit(&self) -> Command {
1382 let mut command = self.new_command();
1383 command.arg("audit");
1384 self.add_shared_options(&mut command, false);
1385 command
1386 }
1387
1388 pub fn workspace_metadata(&self) -> Command {
1390 let mut command = self.new_command();
1391 command.arg("workspace").arg("metadata");
1392 self.add_shared_options(&mut command, false);
1393 command
1394 }
1395
1396 pub fn workspace_dir(&self) -> Command {
1398 let mut command = self.new_command();
1399 command.arg("workspace").arg("dir");
1400 self.add_shared_options(&mut command, false);
1401 command
1402 }
1403
1404 pub fn workspace_list(&self) -> Command {
1406 let mut command = self.new_command();
1407 command.arg("workspace").arg("list");
1408 self.add_shared_options(&mut command, false);
1409 command
1410 }
1411
1412 pub fn export(&self) -> Command {
1414 let mut command = self.new_command();
1415 command.arg("export");
1416 self.add_shared_options(&mut command, false);
1417 command
1418 }
1419
1420 pub fn format(&self) -> Command {
1422 let mut command = self.new_command();
1423 command.arg("format");
1424 self.add_shared_options(&mut command, false);
1425 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1427 command
1428 }
1429
1430 pub fn check(&self) -> Command {
1432 let mut command = self.new_command();
1433 command.arg("check");
1434 self.add_shared_options(&mut command, false);
1435 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1437 command
1438 }
1439
1440 pub fn build(&self) -> Command {
1442 let mut command = self.new_command();
1443 command.arg("build");
1444 self.add_shared_options(&mut command, false);
1445 command
1446 }
1447
1448 pub fn version(&self) -> Command {
1449 let mut command = self.new_command();
1450 command.arg("version");
1451 self.add_shared_options(&mut command, false);
1452 command
1453 }
1454
1455 pub fn self_version(&self) -> Command {
1456 let mut command = self.new_command();
1457 command.arg("self").arg("version");
1458 self.add_shared_options(&mut command, false);
1459 command
1460 }
1461
1462 pub fn self_update(&self) -> Command {
1463 let mut command = self.new_command();
1464 command.arg("self").arg("update");
1465 self.add_shared_options(&mut command, false);
1466 command
1467 }
1468
1469 pub fn publish(&self) -> Command {
1471 let mut command = self.new_command();
1472 command.arg("publish");
1473 self.add_shared_options(&mut command, false);
1474 command
1475 }
1476
1477 pub fn python_find(&self) -> Command {
1479 let mut command = self.new_command();
1480 command
1481 .arg("python")
1482 .arg("find")
1483 .env(EnvVars::UV_PREVIEW, "1")
1484 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1485 self.add_shared_options(&mut command, false);
1486 command
1487 }
1488
1489 pub fn python_list(&self) -> Command {
1491 let mut command = self.new_command();
1492 command
1493 .arg("python")
1494 .arg("list")
1495 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1496 self.add_shared_options(&mut command, false);
1497 command
1498 }
1499
1500 pub fn python_install(&self) -> Command {
1502 let mut command = self.new_command();
1503 command.arg("python").arg("install");
1504 self.add_shared_options(&mut command, true);
1505 command
1506 }
1507
1508 pub fn python_uninstall(&self) -> Command {
1510 let mut command = self.new_command();
1511 command.arg("python").arg("uninstall");
1512 self.add_shared_options(&mut command, true);
1513 command
1514 }
1515
1516 pub fn python_upgrade(&self) -> Command {
1518 let mut command = self.new_command();
1519 command.arg("python").arg("upgrade");
1520 self.add_shared_options(&mut command, true);
1521 command
1522 }
1523
1524 pub fn python_pin(&self) -> Command {
1526 let mut command = self.new_command();
1527 command.arg("python").arg("pin");
1528 self.add_shared_options(&mut command, true);
1529 command
1530 }
1531
1532 pub fn python_dir(&self) -> Command {
1534 let mut command = self.new_command();
1535 command.arg("python").arg("dir");
1536 self.add_shared_options(&mut command, true);
1537 command
1538 }
1539
1540 pub fn run(&self) -> Command {
1542 let mut command = self.new_command();
1543 command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1");
1544 self.add_shared_options(&mut command, true);
1545 command
1546 }
1547
1548 pub fn tool_run(&self) -> Command {
1550 let mut command = self.new_command();
1551 command
1552 .arg("tool")
1553 .arg("run")
1554 .env(EnvVars::UV_SHOW_RESOLUTION, "1");
1555 self.add_shared_options(&mut command, false);
1556 command
1557 }
1558
1559 pub fn tool_upgrade(&self) -> Command {
1561 let mut command = self.new_command();
1562 command.arg("tool").arg("upgrade");
1563 self.add_shared_options(&mut command, false);
1564 command
1565 }
1566
1567 pub fn tool_install(&self) -> Command {
1569 let mut command = self.new_command();
1570 command.arg("tool").arg("install");
1571 self.add_shared_options(&mut command, false);
1572 command
1573 }
1574
1575 pub fn tool_list(&self) -> Command {
1577 let mut command = self.new_command();
1578 command.arg("tool").arg("list");
1579 self.add_shared_options(&mut command, false);
1580 command
1581 }
1582
1583 pub fn tool_dir(&self) -> Command {
1585 let mut command = self.new_command();
1586 command.arg("tool").arg("dir");
1587 self.add_shared_options(&mut command, false);
1588 command
1589 }
1590
1591 pub fn tool_uninstall(&self) -> Command {
1593 let mut command = self.new_command();
1594 command.arg("tool").arg("uninstall");
1595 self.add_shared_options(&mut command, false);
1596 command
1597 }
1598
1599 pub fn add(&self) -> Command {
1601 let mut command = self.new_command();
1602 command.arg("add");
1603 self.add_shared_options(&mut command, false);
1604 command
1605 }
1606
1607 pub fn remove(&self) -> Command {
1609 let mut command = self.new_command();
1610 command.arg("remove");
1611 self.add_shared_options(&mut command, false);
1612 command
1613 }
1614
1615 pub fn tree(&self) -> Command {
1617 let mut command = self.new_command();
1618 command.arg("tree");
1619 self.add_shared_options(&mut command, false);
1620 command
1621 }
1622
1623 pub fn clean(&self) -> Command {
1625 let mut command = self.new_command();
1626 command.arg("cache").arg("clean");
1627 self.add_shared_options(&mut command, false);
1628 command
1629 }
1630
1631 pub fn prune(&self) -> Command {
1633 let mut command = self.new_command();
1634 command.arg("cache").arg("prune");
1635 self.add_shared_options(&mut command, false);
1636 command
1637 }
1638
1639 pub fn cache_size(&self) -> Command {
1641 let mut command = self.new_command();
1642 command.arg("cache").arg("size");
1643 self.add_shared_options(&mut command, false);
1644 command
1645 }
1646
1647 pub fn build_backend(&self) -> Command {
1651 let mut command = self.new_command();
1652 command.arg("build-backend");
1653 self.add_shared_options(&mut command, false);
1654 command
1655 }
1656
1657 pub fn interpreter(&self) -> PathBuf {
1661 let venv = &self.venv;
1662 if cfg!(unix) {
1663 venv.join("bin").join("python")
1664 } else if cfg!(windows) {
1665 venv.join("Scripts").join("python.exe")
1666 } else {
1667 unimplemented!("Only Windows and Unix are supported")
1668 }
1669 }
1670
1671 pub fn python_command(&self) -> Command {
1672 let mut interpreter = self.interpreter();
1673
1674 if !interpreter.exists() {
1676 interpreter.clone_from(
1677 &self
1678 .python_versions
1679 .first()
1680 .expect("At least one Python version is required")
1681 .1,
1682 );
1683 }
1684
1685 let mut command = Self::new_command_with(&interpreter);
1686 command
1687 .arg("-B")
1690 .env(EnvVars::PYTHONUTF8, "1");
1692
1693 self.add_shared_env(&mut command, false);
1694
1695 command
1696 }
1697
1698 pub fn auth_login(&self) -> Command {
1700 let mut command = self.new_command();
1701 command.arg("auth").arg("login");
1702 self.add_shared_options(&mut command, false);
1703 command
1704 }
1705
1706 pub fn auth_logout(&self) -> Command {
1708 let mut command = self.new_command();
1709 command.arg("auth").arg("logout");
1710 self.add_shared_options(&mut command, false);
1711 command
1712 }
1713
1714 pub fn auth_helper(&self) -> Command {
1716 let mut command = self.new_command();
1717 command.arg("auth").arg("helper");
1718 self.add_shared_options(&mut command, false);
1719 command
1720 }
1721
1722 pub fn auth_token(&self) -> Command {
1724 let mut command = self.new_command();
1725 command.arg("auth").arg("token");
1726 self.add_shared_options(&mut command, false);
1727 command
1728 }
1729
1730 #[must_use]
1734 pub fn with_real_home(mut self) -> Self {
1735 if let Some(home) = env::var_os(EnvVars::HOME) {
1736 self.extra_env
1737 .push((EnvVars::HOME.to_string().into(), home));
1738 }
1739 self.extra_env.push((
1742 EnvVars::XDG_CONFIG_HOME.into(),
1743 self.user_config_dir.as_os_str().into(),
1744 ));
1745 self
1746 }
1747
1748 pub fn assert_command(&self, command: &str) -> Assert {
1750 self.python_command()
1751 .arg("-c")
1752 .arg(command)
1753 .current_dir(&self.temp_dir)
1754 .assert()
1755 }
1756
1757 pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
1759 self.python_command()
1760 .arg(file.as_ref())
1761 .current_dir(&self.temp_dir)
1762 .assert()
1763 }
1764
1765 pub fn assert_installed(&self, package: &'static str, version: &'static str) {
1767 self.assert_command(
1768 format!("import {package} as package; print(package.__version__, end='')").as_str(),
1769 )
1770 .success()
1771 .stdout(version);
1772 }
1773
1774 pub fn assert_not_installed(&self, package: &'static str) {
1776 self.assert_command(format!("import {package}").as_str())
1777 .failure();
1778 }
1779
1780 pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
1782 let mut patterns = Vec::new();
1783
1784 if path.as_ref().exists() {
1786 patterns.push(Self::path_pattern(
1787 path.as_ref()
1788 .canonicalize()
1789 .expect("Failed to create canonical path"),
1790 ));
1791 }
1792
1793 patterns.push(Self::path_pattern(path));
1795
1796 patterns
1797 }
1798
1799 fn path_pattern(path: impl AsRef<Path>) -> String {
1801 format!(
1802 r"{}\\?/?",
1804 regex::escape(&path.as_ref().simplified_display().to_string())
1805 .replace(r"\\", r"(\\|\/)")
1808 )
1809 }
1810
1811 pub fn python_path(&self) -> OsString {
1812 if cfg!(unix) {
1813 env::join_paths(
1815 self.python_versions
1816 .iter()
1817 .map(|(version, _)| self.python_dir.join(version.to_string())),
1818 )
1819 .unwrap()
1820 } else {
1821 env::join_paths(
1823 self.python_versions
1824 .iter()
1825 .map(|(_, executable)| executable.parent().unwrap().to_path_buf()),
1826 )
1827 .unwrap()
1828 }
1829 }
1830
1831 pub fn filters(&self) -> Vec<(&str, &str)> {
1833 self.filters
1836 .iter()
1837 .map(|(p, r)| (p.as_str(), r.as_str()))
1838 .chain(INSTA_FILTERS.iter().copied())
1839 .collect()
1840 }
1841
1842 #[cfg(windows)]
1844 pub fn filters_without_standard_filters(&self) -> Vec<(&str, &str)> {
1845 self.filters
1846 .iter()
1847 .map(|(p, r)| (p.as_str(), r.as_str()))
1848 .collect()
1849 }
1850
1851 pub fn python_kind(&self) -> &'static str {
1853 "python"
1854 }
1855
1856 pub fn site_packages(&self) -> PathBuf {
1858 site_packages_path(
1859 &self.venv,
1860 &format!(
1861 "{}{}",
1862 self.python_kind(),
1863 self.python_version.as_ref().expect(
1864 "A Python version must be provided to retrieve the test site packages path"
1865 )
1866 ),
1867 )
1868 }
1869
1870 pub fn reset_venv(&self) {
1872 self.create_venv();
1873 }
1874
1875 fn create_venv(&self) {
1877 let executable = get_python(
1878 self.python_version
1879 .as_ref()
1880 .expect("A Python version must be provided to create a test virtual environment"),
1881 );
1882 create_venv_from_executable(&self.venv, &self.cache_dir, &executable, &self.uv_bin);
1883 }
1884
1885 pub fn copy_ecosystem_project(&self, name: &str) {
1896 let project_dir = PathBuf::from(format!("../../test/ecosystem/{name}"));
1897 self.temp_dir.copy_from(project_dir, &["*"]).unwrap();
1898 if let Err(err) = fs_err::remove_file(self.temp_dir.join("uv.lock")) {
1900 assert_eq!(
1901 err.kind(),
1902 io::ErrorKind::NotFound,
1903 "Failed to remove uv.lock: {err}"
1904 );
1905 }
1906 }
1907
1908 pub fn diff_lock(&self, change: impl Fn(&Self) -> Command) -> String {
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 let diff = similar::TextDiff::from_lines(old, new);
1988 let unified = diff
1989 .unified_diff()
1990 .context_radius(context_radius)
1991 .header("old", "new")
1992 .to_string();
1993 regex!(r"(?m)^\s+$").replace_all(&unified, "").into_owned()
1997}
1998
1999#[macro_export]
2003macro_rules! diff_uv_snapshot {
2004 ($filters:expr, $old:expr, $spawnable:expr, @$snapshot:literal) => {{
2005 let new = $crate::capture_uv_snapshot!($filters, $spawnable);
2006 let snapshot = $crate::diff_snapshot($old, &new, 3);
2007 let mut settings = ::insta::Settings::clone_current();
2008 let description = match settings.description() {
2010 Some(description) => format!("{description}\n\nUnfiltered diff:\n{snapshot}"),
2011 None => format!("Unfiltered diff:\n{snapshot}"),
2012 };
2013 settings.set_description(description);
2014 settings.add_filter(r"^--- old\n\+\+\+ new\n", "");
2015 settings.add_filter(r"(?m)^@@.*$", "...");
2016 settings.add_filter(r"\n$", "\n...\n");
2017 settings.bind(|| {
2018 ::insta::assert_snapshot!(snapshot, @$snapshot);
2019 });
2020 new
2021 }};
2022}
2023
2024#[macro_export]
2026macro_rules! capture_uv_snapshot {
2027 ($filters:expr, $spawnable:expr) => {{
2028 let (snapshot, _) = $crate::run_and_format_silent(
2030 $spawnable,
2031 &$filters,
2032 $crate::function_name!(),
2033 Some($crate::WindowsFilters::Platform),
2034 None,
2035 );
2036 snapshot
2037 }};
2038 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2039 let (snapshot, _) = $crate::run_and_format(
2040 $spawnable,
2041 &$filters,
2042 $crate::function_name!(),
2043 Some($crate::WindowsFilters::Platform),
2044 None,
2045 );
2046 ::insta::assert_snapshot!(snapshot, @$snapshot);
2047 snapshot
2048 }};
2049}
2050
2051pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
2052 if cfg!(unix) {
2053 venv.join("lib").join(python).join("site-packages")
2054 } else if cfg!(windows) {
2055 venv.join("Lib").join("site-packages")
2056 } else {
2057 unimplemented!("Only Windows and Unix are supported")
2058 }
2059}
2060
2061pub fn venv_bin_path(venv: impl AsRef<Path>) -> PathBuf {
2062 if cfg!(unix) {
2063 venv.as_ref().join("bin")
2064 } else if cfg!(windows) {
2065 venv.as_ref().join("Scripts")
2066 } else {
2067 unimplemented!("Only Windows and Unix are supported")
2068 }
2069}
2070
2071fn get_python(version: &PythonVersion) -> PathBuf {
2073 ManagedPythonInstallations::from_settings(None)
2074 .map(|installed_pythons| {
2075 installed_pythons
2076 .find_version(version)
2077 .expect("Tests are run on a supported platform")
2078 .next()
2079 .as_ref()
2080 .map(|python| python.executable(false))
2081 })
2082 .unwrap_or_default()
2085 .unwrap_or(PathBuf::from(version.to_string()))
2086}
2087
2088fn create_venv_from_executable<P: AsRef<Path>>(
2090 path: P,
2091 cache_dir: &ChildPath,
2092 python: &Path,
2093 uv_bin: &Path,
2094) {
2095 TestContext::new_command_with(uv_bin)
2096 .arg("venv")
2097 .arg(path.as_ref().as_os_str())
2098 .arg("--clear")
2099 .arg("--cache-dir")
2100 .arg(cache_dir.path())
2101 .arg("--python")
2102 .arg(python)
2103 .current_dir(path.as_ref().parent().unwrap())
2104 .assert()
2105 .success();
2106 ChildPath::new(path.as_ref()).assert(predicate::path::is_dir());
2107}
2108
2109pub fn python_path_with_versions(
2113 temp_dir: &ChildPath,
2114 python_versions: &[&str],
2115) -> anyhow::Result<OsString> {
2116 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
2117 Ok(env::join_paths(
2118 python_installations_for_versions(temp_dir, python_versions, &download_list)?
2119 .into_iter()
2120 .map(|path| path.parent().unwrap().to_path_buf()),
2121 )?)
2122}
2123
2124fn python_installations_for_versions(
2128 temp_dir: &ChildPath,
2129 python_versions: &[&str],
2130 download_list: &ManagedPythonDownloadList,
2131) -> anyhow::Result<Vec<PathBuf>> {
2132 let cache = Cache::from_path(temp_dir.child("cache").to_path_buf())
2133 .init_no_wait()?
2134 .expect("No cache contention when setting up Python in tests");
2135 let _preview = uv_preview::test::with_features(&[]);
2136 let selected_pythons = python_versions
2137 .iter()
2138 .map(|python_version| {
2139 if let Ok(python) = PythonInstallation::find(
2140 &PythonRequest::parse(python_version),
2141 EnvironmentPreference::OnlySystem,
2142 PythonPreference::Managed,
2143 download_list,
2144 &cache,
2145 ) {
2146 python.into_interpreter().sys_executable().to_owned()
2147 } else {
2148 panic!("Could not find Python {python_version} for test\nTry `cargo run python install` first, or refer to CONTRIBUTING.md");
2149 }
2150 })
2151 .collect::<Vec<_>>();
2152
2153 assert!(
2154 python_versions.is_empty() || !selected_pythons.is_empty(),
2155 "Failed to fulfill requested test Python versions: {selected_pythons:?}"
2156 );
2157
2158 Ok(selected_pythons)
2159}
2160
2161#[derive(Debug, Copy, Clone)]
2162pub enum WindowsFilters {
2163 Platform,
2164 Universal,
2165}
2166
2167pub fn apply_filters<T: AsRef<str>>(mut snapshot: String, filters: impl AsRef<[(T, T)]>) -> String {
2169 for (matcher, replacement) in filters.as_ref() {
2170 let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?");
2172 if re.is_match(&snapshot) {
2173 snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string();
2174 }
2175 }
2176 snapshot
2177}
2178
2179#[expect(clippy::print_stderr)]
2183pub fn run_and_format<T: AsRef<str>>(
2184 command: impl BorrowMut<Command>,
2185 filters: impl AsRef<[(T, T)]>,
2186 function_name: &str,
2187 windows_filters: Option<WindowsFilters>,
2188 input: Option<&str>,
2189) -> (String, Output) {
2190 let (snapshot, output) =
2191 run_and_format_silent(command, filters, function_name, windows_filters, input);
2192 eprintln!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Unfiltered output ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2193 eprintln!(
2194 "----- stdout -----\n{}\n----- stderr -----\n{}",
2195 String::from_utf8_lossy(&output.stdout),
2196 String::from_utf8_lossy(&output.stderr),
2197 );
2198 eprintln!("────────────────────────────────────────────────────────────────────────────────\n");
2199 (snapshot, output)
2200}
2201
2202#[doc(hidden)]
2204pub fn run_and_format_silent<T: AsRef<str>>(
2205 mut command: impl BorrowMut<Command>,
2206 filters: impl AsRef<[(T, T)]>,
2207 function_name: &str,
2208 windows_filters: Option<WindowsFilters>,
2209 input: Option<&str>,
2210) -> (String, Output) {
2211 let program = command
2212 .borrow_mut()
2213 .get_program()
2214 .to_string_lossy()
2215 .to_string();
2216
2217 if let Ok(root) = env::var(EnvVars::TRACING_DURATIONS_TEST_ROOT) {
2219 #[expect(clippy::assertions_on_constants)]
2221 {
2222 assert!(
2223 cfg!(feature = "tracing-durations-export"),
2224 "You need to enable the tracing-durations-export feature to use `TRACING_DURATIONS_TEST_ROOT`"
2225 );
2226 }
2227 command.borrow_mut().env(
2228 EnvVars::TRACING_DURATIONS_FILE,
2229 Path::new(&root).join(function_name).with_extension("jsonl"),
2230 );
2231 }
2232
2233 let output = if let Some(input) = input {
2234 let mut child = command
2235 .borrow_mut()
2236 .stdin(Stdio::piped())
2237 .stdout(Stdio::piped())
2238 .stderr(Stdio::piped())
2239 .spawn()
2240 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"));
2241 child
2242 .stdin
2243 .as_mut()
2244 .expect("Failed to open stdin")
2245 .write_all(input.as_bytes())
2246 .expect("Failed to write to stdin");
2247
2248 child
2249 .wait_with_output()
2250 .unwrap_or_else(|err| panic!("Failed to read output from {program}: {err}"))
2251 } else {
2252 command
2253 .borrow_mut()
2254 .output()
2255 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"))
2256 };
2257
2258 let mut snapshot = format!(
2259 "exit_code: {} ({})\n",
2260 output.status.code().unwrap_or(!0),
2261 if output.status.success() {
2262 "success"
2263 } else {
2264 "failure"
2265 },
2266 );
2267 if !output.stdout.is_empty() {
2268 snapshot.push_str("----- stdout -----\n");
2269 snapshot.push_str(&String::from_utf8_lossy(&output.stdout));
2270 }
2271 if !output.stderr.is_empty() {
2272 if !output.stdout.is_empty() {
2273 snapshot.push('\n');
2274 }
2275 snapshot.push_str("----- stderr -----\n");
2276 snapshot.push_str(&String::from_utf8_lossy(&output.stderr));
2277 }
2278 let mut snapshot = apply_filters(snapshot, filters);
2279
2280 if cfg!(windows) {
2285 if let Some(windows_filters) = windows_filters {
2286 let windows_only_deps = [
2288 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2289 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2290 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2291 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2292 ];
2293 let mut removed_packages = 0;
2294 for windows_only_dep in windows_only_deps {
2295 let re = Regex::new(windows_only_dep).unwrap();
2297 if re.is_match(&snapshot) {
2298 snapshot = re.replace(&snapshot, "").to_string();
2299 removed_packages += 1;
2300 }
2301 }
2302 if removed_packages > 0 {
2303 for i in 1..20 {
2304 for verb in match windows_filters {
2305 WindowsFilters::Platform => [
2306 "Resolved",
2307 "Prepared",
2308 "Installed",
2309 "Checked",
2310 "Uninstalled",
2311 ]
2312 .iter(),
2313 WindowsFilters::Universal => {
2314 ["Prepared", "Installed", "Checked", "Uninstalled"].iter()
2315 }
2316 } {
2317 snapshot = snapshot.replace(
2318 &format!("{verb} {} packages", i + removed_packages),
2319 &format!("{verb} {} package{}", i, if i > 1 { "s" } else { "" }),
2320 );
2321 }
2322 }
2323 }
2324 }
2325 }
2326
2327 (snapshot, output)
2328}
2329
2330pub fn copy_dir_ignore(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {
2332 for entry in ignore::Walk::new(&src) {
2333 let entry = entry?;
2334 let relative = entry.path().strip_prefix(&src)?;
2335 let ty = entry.file_type().unwrap();
2336 if ty.is_dir() {
2337 fs_err::create_dir(dst.as_ref().join(relative))?;
2338 } else {
2339 fs_err::copy(entry.path(), dst.as_ref().join(relative))?;
2340 }
2341 }
2342 Ok(())
2343}
2344
2345pub fn make_project(dir: &Path, name: &str, body: &str) -> anyhow::Result<()> {
2347 let pyproject_toml = formatdoc! {r#"
2348 [project]
2349 name = "{name}"
2350 version = "0.1.0"
2351 requires-python = ">=3.11,<3.13"
2352 {body}
2353
2354 [build-system]
2355 requires = ["uv_build>=0.9.0,<10000"]
2356 build-backend = "uv_build"
2357 "#
2358 };
2359 fs_err::create_dir_all(dir)?;
2360 fs_err::write(dir.join("pyproject.toml"), pyproject_toml)?;
2361 fs_err::create_dir_all(dir.join("src").join(name))?;
2362 fs_err::write(dir.join("src").join(name).join("__init__.py"), "")?;
2363 Ok(())
2364}
2365
2366pub const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
2368 "Z2l0aHViCg==",
2369 "cGF0Cg==",
2370 "MTFBQlVDUjZBMERMUTQ3aVphN3hPdV9qQmhTMkZUeHZ4ZE13OHczakxuZndsV2ZlZjc2cE53eHBWS2tiRUFwdnpmUk8zV0dDSUhicDFsT01aago=",
2371];
2372
2373#[cfg(not(windows))]
2375pub const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[
2376 "Z2l0aHViCg==",
2377 "cGF0Cg==",
2378 "MTFBQlVDUjZBMDJTOFYwMTM4YmQ0bV9uTXpueWhxZDBrcllROTQ5SERTeTI0dENKZ2lmdzIybDFSR2s1SE04QW8xTUVYQ1I0Q1YxYUdPRGpvZQo=",
2379];
2380
2381pub const READ_ONLY_GITHUB_SSH_DEPLOY_KEY: &str = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNBeTF1SnNZK1JXcWp1NkdIY3Z6a3AwS21yWDEwdmo3RUZqTkpNTkRqSGZPZ0FBQUpqWUpwVnAyQ2FWCmFRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQXkxdUpzWStSV3FqdTZHSGN2emtwMEttclgxMHZqN0VGak5KTU5EakhmT2cKQUFBRUMwbzBnd1BxbGl6TFBJOEFXWDVaS2dVZHJyQ2ptMDhIQm9FenB4VDg3MXBqTFc0bXhqNUZhcU83b1lkeS9PU25RcQphdGZYUytQc1FXTTBrdzBPTWQ4NkFBQUFFR3R2Ym5OMGFVQmhjM1J5WVd3dWMyZ0JBZ01FQlE9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K";
2382
2383pub fn decode_token(content: &[&str]) -> String {
2386 content
2387 .iter()
2388 .map(|part| base64.decode(part).unwrap())
2389 .map(|decoded| {
2390 std::str::from_utf8(decoded.as_slice())
2391 .unwrap()
2392 .trim_end()
2393 .to_string()
2394 })
2395 .join("_")
2396}
2397
2398#[tokio::main(flavor = "current_thread")]
2401pub async fn download_to_disk(url: &str, path: &Path) {
2402 let trusted_hosts: Vec<_> = env::var(EnvVars::UV_INSECURE_HOST)
2403 .unwrap_or_default()
2404 .split(' ')
2405 .map(|h| uv_configuration::TrustedHost::from_str(h).unwrap())
2406 .collect();
2407
2408 let client = uv_client::BaseClientBuilder::default()
2409 .allow_insecure_host(trusted_hosts)
2410 .build()
2411 .expect("failed to build base client");
2412 let url = url.parse().unwrap();
2413 let response = client
2414 .for_host(&url)
2415 .get(reqwest::Url::from(url))
2416 .send()
2417 .await
2418 .unwrap();
2419
2420 let mut file = fs_err::tokio::File::create(path).await.unwrap();
2421 let mut stream = response.bytes_stream();
2422 while let Some(chunk) = stream.next().await {
2423 file.write_all(&chunk.unwrap()).await.unwrap();
2424 }
2425 file.sync_all().await.unwrap();
2426}
2427
2428#[cfg(unix)]
2433pub struct ReadOnlyDirectoryGuard {
2434 path: PathBuf,
2435 original_mode: u32,
2436}
2437
2438#[cfg(unix)]
2439impl ReadOnlyDirectoryGuard {
2440 pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
2443 use std::os::unix::fs::PermissionsExt;
2444 let path = path.into();
2445 let metadata = fs_err::metadata(&path)?;
2446 let original_mode = metadata.permissions().mode();
2447 let readonly_mode = original_mode & !0o222;
2449 fs_err::set_permissions(&path, std::fs::Permissions::from_mode(readonly_mode))?;
2450 Ok(Self {
2451 path,
2452 original_mode,
2453 })
2454 }
2455}
2456
2457#[cfg(unix)]
2458impl Drop for ReadOnlyDirectoryGuard {
2459 fn drop(&mut self) {
2460 use std::os::unix::fs::PermissionsExt;
2461 let _ = fs_err::set_permissions(
2462 &self.path,
2463 std::fs::Permissions::from_mode(self.original_mode),
2464 );
2465 }
2466}
2467
2468#[doc(hidden)]
2472#[macro_export]
2473macro_rules! function_name {
2474 () => {{
2475 fn f() {}
2476 fn type_name_of_val<T>(_: T) -> &'static str {
2477 std::any::type_name::<T>()
2478 }
2479 let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
2480 while let Some(rest) = name.strip_suffix("::{{closure}}") {
2481 name = rest;
2482 }
2483 name
2484 }};
2485}
2486
2487#[macro_export]
2492macro_rules! uv_snapshot {
2493 ($spawnable:expr, @$snapshot:literal) => {{
2494 uv_snapshot!($crate::INSTA_FILTERS.to_vec(), $spawnable, @$snapshot)
2495 }};
2496 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2497 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), None);
2499 ::insta::assert_snapshot!(snapshot, @$snapshot);
2500 output
2501 }};
2502 ($filters:expr, $spawnable:expr, input=$input:expr, @$snapshot:literal) => {{
2503 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), Some($input));
2505 ::insta::assert_snapshot!(snapshot, @$snapshot);
2506 output
2507 }};
2508 ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{
2509 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), None, None);
2511 ::insta::assert_snapshot!(snapshot, @$snapshot);
2512 output
2513 }};
2514 ($filters:expr, universal_windows_filters=true, $spawnable:expr, @$snapshot:literal) => {{
2515 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Universal), None);
2517 ::insta::assert_snapshot!(snapshot, @$snapshot);
2518 output
2519 }};
2520}