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.0rc1";
48const LATEST_PYTHON_3_14: &str = "3.14.7";
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_audit(&self) -> Command {
1585 let mut command = self.new_command();
1586 command.arg("tool").arg("audit");
1587 self.add_shared_options(&mut command, false);
1588 command
1589 }
1590
1591 pub fn tool_dir(&self) -> Command {
1593 let mut command = self.new_command();
1594 command.arg("tool").arg("dir");
1595 self.add_shared_options(&mut command, false);
1596 command
1597 }
1598
1599 pub fn tool_uninstall(&self) -> Command {
1601 let mut command = self.new_command();
1602 command.arg("tool").arg("uninstall");
1603 self.add_shared_options(&mut command, false);
1604 command
1605 }
1606
1607 pub fn add(&self) -> Command {
1609 let mut command = self.new_command();
1610 command.arg("add");
1611 self.add_shared_options(&mut command, false);
1612 command
1613 }
1614
1615 pub fn remove(&self) -> Command {
1617 let mut command = self.new_command();
1618 command.arg("remove");
1619 self.add_shared_options(&mut command, false);
1620 command
1621 }
1622
1623 pub fn tree(&self) -> Command {
1625 let mut command = self.new_command();
1626 command.arg("tree");
1627 self.add_shared_options(&mut command, false);
1628 command
1629 }
1630
1631 pub fn clean(&self) -> Command {
1633 let mut command = self.new_command();
1634 command.arg("cache").arg("clean");
1635 self.add_shared_options(&mut command, false);
1636 command
1637 }
1638
1639 pub fn prune(&self) -> Command {
1641 let mut command = self.new_command();
1642 command.arg("cache").arg("prune");
1643 self.add_shared_options(&mut command, false);
1644 command
1645 }
1646
1647 pub fn cache_size(&self) -> Command {
1649 let mut command = self.new_command();
1650 command.arg("cache").arg("size");
1651 self.add_shared_options(&mut command, false);
1652 command
1653 }
1654
1655 pub fn build_backend(&self) -> Command {
1659 let mut command = self.new_command();
1660 command.arg("build-backend");
1661 self.add_shared_options(&mut command, false);
1662 command
1663 }
1664
1665 pub fn interpreter(&self) -> PathBuf {
1669 let venv = &self.venv;
1670 if cfg!(unix) {
1671 venv.join("bin").join("python")
1672 } else if cfg!(windows) {
1673 venv.join("Scripts").join("python.exe")
1674 } else {
1675 unimplemented!("Only Windows and Unix are supported")
1676 }
1677 }
1678
1679 pub fn python_command(&self) -> Command {
1680 let mut interpreter = self.interpreter();
1681
1682 if !interpreter.exists() {
1684 interpreter.clone_from(
1685 &self
1686 .python_versions
1687 .first()
1688 .expect("At least one Python version is required")
1689 .1,
1690 );
1691 }
1692
1693 let mut command = Self::new_command_with(&interpreter);
1694 command
1695 .arg("-B")
1698 .env(EnvVars::PYTHONUTF8, "1");
1700
1701 self.add_shared_env(&mut command, false);
1702
1703 command
1704 }
1705
1706 pub fn auth_login(&self) -> Command {
1708 let mut command = self.new_command();
1709 command.arg("auth").arg("login");
1710 self.add_shared_options(&mut command, false);
1711 command
1712 }
1713
1714 pub fn auth_logout(&self) -> Command {
1716 let mut command = self.new_command();
1717 command.arg("auth").arg("logout");
1718 self.add_shared_options(&mut command, false);
1719 command
1720 }
1721
1722 pub fn auth_helper(&self) -> Command {
1724 let mut command = self.new_command();
1725 command.arg("auth").arg("helper");
1726 self.add_shared_options(&mut command, false);
1727 command
1728 }
1729
1730 pub fn auth_token(&self) -> Command {
1732 let mut command = self.new_command();
1733 command.arg("auth").arg("token");
1734 self.add_shared_options(&mut command, false);
1735 command
1736 }
1737
1738 #[must_use]
1742 pub fn with_real_home(mut self) -> Self {
1743 if let Some(home) = env::var_os(EnvVars::HOME) {
1744 self.extra_env
1745 .push((EnvVars::HOME.to_string().into(), home));
1746 }
1747 self.extra_env.push((
1750 EnvVars::XDG_CONFIG_HOME.into(),
1751 self.user_config_dir.as_os_str().into(),
1752 ));
1753 self
1754 }
1755
1756 pub fn assert_command(&self, command: &str) -> Assert {
1758 self.python_command()
1759 .arg("-c")
1760 .arg(command)
1761 .current_dir(&self.temp_dir)
1762 .assert()
1763 }
1764
1765 pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
1767 self.python_command()
1768 .arg(file.as_ref())
1769 .current_dir(&self.temp_dir)
1770 .assert()
1771 }
1772
1773 pub fn assert_installed(&self, package: &'static str, version: &'static str) {
1775 self.assert_command(
1776 format!("import {package} as package; print(package.__version__, end='')").as_str(),
1777 )
1778 .success()
1779 .stdout(version);
1780 }
1781
1782 pub fn assert_not_installed(&self, package: &'static str) {
1784 self.assert_command(format!("import {package}").as_str())
1785 .failure();
1786 }
1787
1788 pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
1790 let mut patterns = Vec::new();
1791
1792 if path.as_ref().exists() {
1794 patterns.push(Self::path_pattern(
1795 path.as_ref()
1796 .canonicalize()
1797 .expect("Failed to create canonical path"),
1798 ));
1799 }
1800
1801 patterns.push(Self::path_pattern(path));
1803
1804 patterns
1805 }
1806
1807 fn path_pattern(path: impl AsRef<Path>) -> String {
1809 format!(
1810 r"{}\\?/?",
1812 regex::escape(&path.as_ref().simplified_display().to_string())
1813 .replace(r"\\", r"(\\|\/)")
1816 )
1817 }
1818
1819 pub fn python_path(&self) -> OsString {
1820 if cfg!(unix) {
1821 env::join_paths(
1823 self.python_versions
1824 .iter()
1825 .map(|(version, _)| self.python_dir.join(version.to_string())),
1826 )
1827 .unwrap()
1828 } else {
1829 env::join_paths(
1831 self.python_versions
1832 .iter()
1833 .map(|(_, executable)| executable.parent().unwrap().to_path_buf()),
1834 )
1835 .unwrap()
1836 }
1837 }
1838
1839 pub fn filters(&self) -> Vec<(&str, &str)> {
1841 self.filters
1844 .iter()
1845 .map(|(p, r)| (p.as_str(), r.as_str()))
1846 .chain(INSTA_FILTERS.iter().copied())
1847 .collect()
1848 }
1849
1850 #[cfg(windows)]
1852 pub fn filters_without_standard_filters(&self) -> Vec<(&str, &str)> {
1853 self.filters
1854 .iter()
1855 .map(|(p, r)| (p.as_str(), r.as_str()))
1856 .collect()
1857 }
1858
1859 pub fn python_kind(&self) -> &'static str {
1861 "python"
1862 }
1863
1864 pub fn site_packages(&self) -> PathBuf {
1866 site_packages_path(
1867 &self.venv,
1868 &format!(
1869 "{}{}",
1870 self.python_kind(),
1871 self.python_version.as_ref().expect(
1872 "A Python version must be provided to retrieve the test site packages path"
1873 )
1874 ),
1875 )
1876 }
1877
1878 pub fn reset_venv(&self) {
1880 self.create_venv();
1881 }
1882
1883 fn create_venv(&self) {
1885 let executable = get_python(
1886 self.python_version
1887 .as_ref()
1888 .expect("A Python version must be provided to create a test virtual environment"),
1889 );
1890 create_venv_from_executable(&self.venv, &self.cache_dir, &executable, &self.uv_bin);
1891 }
1892
1893 pub fn copy_ecosystem_project(&self, name: &str) {
1904 let project_dir = PathBuf::from(format!("../../test/ecosystem/{name}"));
1905 self.temp_dir.copy_from(project_dir, &["*"]).unwrap();
1906 if let Err(err) = fs_err::remove_file(self.temp_dir.join("uv.lock")) {
1908 assert_eq!(
1909 err.kind(),
1910 io::ErrorKind::NotFound,
1911 "Failed to remove uv.lock: {err}"
1912 );
1913 }
1914 }
1915
1916 pub fn diff_lock(&self, change: impl Fn(&Self) -> Command) -> String {
1925 let lock_path = ChildPath::new(self.temp_dir.join("uv.lock"));
1926 let old_lock = fs_err::read_to_string(&lock_path).unwrap();
1927 let (snapshot, output) = run_and_format(
1928 change(self),
1929 self.filters(),
1930 "diff_lock",
1931 Some(WindowsFilters::Platform),
1932 None,
1933 );
1934 assert!(output.status.success(), "{snapshot}");
1935 let new_lock = fs_err::read_to_string(&lock_path).unwrap();
1936 diff_snapshot(&old_lock, &new_lock, 10)
1937 }
1938
1939 pub fn read(&self, file: impl AsRef<Path>) -> String {
1941 fs_err::read_to_string(self.temp_dir.join(&file))
1942 .unwrap_or_else(|_| panic!("Missing file: `{}`", file.user_display()))
1943 }
1944
1945 fn new_command(&self) -> Command {
1948 Self::new_command_with(&self.uv_bin)
1949 }
1950
1951 fn new_command_with(bin: &Path) -> Command {
1957 let mut command = Command::new(bin);
1958
1959 let passthrough = [
1960 EnvVars::PATH,
1962 EnvVars::RUST_LOG,
1964 EnvVars::RUST_BACKTRACE,
1965 EnvVars::SYSTEMDRIVE,
1967 EnvVars::RUST_MIN_STACK,
1969 EnvVars::UV_STACK_SIZE,
1970 EnvVars::ALL_PROXY,
1972 EnvVars::HTTPS_PROXY,
1973 EnvVars::HTTP_PROXY,
1974 EnvVars::NO_PROXY,
1975 EnvVars::SSL_CERT_DIR,
1976 EnvVars::SSL_CERT_FILE,
1977 EnvVars::UV_NATIVE_TLS,
1978 EnvVars::UV_SYSTEM_CERTS,
1979 ];
1980
1981 for env_var in EnvVars::all_names()
1982 .iter()
1983 .filter(|name| !passthrough.contains(name))
1984 {
1985 command.env_remove(env_var);
1986 }
1987
1988 command
1989 }
1990}
1991
1992pub fn diff_snapshot(old: &str, new: &str, context_radius: usize) -> String {
1995 let diff = similar::TextDiff::from_lines(old, new);
1996 let unified = diff
1997 .unified_diff()
1998 .context_radius(context_radius)
1999 .header("old", "new")
2000 .to_string();
2001 regex!(r"(?m)^\s+$").replace_all(&unified, "").into_owned()
2005}
2006
2007#[macro_export]
2011macro_rules! diff_uv_snapshot {
2012 ($filters:expr, $old:expr, $spawnable:expr, @$snapshot:literal) => {{
2013 let new = $crate::capture_uv_snapshot!($filters, $spawnable);
2014 let snapshot = $crate::diff_snapshot($old, &new, 3);
2015 let mut settings = ::insta::Settings::clone_current();
2016 let description = match settings.description() {
2018 Some(description) => format!("{description}\n\nUnfiltered diff:\n{snapshot}"),
2019 None => format!("Unfiltered diff:\n{snapshot}"),
2020 };
2021 settings.set_description(description);
2022 settings.add_filter(r"^--- old\n\+\+\+ new\n", "");
2023 settings.add_filter(r"(?m)^@@.*$", "...");
2024 settings.add_filter(r"\n$", "\n...\n");
2025 settings.bind(|| {
2026 ::insta::assert_snapshot!(snapshot, @$snapshot);
2027 });
2028 new
2029 }};
2030}
2031
2032#[macro_export]
2034macro_rules! capture_uv_snapshot {
2035 ($filters:expr, $spawnable:expr) => {{
2036 let (snapshot, _) = $crate::run_and_format_silent(
2038 $spawnable,
2039 &$filters,
2040 $crate::function_name!(),
2041 Some($crate::WindowsFilters::Platform),
2042 None,
2043 );
2044 snapshot
2045 }};
2046 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2047 let (snapshot, _) = $crate::run_and_format(
2048 $spawnable,
2049 &$filters,
2050 $crate::function_name!(),
2051 Some($crate::WindowsFilters::Platform),
2052 None,
2053 );
2054 ::insta::assert_snapshot!(snapshot, @$snapshot);
2055 snapshot
2056 }};
2057}
2058
2059pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
2060 if cfg!(unix) {
2061 venv.join("lib").join(python).join("site-packages")
2062 } else if cfg!(windows) {
2063 venv.join("Lib").join("site-packages")
2064 } else {
2065 unimplemented!("Only Windows and Unix are supported")
2066 }
2067}
2068
2069pub fn venv_bin_path(venv: impl AsRef<Path>) -> PathBuf {
2070 if cfg!(unix) {
2071 venv.as_ref().join("bin")
2072 } else if cfg!(windows) {
2073 venv.as_ref().join("Scripts")
2074 } else {
2075 unimplemented!("Only Windows and Unix are supported")
2076 }
2077}
2078
2079fn get_python(version: &PythonVersion) -> PathBuf {
2081 ManagedPythonInstallations::from_settings(None)
2082 .map(|installed_pythons| {
2083 installed_pythons
2084 .find_version(version)
2085 .expect("Tests are run on a supported platform")
2086 .next()
2087 .as_ref()
2088 .map(|python| python.executable(false))
2089 })
2090 .unwrap_or_default()
2093 .unwrap_or(PathBuf::from(version.to_string()))
2094}
2095
2096fn create_venv_from_executable<P: AsRef<Path>>(
2098 path: P,
2099 cache_dir: &ChildPath,
2100 python: &Path,
2101 uv_bin: &Path,
2102) {
2103 TestContext::new_command_with(uv_bin)
2104 .arg("venv")
2105 .arg(path.as_ref().as_os_str())
2106 .arg("--clear")
2107 .arg("--cache-dir")
2108 .arg(cache_dir.path())
2109 .arg("--python")
2110 .arg(python)
2111 .current_dir(path.as_ref().parent().unwrap())
2112 .assert()
2113 .success();
2114 ChildPath::new(path.as_ref()).assert(predicate::path::is_dir());
2115}
2116
2117pub fn python_path_with_versions(
2121 temp_dir: &ChildPath,
2122 python_versions: &[&str],
2123) -> anyhow::Result<OsString> {
2124 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
2125 Ok(env::join_paths(
2126 python_installations_for_versions(temp_dir, python_versions, &download_list)?
2127 .into_iter()
2128 .map(|path| path.parent().unwrap().to_path_buf()),
2129 )?)
2130}
2131
2132fn python_installations_for_versions(
2136 temp_dir: &ChildPath,
2137 python_versions: &[&str],
2138 download_list: &ManagedPythonDownloadList,
2139) -> anyhow::Result<Vec<PathBuf>> {
2140 let cache = Cache::from_path(temp_dir.child("cache").to_path_buf())
2141 .init_no_wait()?
2142 .expect("No cache contention when setting up Python in tests");
2143 let _preview = uv_preview::test::with_features(&[]);
2144 let selected_pythons = python_versions
2145 .iter()
2146 .map(|python_version| {
2147 if let Ok(python) = PythonInstallation::find(
2148 &PythonRequest::parse(python_version),
2149 EnvironmentPreference::OnlySystem,
2150 PythonPreference::Managed,
2151 download_list,
2152 &cache,
2153 ) {
2154 python.into_interpreter().sys_executable().to_owned()
2155 } else {
2156 panic!("Could not find Python {python_version} for test\nTry `cargo run python install` first, or refer to CONTRIBUTING.md");
2157 }
2158 })
2159 .collect::<Vec<_>>();
2160
2161 assert!(
2162 python_versions.is_empty() || !selected_pythons.is_empty(),
2163 "Failed to fulfill requested test Python versions: {selected_pythons:?}"
2164 );
2165
2166 Ok(selected_pythons)
2167}
2168
2169#[derive(Debug, Copy, Clone)]
2170pub enum WindowsFilters {
2171 Platform,
2172 Universal,
2173}
2174
2175pub fn apply_filters<T: AsRef<str>>(mut snapshot: String, filters: impl AsRef<[(T, T)]>) -> String {
2177 for (matcher, replacement) in filters.as_ref() {
2178 let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?");
2180 if re.is_match(&snapshot) {
2181 snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string();
2182 }
2183 }
2184 snapshot
2185}
2186
2187#[expect(clippy::print_stderr)]
2191pub fn run_and_format<T: AsRef<str>>(
2192 command: impl BorrowMut<Command>,
2193 filters: impl AsRef<[(T, T)]>,
2194 function_name: &str,
2195 windows_filters: Option<WindowsFilters>,
2196 input: Option<&str>,
2197) -> (String, Output) {
2198 let (snapshot, output) =
2199 run_and_format_silent(command, filters, function_name, windows_filters, input);
2200 eprintln!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Unfiltered output ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2201 eprintln!(
2202 "----- stdout -----\n{}\n----- stderr -----\n{}",
2203 String::from_utf8_lossy(&output.stdout),
2204 String::from_utf8_lossy(&output.stderr),
2205 );
2206 eprintln!("────────────────────────────────────────────────────────────────────────────────\n");
2207 (snapshot, output)
2208}
2209
2210#[doc(hidden)]
2212pub fn run_and_format_silent<T: AsRef<str>>(
2213 mut command: impl BorrowMut<Command>,
2214 filters: impl AsRef<[(T, T)]>,
2215 function_name: &str,
2216 windows_filters: Option<WindowsFilters>,
2217 input: Option<&str>,
2218) -> (String, Output) {
2219 let program = command
2220 .borrow_mut()
2221 .get_program()
2222 .to_string_lossy()
2223 .to_string();
2224
2225 if let Ok(root) = env::var(EnvVars::TRACING_DURATIONS_TEST_ROOT) {
2227 #[expect(clippy::assertions_on_constants)]
2229 {
2230 assert!(
2231 cfg!(feature = "tracing-durations-export"),
2232 "You need to enable the tracing-durations-export feature to use `TRACING_DURATIONS_TEST_ROOT`"
2233 );
2234 }
2235 command.borrow_mut().env(
2236 EnvVars::TRACING_DURATIONS_FILE,
2237 Path::new(&root).join(function_name).with_extension("jsonl"),
2238 );
2239 }
2240
2241 let output = if let Some(input) = input {
2242 let mut child = command
2243 .borrow_mut()
2244 .stdin(Stdio::piped())
2245 .stdout(Stdio::piped())
2246 .stderr(Stdio::piped())
2247 .spawn()
2248 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"));
2249 child
2250 .stdin
2251 .as_mut()
2252 .expect("Failed to open stdin")
2253 .write_all(input.as_bytes())
2254 .expect("Failed to write to stdin");
2255
2256 child
2257 .wait_with_output()
2258 .unwrap_or_else(|err| panic!("Failed to read output from {program}: {err}"))
2259 } else {
2260 command
2261 .borrow_mut()
2262 .output()
2263 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"))
2264 };
2265
2266 let mut snapshot = format!(
2267 "exit_code: {} ({})\n",
2268 output.status.code().unwrap_or(!0),
2269 if output.status.success() {
2270 "success"
2271 } else {
2272 "failure"
2273 },
2274 );
2275 if !output.stdout.is_empty() {
2276 snapshot.push_str("----- stdout -----\n");
2277 snapshot.push_str(&String::from_utf8_lossy(&output.stdout));
2278 }
2279 if !output.stderr.is_empty() {
2280 if !output.stdout.is_empty() {
2281 snapshot.push('\n');
2282 }
2283 snapshot.push_str("----- stderr -----\n");
2284 snapshot.push_str(&String::from_utf8_lossy(&output.stderr));
2285 }
2286 let mut snapshot = apply_filters(snapshot, filters);
2287
2288 if cfg!(windows) {
2293 if let Some(windows_filters) = windows_filters {
2294 let windows_only_deps = [
2296 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2297 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2298 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2299 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2300 ];
2301 let mut removed_packages = 0;
2302 for windows_only_dep in windows_only_deps {
2303 let re = Regex::new(windows_only_dep).unwrap();
2305 if re.is_match(&snapshot) {
2306 snapshot = re.replace(&snapshot, "").to_string();
2307 removed_packages += 1;
2308 }
2309 }
2310 if removed_packages > 0 {
2311 for i in 1..20 {
2312 for verb in match windows_filters {
2313 WindowsFilters::Platform => [
2314 "Resolved",
2315 "Prepared",
2316 "Installed",
2317 "Checked",
2318 "Uninstalled",
2319 ]
2320 .iter(),
2321 WindowsFilters::Universal => {
2322 ["Prepared", "Installed", "Checked", "Uninstalled"].iter()
2323 }
2324 } {
2325 snapshot = snapshot.replace(
2326 &format!("{verb} {} packages", i + removed_packages),
2327 &format!("{verb} {} package{}", i, if i > 1 { "s" } else { "" }),
2328 );
2329 }
2330 }
2331 }
2332 }
2333 }
2334
2335 (snapshot, output)
2336}
2337
2338pub fn copy_dir_ignore(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {
2340 for entry in ignore::Walk::new(&src) {
2341 let entry = entry?;
2342 let relative = entry.path().strip_prefix(&src)?;
2343 let ty = entry.file_type().unwrap();
2344 if ty.is_dir() {
2345 fs_err::create_dir(dst.as_ref().join(relative))?;
2346 } else {
2347 fs_err::copy(entry.path(), dst.as_ref().join(relative))?;
2348 }
2349 }
2350 Ok(())
2351}
2352
2353pub fn make_project(dir: &Path, name: &str, body: &str) -> anyhow::Result<()> {
2355 let pyproject_toml = formatdoc! {r#"
2356 [project]
2357 name = "{name}"
2358 version = "0.1.0"
2359 requires-python = ">=3.11,<3.13"
2360 {body}
2361
2362 [build-system]
2363 requires = ["uv_build>=0.9.0,<10000"]
2364 build-backend = "uv_build"
2365 "#
2366 };
2367 fs_err::create_dir_all(dir)?;
2368 fs_err::write(dir.join("pyproject.toml"), pyproject_toml)?;
2369 fs_err::create_dir_all(dir.join("src").join(name))?;
2370 fs_err::write(dir.join("src").join(name).join("__init__.py"), "")?;
2371 Ok(())
2372}
2373
2374pub const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
2376 "Z2l0aHViCg==",
2377 "cGF0Cg==",
2378 "MTFBQlVDUjZBMERMUTQ3aVphN3hPdV9qQmhTMkZUeHZ4ZE13OHczakxuZndsV2ZlZjc2cE53eHBWS2tiRUFwdnpmUk8zV0dDSUhicDFsT01aago=",
2379];
2380
2381#[cfg(not(windows))]
2383pub const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[
2384 "Z2l0aHViCg==",
2385 "cGF0Cg==",
2386 "MTFBQlVDUjZBMDJTOFYwMTM4YmQ0bV9uTXpueWhxZDBrcllROTQ5SERTeTI0dENKZ2lmdzIybDFSR2s1SE04QW8xTUVYQ1I0Q1YxYUdPRGpvZQo=",
2387];
2388
2389pub const READ_ONLY_GITHUB_SSH_DEPLOY_KEY: &str = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNBeTF1SnNZK1JXcWp1NkdIY3Z6a3AwS21yWDEwdmo3RUZqTkpNTkRqSGZPZ0FBQUpqWUpwVnAyQ2FWCmFRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQXkxdUpzWStSV3FqdTZHSGN2emtwMEttclgxMHZqN0VGak5KTU5EakhmT2cKQUFBRUMwbzBnd1BxbGl6TFBJOEFXWDVaS2dVZHJyQ2ptMDhIQm9FenB4VDg3MXBqTFc0bXhqNUZhcU83b1lkeS9PU25RcQphdGZYUytQc1FXTTBrdzBPTWQ4NkFBQUFFR3R2Ym5OMGFVQmhjM1J5WVd3dWMyZ0JBZ01FQlE9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K";
2390
2391pub fn decode_token(content: &[&str]) -> String {
2394 content
2395 .iter()
2396 .map(|part| base64.decode(part).unwrap())
2397 .map(|decoded| {
2398 std::str::from_utf8(decoded.as_slice())
2399 .unwrap()
2400 .trim_end()
2401 .to_string()
2402 })
2403 .join("_")
2404}
2405
2406#[tokio::main(flavor = "current_thread")]
2409pub async fn download_to_disk(url: &str, path: &Path) {
2410 let trusted_hosts: Vec<_> = env::var(EnvVars::UV_INSECURE_HOST)
2411 .unwrap_or_default()
2412 .split(' ')
2413 .map(|h| uv_configuration::TrustedHost::from_str(h).unwrap())
2414 .collect();
2415
2416 let client = uv_client::BaseClientBuilder::default()
2417 .allow_insecure_host(trusted_hosts)
2418 .build()
2419 .expect("failed to build base client");
2420 let url = url.parse().unwrap();
2421 let response = client
2422 .for_host(&url)
2423 .get(reqwest::Url::from(url))
2424 .send()
2425 .await
2426 .unwrap();
2427
2428 let mut file = fs_err::tokio::File::create(path).await.unwrap();
2429 let mut stream = response.bytes_stream();
2430 while let Some(chunk) = stream.next().await {
2431 file.write_all(&chunk.unwrap()).await.unwrap();
2432 }
2433 file.sync_all().await.unwrap();
2434}
2435
2436#[cfg(unix)]
2441pub struct ReadOnlyDirectoryGuard {
2442 path: PathBuf,
2443 original_mode: u32,
2444}
2445
2446#[cfg(unix)]
2447impl ReadOnlyDirectoryGuard {
2448 pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
2451 use std::os::unix::fs::PermissionsExt;
2452 let path = path.into();
2453 let metadata = fs_err::metadata(&path)?;
2454 let original_mode = metadata.permissions().mode();
2455 let readonly_mode = original_mode & !0o222;
2457 fs_err::set_permissions(&path, std::fs::Permissions::from_mode(readonly_mode))?;
2458 Ok(Self {
2459 path,
2460 original_mode,
2461 })
2462 }
2463}
2464
2465#[cfg(unix)]
2466impl Drop for ReadOnlyDirectoryGuard {
2467 fn drop(&mut self) {
2468 use std::os::unix::fs::PermissionsExt;
2469 let _ = fs_err::set_permissions(
2470 &self.path,
2471 std::fs::Permissions::from_mode(self.original_mode),
2472 );
2473 }
2474}
2475
2476#[doc(hidden)]
2480#[macro_export]
2481macro_rules! function_name {
2482 () => {{
2483 fn f() {}
2484 fn type_name_of_val<T>(_: T) -> &'static str {
2485 std::any::type_name::<T>()
2486 }
2487 let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
2488 while let Some(rest) = name.strip_suffix("::{{closure}}") {
2489 name = rest;
2490 }
2491 name
2492 }};
2493}
2494
2495#[macro_export]
2500macro_rules! uv_snapshot {
2501 ($spawnable:expr, @$snapshot:literal) => {{
2502 uv_snapshot!($crate::INSTA_FILTERS.to_vec(), $spawnable, @$snapshot)
2503 }};
2504 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2505 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), None);
2507 ::insta::assert_snapshot!(snapshot, @$snapshot);
2508 output
2509 }};
2510 ($filters:expr, $spawnable:expr, input=$input:expr, @$snapshot:literal) => {{
2511 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), Some($input));
2513 ::insta::assert_snapshot!(snapshot, @$snapshot);
2514 output
2515 }};
2516 ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{
2517 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), None, None);
2519 ::insta::assert_snapshot!(snapshot, @$snapshot);
2520 output
2521 }};
2522 ($filters:expr, universal_windows_filters=true, $spawnable:expr, @$snapshot:literal) => {{
2523 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Universal), None);
2525 ::insta::assert_snapshot!(snapshot, @$snapshot);
2526 output
2527 }};
2528}