1#![allow(dead_code, unreachable_pub)]
3
4pub mod archive;
5pub mod find_links;
6mod http_server;
7pub mod packse;
8pub mod pypi_proxy;
9mod vendor;
10
11use std::borrow::BorrowMut;
12use std::ffi::OsString;
13use std::io::Write as _;
14use std::iter::Iterator;
15use std::path::{Path, PathBuf};
16use std::process::{Command, Output, Stdio};
17use std::str::FromStr;
18use std::{env, io};
19use uv_python::downloads::ManagedPythonDownloadList;
20
21use assert_cmd::assert::{Assert, OutputAssertExt};
22use assert_fs::assert::PathAssert;
23use assert_fs::fixture::{
24 ChildPath, FileWriteStr, PathChild, PathCopy, PathCreateDir, SymlinkToFile,
25};
26use base64::{Engine, prelude::BASE64_STANDARD as base64};
27use futures::StreamExt;
28use indoc::{formatdoc, indoc};
29use itertools::Itertools;
30use predicates::prelude::predicate;
31use regex::{Regex, regex};
32use tokio::io::AsyncWriteExt;
33use walkdir::WalkDir;
34
35use uv_cache::{Cache, CacheBucket};
36use uv_fs::Simplified;
37use uv_python::managed::ManagedPythonInstallations;
38use uv_python::{
39 EnvironmentPreference, PythonInstallation, PythonPreference, PythonRequest, PythonVersion,
40};
41use uv_static::EnvVars;
42
43static TEST_TIMESTAMP: &str = "2024-03-25T00:00:00Z";
45
46pub const DEFAULT_PYTHON_VERSION: &str = "3.12";
47
48const LATEST_PYTHON_3_15: &str = "3.15.0rc2";
50const LATEST_PYTHON_3_14: &str = "3.14.7";
51const LATEST_PYTHON_3_13: &str = "3.13.15";
52pub const LATEST_PYTHON_3_12: &str = "3.12.14";
53const LATEST_PYTHON_3_11: &str = "3.11.16";
54const LATEST_PYTHON_3_10: &str = "3.10.21";
55
56#[macro_export]
63macro_rules! test_context {
64 ($python_version:expr) => {
65 $crate::TestContext::new_with_bin(
66 $python_version,
67 std::path::PathBuf::from(env!("CARGO_BIN_EXE_uv")),
68 )
69 };
70}
71
72#[macro_export]
79macro_rules! test_context_with_versions {
80 ($python_versions:expr) => {
81 $crate::TestContext::new_with_versions_and_bin(
82 $python_versions,
83 std::path::PathBuf::from(env!("CARGO_BIN_EXE_uv")),
84 )
85 };
86}
87
88#[macro_export]
93macro_rules! get_bin {
94 () => {
95 std::path::PathBuf::from(env!("CARGO_BIN_EXE_uv"))
96 };
97}
98
99#[doc(hidden)] pub const INSTA_FILTERS: &[(&str, &str)] = &[
101 (r"--cache-dir [^\s]+", "--cache-dir [CACHE_DIR]"),
102 (r"(\s|\()(\d+m )?(\d+\.)?\d+(ms|s)", "$1[TIME]"),
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]
181 pub fn with_cache_dir(mut self, cache_dir: impl AsRef<Path>) -> Self {
182 let cache_dir = if cache_dir.as_ref().is_absolute() {
183 cache_dir.as_ref().to_path_buf()
184 } else {
185 self.temp_dir
186 .join(cache_dir.as_ref().components().collect::<PathBuf>())
187 };
188
189 self.filters
190 .retain(|(_, replacement)| replacement != "[CACHE_DIR]/");
191 self.cache_dir = ChildPath::new(cache_dir);
192
193 for pattern in Self::path_patterns(&self.cache_dir) {
194 self.filters
195 .insert(0, (pattern, "[CACHE_DIR]/".to_string()));
196 }
197
198 self
199 }
200
201 pub fn cache_files(&self, bucket: CacheBucket) -> anyhow::Result<Vec<PathBuf>> {
203 let cache = Cache::from_path(self.cache_dir.path());
204 let mut files = Vec::new();
205 for entry in WalkDir::new(cache.bucket(bucket)).min_depth(1) {
206 let entry = entry?;
207 if entry.file_type().is_file() {
208 files.push(entry.path().to_path_buf());
209 }
210 }
211 files.sort();
212 Ok(files)
213 }
214
215 #[must_use]
217 pub fn with_env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
218 self.extra_env.push((key.into(), value.into()));
219 self
220 }
221
222 #[must_use]
224 pub fn with_exclude_newer(mut self, exclude_newer: &str) -> Self {
225 self.extra_env
226 .push((EnvVars::UV_EXCLUDE_NEWER.into(), exclude_newer.into()));
227 self
228 }
229
230 #[must_use]
232 pub fn with_http_timeout(mut self, http_timeout: &str) -> Self {
233 self.extra_env
234 .push((EnvVars::UV_HTTP_TIMEOUT.into(), http_timeout.into()));
235 self
236 }
237
238 #[must_use]
240 pub fn with_http_retries(mut self, http_retries: &str) -> Self {
241 self.extra_env
242 .push((EnvVars::UV_HTTP_RETRIES.into(), http_retries.into()));
243 self
244 }
245
246 #[must_use]
248 pub fn with_fast_http_retry(self) -> Self {
249 self.with_http_timeout("1").with_http_retries("1")
250 }
251
252 #[must_use]
254 pub fn with_concurrent_installs(mut self, concurrent_installs: &str) -> Self {
255 self.extra_env.push((
256 EnvVars::UV_CONCURRENT_INSTALLS.into(),
257 concurrent_installs.into(),
258 ));
259 self
260 }
261
262 #[must_use]
267 pub fn with_filtered_counts(mut self) -> Self {
268 for verb in &[
269 "Resolved",
270 "Prepared",
271 "Installed",
272 "Uninstalled",
273 "Checked",
274 ] {
275 self.filters.push((
276 format!("{verb} \\d+ packages?"),
277 format!("{verb} [N] packages"),
278 ));
279 }
280 self.with_filtered_file_counts()
281 }
282
283 #[must_use]
285 pub fn with_filtered_file_counts(mut self) -> Self {
286 self.filters.push((
287 "Removed \\d+ files?".to_string(),
288 "Removed [N] files".to_string(),
289 ));
290 self
291 }
292
293 #[must_use]
295 pub fn with_filtered_sizes(mut self) -> Self {
296 self.filters.push((
297 r"(\s|\()(\d+\.)?\d+(([KMGT]i)?B)".to_string(),
298 "$1[SIZE]$3".to_string(),
299 ));
300 self
301 }
302
303 #[must_use]
305 pub fn with_filtered_sizes_and_units(mut self) -> Self {
306 self.filters.push((
307 r"(\s|\()(\d+\.)?\d+([KMGT]i)?B".to_string(),
308 "$1[SIZE]".to_string(),
309 ));
310 self
311 }
312
313 #[must_use]
315 pub fn with_filtered_cache_size(mut self) -> Self {
316 self.filters
318 .push((r"(?m)^\d+\n".to_string(), "[SIZE]\n".to_string()));
319 self.filters.push((
321 r"(?m)^\d+(\.\d+)?( ?[KMGT]i?B)\n".to_string(),
322 "[SIZE]$2\n".to_string(),
323 ));
324 self
325 }
326
327 #[must_use]
329 pub fn with_filtered_centralized_environment_hashes(mut self) -> Self {
330 self.filters.push((
331 r"`([\w.\[\]-]+)-[a-f0-9]{16}`".to_string(),
332 "`$1-[HASH]`".to_string(),
333 ));
334 self
335 }
336
337 #[must_use]
339 pub fn with_filtered_missing_file_error(mut self) -> Self {
340 self.filters.push((
343 r"[^:\n]* \(os error 2\)".to_string(),
344 " [OS ERROR 2]".to_string(),
345 ));
346 self.filters.push((
350 r"[^:\n]* \(os error 3\)".to_string(),
351 " [OS ERROR 2]".to_string(),
352 ));
353 self
354 }
355
356 #[must_use]
359 pub fn with_filtered_exe_suffix(mut self) -> Self {
360 self.filters
361 .push((regex::escape(env::consts::EXE_SUFFIX), String::new()));
362 self
363 }
364
365 #[must_use]
367 pub fn with_filtered_python_sources(mut self) -> Self {
368 self.filters.push((
369 "virtual environments, managed installations, or search path".to_string(),
370 "[PYTHON SOURCES]".to_string(),
371 ));
372 self.filters.push((
373 "virtual environments, managed installations, search path, or registry".to_string(),
374 "[PYTHON SOURCES]".to_string(),
375 ));
376 self.filters.push((
377 "virtual environments, search path, or registry".to_string(),
378 "[PYTHON SOURCES]".to_string(),
379 ));
380 self.filters.push((
381 "virtual environments, registry, or search path".to_string(),
382 "[PYTHON SOURCES]".to_string(),
383 ));
384 self.filters.push((
385 "virtual environments or search path".to_string(),
386 "[PYTHON SOURCES]".to_string(),
387 ));
388 self.filters.push((
389 "managed installations or search path".to_string(),
390 "[PYTHON SOURCES]".to_string(),
391 ));
392 self.filters.push((
393 "managed installations, search path, or registry".to_string(),
394 "[PYTHON SOURCES]".to_string(),
395 ));
396 self.filters.push((
397 "search path or registry".to_string(),
398 "[PYTHON SOURCES]".to_string(),
399 ));
400 self.filters.push((
401 "registry or search path".to_string(),
402 "[PYTHON SOURCES]".to_string(),
403 ));
404 self.filters
405 .push(("search path".to_string(), "[PYTHON SOURCES]".to_string()));
406 self
407 }
408
409 #[must_use]
412 pub fn with_filtered_python_names(mut self) -> Self {
413 for name in ["python", "pypy"] {
414 let suffix = if cfg!(windows) {
417 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
421 format!(r"(\d\.\d+|\d)?{exe_suffix}")
422 } else {
423 if name == "python" {
425 r"(\d\.\d+|\d)?(t|d|td)?".to_string()
427 } else {
428 r"(\d\.\d+|\d)(t|d|td)?".to_string()
430 }
431 };
432
433 self.filters.push((
434 format!(r"[\\/]{name}{suffix}"),
437 format!("/[{}]", name.to_uppercase()),
438 ));
439 }
440
441 self
442 }
443
444 #[must_use]
447 pub fn with_filtered_virtualenv_bin(mut self) -> Self {
448 self.filters.push((
449 format!(
450 r"[\\/]{}[\\/]",
451 venv_bin_path(PathBuf::new()).to_string_lossy()
452 ),
453 "/[BIN]/".to_string(),
454 ));
455 self.filters.push((
456 format!(
457 r"[\\/]{}\b",
458 venv_bin_path(PathBuf::new()).to_string_lossy()
459 ),
460 "/[BIN]".to_string(),
461 ));
462 self
463 }
464
465 #[must_use]
469 pub fn with_filtered_python_install_bin(mut self) -> Self {
470 let suffix = if cfg!(windows) {
473 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
474 format!(r"(\d\.\d+|\d)?{exe_suffix}")
476 } else {
477 r"\d\.\d+|\d".to_string()
479 };
480
481 if cfg!(unix) {
482 self.filters.push((
483 format!(r"[\\/]bin/python({suffix})"),
484 "/[INSTALL-BIN]/python$1".to_string(),
485 ));
486 self.filters.push((
487 format!(r"[\\/]bin/pypy({suffix})"),
488 "/[INSTALL-BIN]/pypy$1".to_string(),
489 ));
490 } else {
491 self.filters.push((
492 format!(r"[\\/]python({suffix})"),
493 "/[INSTALL-BIN]/python$1".to_string(),
494 ));
495 self.filters.push((
496 format!(r"[\\/]pypy({suffix})"),
497 "/[INSTALL-BIN]/pypy$1".to_string(),
498 ));
499 }
500 self
501 }
502
503 #[must_use]
508 pub fn with_pyvenv_cfg_filters(mut self) -> Self {
509 let added_filters = [
510 (r"home = .+".to_string(), "home = [PYTHON_HOME]".to_string()),
511 (
512 r"uv = \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?".to_string(),
513 "uv = [UV_VERSION]".to_string(),
514 ),
515 ];
516 for filter in added_filters {
517 self.filters.insert(0, filter);
518 }
519 self
520 }
521
522 #[must_use]
525 pub fn with_filtered_python_symlinks(mut self) -> Self {
526 for (version, executable) in &self.python_versions {
527 if fs_err::symlink_metadata(executable).unwrap().is_symlink() {
528 self.filters.extend(
529 Self::path_patterns(executable.read_link().unwrap())
530 .into_iter()
531 .map(|pattern| (format! {" -> {pattern}"}, String::new())),
532 );
533 }
534 self.filters.push((
536 regex::escape(&format!(" -> [PYTHON-{version}]")),
537 String::new(),
538 ));
539 }
540 self
541 }
542
543 #[must_use]
545 pub fn with_filtered_path(mut self, path: &Path, name: &str) -> Self {
546 for pattern in Self::path_patterns(path)
550 .into_iter()
551 .map(|pattern| (pattern, format!("[{name}]/")))
552 {
553 self.filters.insert(0, pattern);
554 }
555 self
556 }
557
558 #[inline]
566 #[must_use]
567 pub fn with_filtered_link_mode_warning(mut self) -> Self {
568 let pattern = "warning: Failed to hardlink files; .*\n.*\n.*\n";
569 self.filters.push((pattern.to_string(), String::new()));
570 self
571 }
572
573 #[inline]
575 #[must_use]
576 pub fn with_filtered_not_executable(mut self) -> Self {
577 let pattern = if cfg!(unix) {
578 r"Permission denied \(os error 13\)"
579 } else {
580 r"\%1 is not a valid Win32 application. \(os error 193\)"
581 };
582 self.filters
583 .push((pattern.to_string(), "[PERMISSION DENIED]".to_string()));
584 self
585 }
586
587 #[must_use]
589 pub fn with_filtered_python_keys(mut self) -> Self {
590 let platform_re = r"(?x)
592 ( # We capture the group before the platform
593 (?:cpython|pypy|graalpy)# Python implementation
594 -
595 \d+\.\d+ # Major and minor version
596 (?: # The patch version is handled separately
597 \.
598 (?:
599 \[X\] # A previously filtered patch version [X]
600 | # OR
601 \[LATEST\] # A previously filtered latest patch version [LATEST]
602 | # OR
603 \d+ # An actual patch version
604 )
605 )? # (we allow the patch version to be missing entirely, e.g., in a request)
606 (?:(?:a|b|rc)[0-9]+)? # Pre-release version component, e.g., `a6` or `rc2`
607 (?:[td])? # A short variant, such as `t` (for freethreaded) or `d` (for debug)
608 (?:(\+[a-z]+)+)? # A long variant, such as `+freethreaded` or `+freethreaded+debug`
609 )
610 -
611 [a-z0-9]+ # Operating system (e.g., 'macos')
612 -
613 [a-z0-9_]+ # Architecture (e.g., 'aarch64')
614 -
615 [a-z]+ # Libc (e.g., 'none')
616";
617 self.filters
618 .push((platform_re.to_string(), "$1-[PLATFORM]".to_string()));
619 self
620 }
621
622 #[must_use]
624 pub fn with_filtered_latest_python_versions(mut self) -> Self {
625 for (minor, patch) in [
628 ("3.15", LATEST_PYTHON_3_15.strip_prefix("3.15.").unwrap()),
629 ("3.14", LATEST_PYTHON_3_14.strip_prefix("3.14.").unwrap()),
630 ("3.13", LATEST_PYTHON_3_13.strip_prefix("3.13.").unwrap()),
631 ("3.12", LATEST_PYTHON_3_12.strip_prefix("3.12.").unwrap()),
632 ("3.11", LATEST_PYTHON_3_11.strip_prefix("3.11.").unwrap()),
633 ("3.10", LATEST_PYTHON_3_10.strip_prefix("3.10.").unwrap()),
634 ] {
635 let pattern = format!(r"(\b){minor}\.{patch}(\b)");
637 let replacement = format!("${{1}}{minor}.[LATEST]${{2}}");
638 self.filters.push((pattern, replacement));
639 }
640 self
641 }
642
643 #[must_use]
645 #[cfg(windows)]
646 pub fn with_filtered_windows_temp_dir(mut self) -> Self {
647 let pattern = regex::escape(
648 &self
649 .temp_dir
650 .simplified_display()
651 .to_string()
652 .replace('/', "\\"),
653 );
654 self.filters.push((pattern, "[TEMP_DIR]".to_string()));
655 self
656 }
657
658 #[must_use]
660 pub fn with_filtered_compiled_file_count(mut self) -> Self {
661 self.filters.push((
662 r"compiled \d+ files".to_string(),
663 "compiled [COUNT] files".to_string(),
664 ));
665 self
666 }
667
668 #[must_use]
670 pub fn with_filtered_current_version(mut self) -> Self {
671 self.filters.push((
672 regex::escape(&format!("v{}", env!("CARGO_PKG_VERSION"))),
673 "v[CURRENT_VERSION]".to_string(),
674 ));
675 self
676 }
677
678 #[must_use]
680 pub fn with_cyclonedx_filters(mut self) -> Self {
681 self.filters.push((
682 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(),
683 "[SERIAL_NUMBER]".to_string(),
684 ));
685 self.filters.push((
686 r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#
687 .to_string(),
688 r#""timestamp": "[TIMESTAMP]""#.to_string(),
689 ));
690 self.filters.push((
691 r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#
692 .to_string(),
693 r#""name": "uv",
694 "version": "[VERSION]""#
695 .to_string(),
696 ));
697 self
698 }
699
700 #[must_use]
702 pub fn with_collapsed_whitespace(mut self) -> Self {
703 self.filters.push((r"[ \t]+".to_string(), " ".to_string()));
704 self
705 }
706
707 #[must_use]
709 pub fn with_python_download_cache(mut self) -> Self {
710 self.extra_env.push((
711 EnvVars::UV_PYTHON_CACHE_DIR.into(),
712 env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).unwrap_or_else(|| {
714 uv_cache::Cache::from_settings(false, None)
715 .unwrap()
716 .bucket(CacheBucket::Python)
717 .into()
718 }),
719 ));
720 self
721 }
722
723 #[must_use]
724 pub fn with_empty_python_install_mirror(mut self) -> Self {
725 self.extra_env.push((
726 EnvVars::UV_PYTHON_INSTALL_MIRROR.into(),
727 String::new().into(),
728 ));
729 self
730 }
731
732 #[must_use]
734 pub fn with_managed_python_dirs(mut self) -> Self {
735 let managed = self.temp_dir.join("managed");
736
737 self.extra_env.push((
738 EnvVars::UV_PYTHON_BIN_DIR.into(),
739 self.bin_dir.as_os_str().to_owned(),
740 ));
741 self.extra_env
742 .push((EnvVars::UV_PYTHON_INSTALL_DIR.into(), managed.into()));
743 self.extra_env
744 .push((EnvVars::UV_PYTHON_DOWNLOADS.into(), "automatic".into()));
745
746 self
747 }
748
749 #[must_use]
751 pub fn with_tool_dirs(mut self) -> Self {
752 self.extra_env.push((
753 EnvVars::UV_TOOL_DIR.into(),
754 self.temp_dir.join("tools").into(),
755 ));
756 self.extra_env.push((
757 EnvVars::XDG_BIN_HOME.into(),
758 self.temp_dir.join("bin").into(),
759 ));
760
761 self
762 }
763
764 #[must_use]
765 pub fn with_versions_as_managed(mut self, versions: &[&str]) -> Self {
766 self.extra_env.push((
767 EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED.into(),
768 versions.iter().join(" ").into(),
769 ));
770
771 self
772 }
773
774 #[must_use]
776 pub fn with_filter(mut self, filter: (impl Into<String>, impl Into<String>)) -> Self {
777 self.filters.push((filter.0.into(), filter.1.into()));
778 self
779 }
780
781 #[must_use]
783 pub fn with_unset_git_credential_helper(self) -> Self {
784 let git_config = self.home_dir.child(".gitconfig");
785 git_config
786 .write_str(indoc! {r"
787 [credential]
788 helper =
789 "})
790 .expect("Failed to unset git credential helper");
791
792 self
793 }
794
795 #[must_use]
797 #[cfg(windows)]
798 pub fn clear_filters(mut self) -> Self {
799 self.filters.clear();
800 self
801 }
802
803 pub fn with_cache_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
808 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
809 return Ok(None);
810 };
811 self.with_cache_on_fs(&dir, "COW_FS").map(Some)
812 }
813
814 pub fn with_cache_on_alt_fs(self) -> anyhow::Result<Option<Self>> {
819 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_ALT_FS).ok() else {
820 return Ok(None);
821 };
822 self.with_cache_on_fs(&dir, "ALT_FS").map(Some)
823 }
824
825 pub fn with_cache_on_lowlinks_fs(self) -> anyhow::Result<Option<Self>> {
830 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_LOWLINKS_FS).ok() else {
831 return Ok(None);
832 };
833 self.with_cache_on_fs(&dir, "LOWLINKS_FS").map(Some)
834 }
835
836 pub fn with_cache_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
841 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
842 return Ok(None);
843 };
844 self.with_cache_on_fs(&dir, "NOCOW_FS").map(Some)
845 }
846
847 pub fn with_working_dir_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
854 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
855 return Ok(None);
856 };
857 self.with_working_dir_on_fs(&dir, "COW_FS").map(Some)
858 }
859
860 pub fn with_working_dir_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
867 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
868 return Ok(None);
869 };
870 self.with_working_dir_on_fs(&dir, "NOCOW_FS").map(Some)
871 }
872
873 fn with_cache_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
874 fs_err::create_dir_all(dir)?;
875 let tmp = tempfile::TempDir::new_in(dir)?;
876 self.cache_dir = ChildPath::new(tmp.path()).child("cache");
877 fs_err::create_dir_all(&self.cache_dir)?;
878 let replacement = format!("[{name}]/[CACHE_DIR]/");
879 for pattern in Self::path_patterns(&self.cache_dir) {
880 self.filters.insert(0, (pattern, replacement.clone()));
881 }
882 self._extra_tempdirs.push(tmp);
883 Ok(self)
884 }
885
886 fn with_working_dir_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
887 fs_err::create_dir_all(dir)?;
888 let tmp = tempfile::TempDir::new_in(dir)?;
889 self.temp_dir = ChildPath::new(tmp.path()).child("temp");
890 fs_err::create_dir_all(&self.temp_dir)?;
891 let canonical_temp_dir = self.temp_dir.canonicalize()?;
894 self.venv = ChildPath::new(canonical_temp_dir.join(".venv"));
895 let temp_replacement = format!("[{name}]/[TEMP_DIR]/");
896 self.filters.extend(
897 Self::path_patterns(&self.temp_dir)
898 .into_iter()
899 .map(|pattern| (pattern, temp_replacement.clone())),
900 );
901 let venv_replacement = format!("[{name}]/[VENV]/");
902 self.filters.extend(
903 Self::path_patterns(&self.venv)
904 .into_iter()
905 .map(|pattern| (pattern, venv_replacement.clone())),
906 );
907 self._extra_tempdirs.push(tmp);
908 Ok(self)
909 }
910
911 pub fn test_bucket_dir() -> PathBuf {
920 std::env::temp_dir()
921 .simple_canonicalize()
922 .expect("failed to canonicalize temp dir")
923 .join("uv")
924 .join("tests")
925 }
926
927 pub fn new_with_versions_and_bin(python_versions: &[&str], uv_bin: PathBuf) -> Self {
934 let bucket = Self::test_bucket_dir();
935 fs_err::create_dir_all(&bucket).expect("Failed to create test bucket");
936
937 let root = tempfile::TempDir::new_in(bucket).expect("Failed to create test root directory");
938
939 fs_err::create_dir_all(root.path().join(".git"))
942 .expect("Failed to create `.git` placeholder in test root directory");
943
944 let temp_dir = ChildPath::new(root.path()).child("temp");
945 fs_err::create_dir_all(&temp_dir).expect("Failed to create test working directory");
946
947 let cache_dir = ChildPath::new(root.path()).child("cache");
948 fs_err::create_dir_all(&cache_dir).expect("Failed to create test cache directory");
949
950 let python_dir = ChildPath::new(root.path()).child("python");
951 fs_err::create_dir_all(&python_dir).expect("Failed to create test Python directory");
952
953 let bin_dir = ChildPath::new(root.path()).child("bin");
954 fs_err::create_dir_all(&bin_dir).expect("Failed to create test bin directory");
955
956 if cfg!(not(feature = "git")) {
958 Self::disallow_git_cli(&bin_dir).expect("Failed to setup disallowed `git` command");
959 }
960
961 let home_dir = ChildPath::new(root.path()).child("home");
962 fs_err::create_dir_all(&home_dir).expect("Failed to create test home directory");
963
964 let user_config_dir = if cfg!(windows) {
965 ChildPath::new(home_dir.path())
966 } else {
967 ChildPath::new(home_dir.path()).child(".config")
968 };
969
970 let canonical_temp_dir = temp_dir.canonicalize().unwrap();
972 let venv = ChildPath::new(canonical_temp_dir.join(".venv"));
973
974 let python_version = python_versions
975 .first()
976 .map(|version| PythonVersion::from_str(version).unwrap());
977
978 let site_packages = python_version
979 .as_ref()
980 .map(|version| site_packages_path(&venv, &format!("python{version}")));
981
982 let workspace_root = Path::new(&env::var(EnvVars::CARGO_MANIFEST_DIR).unwrap())
985 .parent()
986 .expect("CARGO_MANIFEST_DIR should be nested in workspace")
987 .parent()
988 .expect("CARGO_MANIFEST_DIR should be doubly nested in workspace")
989 .to_path_buf();
990
991 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
992
993 let python_versions: Vec<_> = python_versions
994 .iter()
995 .map(|version| PythonVersion::from_str(version).unwrap())
996 .zip(
997 python_installations_for_versions(&temp_dir, python_versions, &download_list)
998 .expect("Failed to find test Python versions"),
999 )
1000 .collect();
1001
1002 if cfg!(unix) {
1005 for (version, executable) in &python_versions {
1006 let parent = python_dir.child(version.to_string());
1007 parent.create_dir_all().unwrap();
1008 parent.child("python3").symlink_to_file(executable).unwrap();
1009 }
1010 }
1011
1012 let mut filters = Vec::new();
1013
1014 filters.extend(
1015 Self::path_patterns(&uv_bin)
1016 .into_iter()
1017 .map(|pattern| (pattern, "[UV]".to_string())),
1018 );
1019
1020 if cfg!(windows) {
1022 filters.push((" --link-mode <LINK_MODE>".to_string(), String::new()));
1023 filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new()));
1024 filters.push((r"exit code: ".to_string(), "exit status: ".to_string()));
1026 }
1027
1028 for (version, executable) in &python_versions {
1029 filters.extend(
1031 Self::path_patterns(executable)
1032 .into_iter()
1033 .map(|pattern| (pattern, format!("[PYTHON-{version}]"))),
1034 );
1035
1036 filters.extend(
1038 Self::path_patterns(python_dir.join(version.to_string()))
1039 .into_iter()
1040 .map(|pattern| {
1041 (
1042 format!("{pattern}[a-zA-Z0-9]*"),
1043 format!("[PYTHON-{version}]"),
1044 )
1045 }),
1046 );
1047
1048 if version.patch().is_none() {
1051 filters.push((
1052 format!(r"({})\.\d+", regex::escape(version.to_string().as_str())),
1053 "$1.[X]".to_string(),
1054 ));
1055 }
1056 }
1057
1058 filters.extend(
1059 Self::path_patterns(&bin_dir)
1060 .into_iter()
1061 .map(|pattern| (pattern, "[BIN]/".to_string())),
1062 );
1063 filters.extend(
1064 Self::path_patterns(&cache_dir)
1065 .into_iter()
1066 .map(|pattern| (pattern, "[CACHE_DIR]/".to_string())),
1067 );
1068 if let Some(ref site_packages) = site_packages {
1069 filters.extend(
1070 Self::path_patterns(site_packages)
1071 .into_iter()
1072 .map(|pattern| (pattern, "[SITE_PACKAGES]/".to_string())),
1073 );
1074 }
1075 filters.extend(
1076 Self::path_patterns(&venv)
1077 .into_iter()
1078 .map(|pattern| (pattern, "[VENV]/".to_string())),
1079 );
1080
1081 if let Some(site_packages) = site_packages {
1083 filters.push((
1084 Self::path_pattern(
1085 site_packages
1086 .strip_prefix(&canonical_temp_dir)
1087 .expect("The test site-packages directory is always in the tempdir"),
1088 ),
1089 "[SITE_PACKAGES]/".to_string(),
1090 ));
1091 }
1092
1093 filters.push((
1095 r"[\\/]lib[\\/]python\d+\.\d+[\\/]".to_string(),
1096 "/[PYTHON-LIB]/".to_string(),
1097 ));
1098 filters.push((r"[\\/]Lib[\\/]".to_string(), "/[PYTHON-LIB]/".to_string()));
1099
1100 filters.extend(
1101 Self::path_patterns(&temp_dir)
1102 .into_iter()
1103 .map(|pattern| (pattern, "[TEMP_DIR]/".to_string())),
1104 );
1105 filters.extend(
1106 Self::path_patterns(&python_dir)
1107 .into_iter()
1108 .map(|pattern| (pattern, "[PYTHON_DIR]/".to_string())),
1109 );
1110 let mut uv_user_config_dir = PathBuf::from(user_config_dir.path());
1111 uv_user_config_dir.push("uv");
1112 filters.extend(
1113 Self::path_patterns(&uv_user_config_dir)
1114 .into_iter()
1115 .map(|pattern| (pattern, "[UV_USER_CONFIG_DIR]/".to_string())),
1116 );
1117 filters.extend(
1118 Self::path_patterns(&user_config_dir)
1119 .into_iter()
1120 .map(|pattern| (pattern, "[USER_CONFIG_DIR]/".to_string())),
1121 );
1122 filters.extend(
1123 Self::path_patterns(&home_dir)
1124 .into_iter()
1125 .map(|pattern| (pattern, "[HOME]/".to_string())),
1126 );
1127 filters.extend(
1128 Self::path_patterns(&workspace_root)
1129 .into_iter()
1130 .map(|pattern| (pattern, "[WORKSPACE]/".to_string())),
1131 );
1132
1133 filters.push((
1135 r"Activate with: (.*)\\Scripts\\activate".to_string(),
1136 "Activate with: source $1/[BIN]/activate".to_string(),
1137 ));
1138 filters.push((
1139 r"Activate with: Scripts\\activate".to_string(),
1140 "Activate with: source [BIN]/activate".to_string(),
1141 ));
1142 filters.push((
1143 r"Activate with: source (.*/|)bin/activate(?:\.\w+)?".to_string(),
1144 "Activate with: source $1[BIN]/activate".to_string(),
1145 ));
1146
1147 filters.push((
1150 r#"(\\|/)\.tmp[^\\/\s"'`]*"#.to_string(),
1151 "/[TMP]".to_string(),
1152 ));
1153
1154 filters.push((r"file:///".to_string(), "file://".to_string()));
1156
1157 filters.push((r"\\\\\?\\".to_string(), String::new()));
1159
1160 filters.push((r"127\.0\.0\.1:\d*".to_string(), "[LOCALHOST]".to_string()));
1162 filters.push((
1164 format!(
1165 r#"requires = \["uv_build>={},<[0-9.]+"\]"#,
1166 uv_version::version()
1167 ),
1168 r#"requires = ["uv_build>=[CURRENT_VERSION],<[NEXT_BREAKING]"]"#.to_string(),
1169 ));
1170 filters.push((
1172 r"environments-v(\d+)[\\/]([\w.\[\]-]+)-[a-f0-9]{16}".to_string(),
1173 "environments-v$1/$2-[HASH]".to_string(),
1174 ));
1175 filters.push((
1177 r"archive-v(\d+)[\\/][A-Za-z0-9\-\_]+".to_string(),
1178 "archive-v$1/[HASH]".to_string(),
1179 ));
1180
1181 Self {
1182 root: ChildPath::new(root.path()),
1183 temp_dir,
1184 cache_dir,
1185 python_dir,
1186 home_dir,
1187 user_config_dir,
1188 bin_dir,
1189 venv,
1190 workspace_root,
1191 python_version,
1192 python_versions,
1193 uv_bin,
1194 filters,
1195 extra_env: vec![],
1196 _root: root,
1197 _extra_tempdirs: vec![],
1198 }
1199 }
1200
1201 pub fn command(&self) -> Command {
1203 let mut command = self.new_command();
1204 self.add_shared_options(&mut command, true);
1205 command
1206 }
1207
1208 pub fn disallow_git_cli(bin_dir: &Path) -> std::io::Result<()> {
1209 let contents = r"#!/bin/sh
1210 echo 'error: `git` operations are not allowed — are you missing a cfg for the `git` feature?' >&2
1211 exit 127";
1212 let git = bin_dir.join(format!("git{}", env::consts::EXE_SUFFIX));
1213 fs_err::write(&git, contents)?;
1214
1215 #[cfg(unix)]
1216 {
1217 use std::os::unix::fs::PermissionsExt;
1218 let mut perms = fs_err::metadata(&git)?.permissions();
1219 perms.set_mode(0o755);
1220 fs_err::set_permissions(&git, perms)?;
1221 }
1222
1223 Ok(())
1224 }
1225
1226 #[must_use]
1231 pub fn with_git_lfs_config(mut self) -> Self {
1232 let git_lfs_config = self.root.child(".gitconfig");
1233 git_lfs_config
1234 .write_str(indoc! {r#"
1235 [filter "lfs"]
1236 clean = git-lfs clean -- %f
1237 smudge = git-lfs smudge -- %f
1238 process = git-lfs filter-process
1239 required = true
1240 "#})
1241 .expect("Failed to setup `git-lfs` filters");
1242
1243 self.extra_env.push((
1246 EnvVars::GIT_CONFIG_GLOBAL.into(),
1247 git_lfs_config.as_os_str().into(),
1248 ));
1249 self
1250 }
1251
1252 pub fn add_shared_options(&self, command: &mut Command, activate_venv: bool) {
1264 self.add_shared_args(command);
1265 self.add_shared_env(command, activate_venv);
1266 }
1267
1268 fn add_shared_args(&self, command: &mut Command) {
1270 command.arg("--cache-dir").arg(self.cache_dir.path());
1271 }
1272
1273 pub fn add_shared_env(&self, command: &mut Command, activate_venv: bool) {
1275 let path = env::join_paths(std::iter::once(self.bin_dir.to_path_buf()).chain(
1277 env::split_paths(&env::var(EnvVars::PATH).unwrap_or_default()),
1278 ))
1279 .unwrap();
1280
1281 if cfg!(not(windows)) {
1284 command.env(EnvVars::SHELL, "bash");
1285 }
1286
1287 command
1288 .env_remove(EnvVars::VIRTUAL_ENV)
1290 .env(EnvVars::UV_NO_WRAP, "1")
1292 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1294 .env(EnvVars::COLUMNS, "100")
1297 .env(EnvVars::PATH, path)
1298 .env(EnvVars::HOME, self.home_dir.as_os_str())
1299 .env(EnvVars::APPDATA, self.home_dir.as_os_str())
1300 .env(EnvVars::USERPROFILE, self.home_dir.as_os_str())
1301 .env(
1302 EnvVars::XDG_CONFIG_DIRS,
1303 self.home_dir.join("config").as_os_str(),
1304 )
1305 .env(
1306 EnvVars::XDG_DATA_HOME,
1307 self.home_dir.join("data").as_os_str(),
1308 )
1309 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1310 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "")
1311 .env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
1313 .env(EnvVars::UV_PYTHON_SEARCH_PATH, self.python_path())
1314 .env(EnvVars::UV_EXCLUDE_NEWER, TEST_TIMESTAMP)
1315 .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, TEST_TIMESTAMP)
1316 .env(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF, TEST_TIMESTAMP)
1317 .env(EnvVars::UV_PYTHON_NO_REGISTRY, "1")
1320 .env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "0")
1321 .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1")
1324 .env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path())
1338 .current_dir(self.temp_dir.path());
1339
1340 for (key, value) in &self.extra_env {
1341 command.env(key, value);
1342 }
1343
1344 if activate_venv {
1345 command.env(EnvVars::VIRTUAL_ENV, self.venv.as_os_str());
1346 }
1347
1348 if cfg!(unix) {
1349 command.env(EnvVars::LC_ALL, "C");
1351 }
1352 }
1353
1354 pub fn pip_compile(&self) -> Command {
1356 let mut command = self.new_command();
1357 command.arg("pip").arg("compile");
1358 self.add_shared_options(&mut command, true);
1359 command
1360 }
1361
1362 pub fn pip_sync(&self) -> Command {
1364 let mut command = self.new_command();
1365 command.arg("pip").arg("sync");
1366 self.add_shared_options(&mut command, true);
1367 command
1368 }
1369
1370 pub fn pip_show(&self) -> Command {
1371 let mut command = self.new_command();
1372 command.arg("pip").arg("show");
1373 self.add_shared_options(&mut command, true);
1374 command
1375 }
1376
1377 pub fn pip_freeze(&self) -> Command {
1379 let mut command = self.new_command();
1380 command.arg("pip").arg("freeze");
1381 self.add_shared_options(&mut command, true);
1382 command
1383 }
1384
1385 pub fn pip_check(&self) -> Command {
1387 let mut command = self.new_command();
1388 command.arg("pip").arg("check");
1389 self.add_shared_options(&mut command, true);
1390 command
1391 }
1392
1393 pub fn pip_list(&self) -> Command {
1394 let mut command = self.new_command();
1395 command.arg("pip").arg("list");
1396 self.add_shared_options(&mut command, true);
1397 command
1398 }
1399
1400 pub fn venv(&self) -> Command {
1402 let mut command = self.new_command();
1403 command.arg("venv");
1404 self.add_shared_options(&mut command, false);
1405 command
1406 }
1407
1408 pub fn pip_install(&self) -> Command {
1410 let mut command = self.new_command();
1411 command.arg("pip").arg("install");
1412 self.add_shared_options(&mut command, true);
1413 command
1414 }
1415
1416 pub fn pip_uninstall(&self) -> Command {
1418 let mut command = self.new_command();
1419 command.arg("pip").arg("uninstall");
1420 self.add_shared_options(&mut command, true);
1421 command
1422 }
1423
1424 pub fn pip_tree(&self) -> Command {
1426 let mut command = self.new_command();
1427 command.arg("pip").arg("tree");
1428 self.add_shared_options(&mut command, true);
1429 command
1430 }
1431
1432 pub fn pip_debug(&self) -> Command {
1434 let mut command = self.new_command();
1435 command.arg("pip").arg("debug");
1436 self.add_shared_options(&mut command, true);
1437 command
1438 }
1439
1440 pub fn help(&self) -> Command {
1442 let mut command = self.new_command();
1443 command.arg("help");
1444 self.add_shared_env(&mut command, false);
1445 command
1446 }
1447
1448 pub fn init(&self) -> Command {
1451 let mut command = self.new_command();
1452 command.arg("init");
1453 self.add_shared_options(&mut command, false);
1454 command
1455 }
1456
1457 pub fn sync(&self) -> Command {
1459 let mut command = self.new_command();
1460 command.arg("sync");
1461 self.add_shared_options(&mut command, false);
1462 command
1463 }
1464
1465 pub fn lock(&self) -> Command {
1467 let mut command = self.new_command();
1468 command.arg("lock");
1469 self.add_shared_options(&mut command, false);
1470 command
1471 }
1472
1473 pub fn upgrade(&self) -> Command {
1475 let mut command = self.new_command();
1476 command.arg("upgrade");
1477 self.add_shared_options(&mut command, false);
1478 command
1479 }
1480
1481 pub fn audit(&self) -> Command {
1483 let mut command = self.new_command();
1484 command.arg("audit");
1485 self.add_shared_options(&mut command, false);
1486 command
1487 }
1488
1489 pub fn workspace_metadata(&self) -> Command {
1491 let mut command = self.new_command();
1492 command.arg("workspace").arg("metadata");
1493 self.add_shared_options(&mut command, false);
1494 command
1495 }
1496
1497 pub fn workspace_dir(&self) -> Command {
1499 let mut command = self.new_command();
1500 command.arg("workspace").arg("dir");
1501 self.add_shared_options(&mut command, false);
1502 command
1503 }
1504
1505 pub fn workspace_list(&self) -> Command {
1507 let mut command = self.new_command();
1508 command.arg("workspace").arg("list");
1509 self.add_shared_options(&mut command, false);
1510 command
1511 }
1512
1513 pub fn export(&self) -> Command {
1515 let mut command = self.new_command();
1516 command.arg("export");
1517 self.add_shared_options(&mut command, false);
1518 command
1519 }
1520
1521 pub fn format(&self) -> Command {
1523 let mut command = self.new_command();
1524 command.arg("format");
1525 self.add_shared_options(&mut command, false);
1526 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1528 command
1529 }
1530
1531 pub fn check(&self) -> Command {
1533 let mut command = self.new_command();
1534 command.arg("check");
1535 self.add_shared_options(&mut command, false);
1536 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1538 command
1539 }
1540
1541 pub fn build(&self) -> Command {
1543 let mut command = self.new_command();
1544 command.arg("build");
1545 self.add_shared_options(&mut command, false);
1546 command
1547 }
1548
1549 pub fn version(&self) -> Command {
1550 let mut command = self.new_command();
1551 command.arg("version");
1552 self.add_shared_options(&mut command, false);
1553 command
1554 }
1555
1556 pub fn self_version(&self) -> Command {
1557 let mut command = self.new_command();
1558 command.arg("self").arg("version");
1559 self.add_shared_options(&mut command, false);
1560 command
1561 }
1562
1563 pub fn self_update(&self) -> Command {
1564 let mut command = self.new_command();
1565 command.arg("self").arg("update");
1566 self.add_shared_options(&mut command, false);
1567 command
1568 }
1569
1570 pub fn publish(&self) -> Command {
1572 let mut command = self.new_command();
1573 command.arg("publish");
1574 self.add_shared_options(&mut command, false);
1575 command
1576 }
1577
1578 pub fn python_find(&self) -> Command {
1580 let mut command = self.new_command();
1581 command
1582 .arg("python")
1583 .arg("find")
1584 .env(EnvVars::UV_PREVIEW, "1")
1585 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1586 self.add_shared_options(&mut command, false);
1587 command
1588 }
1589
1590 pub fn python_list(&self) -> Command {
1592 let mut command = self.new_command();
1593 command
1594 .arg("python")
1595 .arg("list")
1596 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1597 self.add_shared_options(&mut command, false);
1598 command
1599 }
1600
1601 pub fn python_install(&self) -> Command {
1603 let mut command = self.new_command();
1604 command.arg("python").arg("install");
1605 self.add_shared_options(&mut command, true);
1606 command
1607 }
1608
1609 pub fn python_uninstall(&self) -> Command {
1611 let mut command = self.new_command();
1612 command.arg("python").arg("uninstall");
1613 self.add_shared_options(&mut command, true);
1614 command
1615 }
1616
1617 pub fn python_upgrade(&self) -> Command {
1619 let mut command = self.new_command();
1620 command.arg("python").arg("upgrade");
1621 self.add_shared_options(&mut command, true);
1622 command
1623 }
1624
1625 pub fn python_pin(&self) -> Command {
1627 let mut command = self.new_command();
1628 command.arg("python").arg("pin");
1629 self.add_shared_options(&mut command, true);
1630 command
1631 }
1632
1633 pub fn python_dir(&self) -> Command {
1635 let mut command = self.new_command();
1636 command.arg("python").arg("dir");
1637 self.add_shared_options(&mut command, true);
1638 command
1639 }
1640
1641 pub fn run(&self) -> Command {
1643 let mut command = self.new_command();
1644 command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1");
1645 self.add_shared_options(&mut command, true);
1646 command
1647 }
1648
1649 pub fn tool_run(&self) -> Command {
1651 let mut command = self.new_command();
1652 command
1653 .arg("tool")
1654 .arg("run")
1655 .env(EnvVars::UV_SHOW_RESOLUTION, "1");
1656 self.add_shared_options(&mut command, false);
1657 command
1658 }
1659
1660 pub fn tool_upgrade(&self) -> Command {
1662 let mut command = self.new_command();
1663 command.arg("tool").arg("upgrade");
1664 self.add_shared_options(&mut command, false);
1665 command
1666 }
1667
1668 pub fn tool_install(&self) -> Command {
1670 let mut command = self.new_command();
1671 command.arg("tool").arg("install");
1672 self.add_shared_options(&mut command, false);
1673 command
1674 }
1675
1676 pub fn tool_list(&self) -> Command {
1678 let mut command = self.new_command();
1679 command.arg("tool").arg("list");
1680 self.add_shared_options(&mut command, false);
1681 command
1682 }
1683
1684 pub fn tool_audit(&self) -> Command {
1686 let mut command = self.new_command();
1687 command.arg("tool").arg("audit");
1688 self.add_shared_options(&mut command, false);
1689 command
1690 }
1691
1692 pub fn tool_dir(&self) -> Command {
1694 let mut command = self.new_command();
1695 command.arg("tool").arg("dir");
1696 self.add_shared_options(&mut command, false);
1697 command
1698 }
1699
1700 pub fn tool_uninstall(&self) -> Command {
1702 let mut command = self.new_command();
1703 command.arg("tool").arg("uninstall");
1704 self.add_shared_options(&mut command, false);
1705 command
1706 }
1707
1708 pub fn add(&self) -> Command {
1710 let mut command = self.new_command();
1711 command.arg("add");
1712 self.add_shared_options(&mut command, false);
1713 command
1714 }
1715
1716 pub fn remove(&self) -> Command {
1718 let mut command = self.new_command();
1719 command.arg("remove");
1720 self.add_shared_options(&mut command, false);
1721 command
1722 }
1723
1724 pub fn tree(&self) -> Command {
1726 let mut command = self.new_command();
1727 command.arg("tree");
1728 self.add_shared_options(&mut command, false);
1729 command
1730 }
1731
1732 pub fn clean(&self) -> Command {
1734 let mut command = self.new_command();
1735 command.arg("cache").arg("clean");
1736 self.add_shared_options(&mut command, false);
1737 command
1738 }
1739
1740 pub fn prune(&self) -> Command {
1742 let mut command = self.new_command();
1743 command.arg("cache").arg("prune");
1744 self.add_shared_options(&mut command, false);
1745 command
1746 }
1747
1748 pub fn cache_size(&self) -> Command {
1750 let mut command = self.new_command();
1751 command.arg("cache").arg("size");
1752 self.add_shared_options(&mut command, false);
1753 command
1754 }
1755
1756 pub fn build_backend(&self) -> Command {
1760 let mut command = self.new_command();
1761 command.arg("build-backend");
1762 self.add_shared_options(&mut command, false);
1763 command
1764 }
1765
1766 pub fn interpreter(&self) -> PathBuf {
1770 let venv = &self.venv;
1771 if cfg!(unix) {
1772 venv.join("bin").join("python")
1773 } else if cfg!(windows) {
1774 venv.join("Scripts").join("python.exe")
1775 } else {
1776 unimplemented!("Only Windows and Unix are supported")
1777 }
1778 }
1779
1780 pub fn python_command(&self) -> Command {
1781 let mut interpreter = self.interpreter();
1782
1783 if !interpreter.exists() {
1785 interpreter.clone_from(
1786 &self
1787 .python_versions
1788 .first()
1789 .expect("At least one Python version is required")
1790 .1,
1791 );
1792 }
1793
1794 let mut command = Self::new_command_with(&interpreter);
1795 command
1796 .arg("-B")
1799 .env(EnvVars::PYTHONUTF8, "1");
1801
1802 self.add_shared_env(&mut command, false);
1803
1804 command
1805 }
1806
1807 pub fn auth_login(&self) -> Command {
1809 let mut command = self.new_command();
1810 command.arg("auth").arg("login");
1811 self.add_shared_options(&mut command, false);
1812 command
1813 }
1814
1815 pub fn auth_logout(&self) -> Command {
1817 let mut command = self.new_command();
1818 command.arg("auth").arg("logout");
1819 self.add_shared_options(&mut command, false);
1820 command
1821 }
1822
1823 pub fn auth_helper(&self) -> Command {
1825 let mut command = self.new_command();
1826 command.arg("auth").arg("helper");
1827 self.add_shared_options(&mut command, false);
1828 command
1829 }
1830
1831 pub fn auth_token(&self) -> Command {
1833 let mut command = self.new_command();
1834 command.arg("auth").arg("token");
1835 self.add_shared_options(&mut command, false);
1836 command
1837 }
1838
1839 #[must_use]
1843 pub fn with_real_home(mut self) -> Self {
1844 if let Some(home) = env::var_os(EnvVars::HOME) {
1845 self.extra_env
1846 .push((EnvVars::HOME.to_string().into(), home));
1847 }
1848 self.extra_env.push((
1851 EnvVars::XDG_CONFIG_HOME.into(),
1852 self.user_config_dir.as_os_str().into(),
1853 ));
1854 self
1855 }
1856
1857 pub fn assert_command(&self, command: &str) -> Assert {
1859 self.python_command()
1860 .arg("-c")
1861 .arg(command)
1862 .current_dir(&self.temp_dir)
1863 .assert()
1864 }
1865
1866 pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
1868 self.python_command()
1869 .arg(file.as_ref())
1870 .current_dir(&self.temp_dir)
1871 .assert()
1872 }
1873
1874 pub fn assert_installed(&self, package: &'static str, version: &'static str) {
1876 self.assert_command(
1877 format!("import {package} as package; print(package.__version__, end='')").as_str(),
1878 )
1879 .success()
1880 .stdout(version);
1881 }
1882
1883 pub fn assert_not_installed(&self, package: &'static str) {
1885 self.assert_command(format!("import {package}").as_str())
1886 .failure();
1887 }
1888
1889 pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
1891 let mut patterns = Vec::new();
1892
1893 if path.as_ref().exists() {
1895 patterns.push(Self::path_pattern(
1896 path.as_ref()
1897 .canonicalize()
1898 .expect("Failed to create canonical path"),
1899 ));
1900 }
1901
1902 patterns.push(Self::path_pattern(path));
1904
1905 patterns
1906 }
1907
1908 fn path_pattern(path: impl AsRef<Path>) -> String {
1910 format!(
1911 r"{}\\?/?",
1913 regex::escape(&path.as_ref().simplified_display().to_string())
1914 .replace(r"\\", r"(\\|\/)")
1917 )
1918 }
1919
1920 pub fn python_path(&self) -> OsString {
1921 if cfg!(unix) {
1922 env::join_paths(
1924 self.python_versions
1925 .iter()
1926 .map(|(version, _)| self.python_dir.join(version.to_string())),
1927 )
1928 .unwrap()
1929 } else {
1930 env::join_paths(
1932 self.python_versions
1933 .iter()
1934 .map(|(_, executable)| executable.parent().unwrap().to_path_buf()),
1935 )
1936 .unwrap()
1937 }
1938 }
1939
1940 pub fn filters(&self) -> Vec<(&str, &str)> {
1942 self.filters
1945 .iter()
1946 .map(|(p, r)| (p.as_str(), r.as_str()))
1947 .chain(INSTA_FILTERS.iter().copied())
1948 .collect()
1949 }
1950
1951 #[cfg(windows)]
1953 pub fn filters_without_standard_filters(&self) -> Vec<(&str, &str)> {
1954 self.filters
1955 .iter()
1956 .map(|(p, r)| (p.as_str(), r.as_str()))
1957 .collect()
1958 }
1959
1960 pub fn python_kind(&self) -> &'static str {
1962 "python"
1963 }
1964
1965 pub fn site_packages(&self) -> PathBuf {
1967 site_packages_path(
1968 &self.venv,
1969 &format!(
1970 "{}{}",
1971 self.python_kind(),
1972 self.python_version.as_ref().expect(
1973 "A Python version must be provided to retrieve the test site packages path"
1974 )
1975 ),
1976 )
1977 }
1978
1979 pub fn reset_venv(&self) {
1981 self.create_venv();
1982 }
1983
1984 fn create_venv(&self) {
1986 let executable = get_python(
1987 self.python_version
1988 .as_ref()
1989 .expect("A Python version must be provided to create a test virtual environment"),
1990 );
1991 create_venv_from_executable(&self.venv, &self.cache_dir, &executable, &self.uv_bin);
1992 }
1993
1994 pub fn copy_ecosystem_project(&self, name: &str) {
2005 let project_dir = PathBuf::from(format!("../../test/ecosystem/{name}"));
2006 self.temp_dir.copy_from(project_dir, &["**/*"]).unwrap();
2007 if let Err(err) = fs_err::remove_file(self.temp_dir.join("uv.lock")) {
2009 assert_eq!(
2010 err.kind(),
2011 io::ErrorKind::NotFound,
2012 "Failed to remove uv.lock: {err}"
2013 );
2014 }
2015 }
2016
2017 pub fn diff_lock(&self, change: impl Fn(&Self) -> Command) -> String {
2026 let lock_path = ChildPath::new(self.temp_dir.join("uv.lock"));
2027 let old_lock = fs_err::read_to_string(&lock_path).unwrap();
2028 let (snapshot, output) = run_and_format(
2029 change(self),
2030 self.filters(),
2031 "diff_lock",
2032 Some(WindowsFilters::Platform),
2033 None,
2034 );
2035 assert!(output.status.success(), "{snapshot}");
2036 let new_lock = fs_err::read_to_string(&lock_path).unwrap();
2037 diff_snapshot(&old_lock, &new_lock, 10)
2038 }
2039
2040 pub fn read(&self, file: impl AsRef<Path>) -> String {
2042 fs_err::read_to_string(self.temp_dir.join(&file))
2043 .unwrap_or_else(|_| panic!("Missing file: `{}`", file.user_display()))
2044 }
2045
2046 fn new_command(&self) -> Command {
2049 Self::new_command_with(&self.uv_bin)
2050 }
2051
2052 fn new_command_with(bin: &Path) -> Command {
2058 let mut command = Command::new(bin);
2059
2060 let passthrough = [
2061 EnvVars::PATH,
2063 EnvVars::RUST_LOG,
2065 EnvVars::RUST_BACKTRACE,
2066 EnvVars::SYSTEMDRIVE,
2068 EnvVars::RUST_MIN_STACK,
2070 EnvVars::UV_STACK_SIZE,
2071 EnvVars::ALL_PROXY,
2073 EnvVars::HTTPS_PROXY,
2074 EnvVars::HTTP_PROXY,
2075 EnvVars::NO_PROXY,
2076 EnvVars::SSL_CERT_DIR,
2077 EnvVars::SSL_CERT_FILE,
2078 EnvVars::UV_NATIVE_TLS,
2079 EnvVars::UV_SYSTEM_CERTS,
2080 ];
2081
2082 for env_var in EnvVars::all_names()
2083 .iter()
2084 .filter(|name| !passthrough.contains(name))
2085 {
2086 command.env_remove(env_var);
2087 }
2088
2089 command
2090 }
2091}
2092
2093pub fn diff_snapshot(old: &str, new: &str, context_radius: usize) -> String {
2096 let diff = similar::TextDiff::from_lines(old, new);
2097 let unified = diff
2098 .unified_diff()
2099 .context_radius(context_radius)
2100 .header("old", "new")
2101 .to_string();
2102 regex!(r"(?m)^\s+$").replace_all(&unified, "").into_owned()
2106}
2107
2108#[macro_export]
2112macro_rules! diff_uv_snapshot {
2113 ($filters:expr, $old:expr, $spawnable:expr, @$snapshot:literal) => {{
2114 let new = $crate::capture_uv_snapshot!($filters, $spawnable);
2115 let snapshot = $crate::diff_snapshot($old, &new, 3);
2116 let mut settings = ::insta::Settings::clone_current();
2117 let description = match settings.description() {
2119 Some(description) => format!("{description}\n\nUnfiltered diff:\n{snapshot}"),
2120 None => format!("Unfiltered diff:\n{snapshot}"),
2121 };
2122 settings.set_description(description);
2123 settings.add_filter(r"^--- old\n\+\+\+ new\n", "");
2124 settings.add_filter(r"(?m)^@@.*$", "...");
2125 settings.add_filter(r"\n$", "\n...\n");
2126 settings.bind(|| {
2127 ::insta::assert_snapshot!(snapshot, @$snapshot);
2128 });
2129 new
2130 }};
2131}
2132
2133#[macro_export]
2135macro_rules! capture_uv_snapshot {
2136 ($filters:expr, $spawnable:expr) => {{
2137 let (snapshot, _) = $crate::run_and_format_silent(
2139 $spawnable,
2140 &$filters,
2141 $crate::function_name!(),
2142 Some($crate::WindowsFilters::Platform),
2143 None,
2144 );
2145 snapshot
2146 }};
2147 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2148 let (snapshot, _) = $crate::run_and_format(
2149 $spawnable,
2150 &$filters,
2151 $crate::function_name!(),
2152 Some($crate::WindowsFilters::Platform),
2153 None,
2154 );
2155 ::insta::assert_snapshot!(snapshot, @$snapshot);
2156 snapshot
2157 }};
2158}
2159
2160pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
2161 if cfg!(unix) {
2162 venv.join("lib").join(python).join("site-packages")
2163 } else if cfg!(windows) {
2164 venv.join("Lib").join("site-packages")
2165 } else {
2166 unimplemented!("Only Windows and Unix are supported")
2167 }
2168}
2169
2170pub fn venv_bin_path(venv: impl AsRef<Path>) -> PathBuf {
2171 if cfg!(unix) {
2172 venv.as_ref().join("bin")
2173 } else if cfg!(windows) {
2174 venv.as_ref().join("Scripts")
2175 } else {
2176 unimplemented!("Only Windows and Unix are supported")
2177 }
2178}
2179
2180fn get_python(version: &PythonVersion) -> PathBuf {
2182 ManagedPythonInstallations::from_settings(None)
2183 .map(|installed_pythons| {
2184 installed_pythons
2185 .find_version(version)
2186 .expect("Tests are run on a supported platform")
2187 .next()
2188 .as_ref()
2189 .map(|python| python.executable(false))
2190 })
2191 .unwrap_or_default()
2194 .unwrap_or(PathBuf::from(version.to_string()))
2195}
2196
2197fn create_venv_from_executable<P: AsRef<Path>>(
2199 path: P,
2200 cache_dir: &ChildPath,
2201 python: &Path,
2202 uv_bin: &Path,
2203) {
2204 TestContext::new_command_with(uv_bin)
2205 .arg("venv")
2206 .arg(path.as_ref().as_os_str())
2207 .arg("--clear")
2208 .arg("--cache-dir")
2209 .arg(cache_dir.path())
2210 .arg("--python")
2211 .arg(python)
2212 .current_dir(path.as_ref().parent().unwrap())
2213 .assert()
2214 .success();
2215 ChildPath::new(path.as_ref()).assert(predicate::path::is_dir());
2216}
2217
2218pub fn python_path_with_versions(
2222 temp_dir: &ChildPath,
2223 python_versions: &[&str],
2224) -> anyhow::Result<OsString> {
2225 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
2226 Ok(env::join_paths(
2227 python_installations_for_versions(temp_dir, python_versions, &download_list)?
2228 .into_iter()
2229 .map(|path| path.parent().unwrap().to_path_buf()),
2230 )?)
2231}
2232
2233fn python_installations_for_versions(
2237 temp_dir: &ChildPath,
2238 python_versions: &[&str],
2239 download_list: &ManagedPythonDownloadList,
2240) -> anyhow::Result<Vec<PathBuf>> {
2241 let cache = Cache::from_path(temp_dir.child("cache").to_path_buf())
2242 .init_no_wait()?
2243 .expect("No cache contention when setting up Python in tests");
2244 let _preview = uv_preview::test::with_features(&[]);
2245 let selected_pythons = python_versions
2246 .iter()
2247 .map(|python_version| {
2248 if let Ok(python) = PythonInstallation::find(
2249 &PythonRequest::parse(python_version),
2250 EnvironmentPreference::OnlySystem,
2251 PythonPreference::Managed,
2252 download_list,
2253 &cache,
2254 ) {
2255 python.into_interpreter().sys_executable().to_owned()
2256 } else {
2257 panic!("Could not find Python {python_version} for test\nTry `cargo run python install` first, or refer to CONTRIBUTING.md");
2258 }
2259 })
2260 .collect::<Vec<_>>();
2261
2262 assert!(
2263 python_versions.is_empty() || !selected_pythons.is_empty(),
2264 "Failed to fulfill requested test Python versions: {selected_pythons:?}"
2265 );
2266
2267 Ok(selected_pythons)
2268}
2269
2270#[derive(Debug, Copy, Clone)]
2271pub enum WindowsFilters {
2272 Platform,
2273 Universal,
2274}
2275
2276pub fn apply_filters<T: AsRef<str>>(mut snapshot: String, filters: impl AsRef<[(T, T)]>) -> String {
2278 for (matcher, replacement) in filters.as_ref() {
2279 let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?");
2281 if re.is_match(&snapshot) {
2282 snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string();
2283 }
2284 }
2285 snapshot
2286}
2287
2288#[expect(clippy::print_stderr)]
2292pub fn run_and_format<T: AsRef<str>>(
2293 command: impl BorrowMut<Command>,
2294 filters: impl AsRef<[(T, T)]>,
2295 function_name: &str,
2296 windows_filters: Option<WindowsFilters>,
2297 input: Option<&str>,
2298) -> (String, Output) {
2299 let (snapshot, output) =
2300 run_and_format_silent(command, filters, function_name, windows_filters, input);
2301 eprintln!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Unfiltered output ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2302 eprintln!(
2303 "----- exit status -----\n{}\n----- stdout -----\n{}\n----- stderr -----\n{}",
2304 output.status,
2305 String::from_utf8_lossy(&output.stdout),
2306 String::from_utf8_lossy(&output.stderr),
2307 );
2308 eprintln!("────────────────────────────────────────────────────────────────────────────────\n");
2309 (snapshot, output)
2310}
2311
2312#[doc(hidden)]
2314pub fn run_and_format_silent<T: AsRef<str>>(
2315 mut command: impl BorrowMut<Command>,
2316 filters: impl AsRef<[(T, T)]>,
2317 function_name: &str,
2318 windows_filters: Option<WindowsFilters>,
2319 input: Option<&str>,
2320) -> (String, Output) {
2321 assert_effective_cache_directory(command.borrow_mut());
2322
2323 let program = command
2324 .borrow_mut()
2325 .get_program()
2326 .to_string_lossy()
2327 .to_string();
2328
2329 if let Ok(root) = env::var(EnvVars::TRACING_DURATIONS_TEST_ROOT) {
2331 #[expect(clippy::assertions_on_constants)]
2333 {
2334 assert!(
2335 cfg!(feature = "tracing-durations-export"),
2336 "You need to enable the tracing-durations-export feature to use `TRACING_DURATIONS_TEST_ROOT`"
2337 );
2338 }
2339 command.borrow_mut().env(
2340 EnvVars::TRACING_DURATIONS_FILE,
2341 Path::new(&root).join(function_name).with_extension("jsonl"),
2342 );
2343 }
2344
2345 let output = if let Some(input) = input {
2346 let mut child = command
2347 .borrow_mut()
2348 .stdin(Stdio::piped())
2349 .stdout(Stdio::piped())
2350 .stderr(Stdio::piped())
2351 .spawn()
2352 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"));
2353 child
2354 .stdin
2355 .as_mut()
2356 .expect("Failed to open stdin")
2357 .write_all(input.as_bytes())
2358 .expect("Failed to write to stdin");
2359
2360 child
2361 .wait_with_output()
2362 .unwrap_or_else(|err| panic!("Failed to read output from {program}: {err}"))
2363 } else {
2364 command
2365 .borrow_mut()
2366 .output()
2367 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"))
2368 };
2369
2370 let mut snapshot = format!(
2371 "exit_code: {} ({})\n",
2372 output.status.code().unwrap_or(!0),
2373 if output.status.success() {
2374 "success"
2375 } else {
2376 "failure"
2377 },
2378 );
2379 if output.status.code().is_none() {
2380 snapshot.push_str("exit_status: ");
2381 snapshot.push_str(&output.status.to_string());
2382 snapshot.push('\n');
2383 }
2384 if !output.stdout.is_empty() {
2385 snapshot.push_str("----- stdout -----\n");
2386 snapshot.push_str(&String::from_utf8_lossy(&output.stdout));
2387 }
2388 if !output.stderr.is_empty() {
2389 if !output.stdout.is_empty() {
2390 snapshot.push('\n');
2391 }
2392 snapshot.push_str("----- stderr -----\n");
2393 snapshot.push_str(&String::from_utf8_lossy(&output.stderr));
2394 }
2395 let mut snapshot = apply_filters(snapshot, filters);
2396
2397 if cfg!(windows) {
2402 if let Some(windows_filters) = windows_filters {
2403 let windows_only_deps = [
2405 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2406 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2407 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2408 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2409 ];
2410 let mut removed_packages = 0;
2411 for windows_only_dep in windows_only_deps {
2412 let re = Regex::new(windows_only_dep).unwrap();
2414 if re.is_match(&snapshot) {
2415 snapshot = re.replace(&snapshot, "").to_string();
2416 removed_packages += 1;
2417 }
2418 }
2419 if removed_packages > 0 {
2420 for i in 1..20 {
2421 for verb in match windows_filters {
2422 WindowsFilters::Platform => [
2423 "Resolved",
2424 "Prepared",
2425 "Installed",
2426 "Checked",
2427 "Uninstalled",
2428 ]
2429 .iter(),
2430 WindowsFilters::Universal => {
2431 ["Prepared", "Installed", "Checked", "Uninstalled"].iter()
2432 }
2433 } {
2434 snapshot = snapshot.replace(
2435 &format!("{verb} {} packages", i + removed_packages),
2436 &format!("{verb} {} package{}", i, if i > 1 { "s" } else { "" }),
2437 );
2438 }
2439 }
2440 }
2441 }
2442 }
2443
2444 (snapshot, output)
2445}
2446
2447fn assert_effective_cache_directory(command: &Command) {
2453 let cache_directory_override = command
2454 .get_envs()
2455 .find(|(name, value)| *name == EnvVars::UV_CACHE_DIR && value.is_some());
2456
2457 if cache_directory_override.is_none() {
2458 return;
2459 }
2460
2461 let explicit_cache_directory = command.get_args().any(|argument| {
2462 argument == "--cache-dir"
2463 || argument
2464 .to_str()
2465 .is_some_and(|argument| argument.starts_with("--cache-dir="))
2466 });
2467
2468 assert!(
2469 !explicit_cache_directory,
2470 "`UV_CACHE_DIR` is ignored because this command already supplies `--cache-dir`; configure `TestContext::cache_dir` instead"
2471 );
2472}
2473
2474pub fn copy_dir_ignore(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {
2476 for entry in ignore::Walk::new(&src) {
2477 let entry = entry?;
2478 let relative = entry.path().strip_prefix(&src)?;
2479 let ty = entry.file_type().unwrap();
2480 if ty.is_dir() {
2481 fs_err::create_dir(dst.as_ref().join(relative))?;
2482 } else {
2483 fs_err::copy(entry.path(), dst.as_ref().join(relative))?;
2484 }
2485 }
2486 Ok(())
2487}
2488
2489pub fn make_project(dir: &Path, name: &str, body: &str) -> anyhow::Result<()> {
2491 let pyproject_toml = formatdoc! {r#"
2492 [project]
2493 name = "{name}"
2494 version = "0.1.0"
2495 requires-python = ">=3.11,<3.13"
2496 {body}
2497
2498 [build-system]
2499 requires = ["uv_build>=0.9.0,<10000"]
2500 build-backend = "uv_build"
2501 "#
2502 };
2503 fs_err::create_dir_all(dir)?;
2504 fs_err::write(dir.join("pyproject.toml"), pyproject_toml)?;
2505 fs_err::create_dir_all(dir.join("src").join(name))?;
2506 fs_err::write(dir.join("src").join(name).join("__init__.py"), "")?;
2507 Ok(())
2508}
2509
2510pub const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
2512 "Z2l0aHViCg==",
2513 "cGF0Cg==",
2514 "MTFBQlVDUjZBMERMUTQ3aVphN3hPdV9qQmhTMkZUeHZ4ZE13OHczakxuZndsV2ZlZjc2cE53eHBWS2tiRUFwdnpmUk8zV0dDSUhicDFsT01aago=",
2515];
2516
2517#[cfg(not(windows))]
2519pub const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[
2520 "Z2l0aHViCg==",
2521 "cGF0Cg==",
2522 "MTFBQlVDUjZBMDJTOFYwMTM4YmQ0bV9uTXpueWhxZDBrcllROTQ5SERTeTI0dENKZ2lmdzIybDFSR2s1SE04QW8xTUVYQ1I0Q1YxYUdPRGpvZQo=",
2523];
2524
2525pub const READ_ONLY_GITHUB_SSH_DEPLOY_KEY: &str = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNBeTF1SnNZK1JXcWp1NkdIY3Z6a3AwS21yWDEwdmo3RUZqTkpNTkRqSGZPZ0FBQUpqWUpwVnAyQ2FWCmFRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQXkxdUpzWStSV3FqdTZHSGN2emtwMEttclgxMHZqN0VGak5KTU5EakhmT2cKQUFBRUMwbzBnd1BxbGl6TFBJOEFXWDVaS2dVZHJyQ2ptMDhIQm9FenB4VDg3MXBqTFc0bXhqNUZhcU83b1lkeS9PU25RcQphdGZYUytQc1FXTTBrdzBPTWQ4NkFBQUFFR3R2Ym5OMGFVQmhjM1J5WVd3dWMyZ0JBZ01FQlE9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K";
2526
2527pub fn decode_token(content: &[&str]) -> String {
2530 content
2531 .iter()
2532 .map(|part| base64.decode(part).unwrap())
2533 .map(|decoded| {
2534 std::str::from_utf8(decoded.as_slice())
2535 .unwrap()
2536 .trim_end()
2537 .to_string()
2538 })
2539 .join("_")
2540}
2541
2542#[tokio::main(flavor = "current_thread")]
2545pub async fn download_to_disk(url: &str, path: &Path) {
2546 let trusted_hosts: Vec<_> = env::var(EnvVars::UV_INSECURE_HOST)
2547 .unwrap_or_default()
2548 .split(' ')
2549 .map(|h| uv_configuration::TrustedHost::from_str(h).unwrap())
2550 .collect();
2551
2552 let client = uv_client::BaseClientBuilder::default()
2553 .allow_insecure_host(trusted_hosts)
2554 .build()
2555 .expect("failed to build base client");
2556 let url = url.parse().unwrap();
2557 let response = client
2558 .for_host(&url)
2559 .get(reqwest::Url::from(url))
2560 .send()
2561 .await
2562 .unwrap();
2563
2564 let mut file = fs_err::tokio::File::create(path).await.unwrap();
2565 let mut stream = response.bytes_stream();
2566 while let Some(chunk) = stream.next().await {
2567 file.write_all(&chunk.unwrap()).await.unwrap();
2568 }
2569 file.sync_all().await.unwrap();
2570}
2571
2572#[cfg(unix)]
2577pub struct ReadOnlyDirectoryGuard {
2578 path: PathBuf,
2579 original_mode: u32,
2580}
2581
2582#[cfg(unix)]
2583impl ReadOnlyDirectoryGuard {
2584 pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
2587 use std::os::unix::fs::PermissionsExt;
2588 let path = path.into();
2589 let metadata = fs_err::metadata(&path)?;
2590 let original_mode = metadata.permissions().mode();
2591 let readonly_mode = original_mode & !0o222;
2593 fs_err::set_permissions(&path, std::fs::Permissions::from_mode(readonly_mode))?;
2594 Ok(Self {
2595 path,
2596 original_mode,
2597 })
2598 }
2599}
2600
2601#[cfg(unix)]
2602impl Drop for ReadOnlyDirectoryGuard {
2603 fn drop(&mut self) {
2604 use std::os::unix::fs::PermissionsExt;
2605 let _ = fs_err::set_permissions(
2606 &self.path,
2607 std::fs::Permissions::from_mode(self.original_mode),
2608 );
2609 }
2610}
2611
2612#[doc(hidden)]
2616#[macro_export]
2617macro_rules! function_name {
2618 () => {{
2619 fn f() {}
2620 fn type_name_of_val<T>(_: T) -> &'static str {
2621 std::any::type_name::<T>()
2622 }
2623 let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
2624 while let Some(rest) = name.strip_suffix("::{{closure}}") {
2625 name = rest;
2626 }
2627 name
2628 }};
2629}
2630
2631#[macro_export]
2636macro_rules! uv_snapshot {
2637 ($spawnable:expr, @$snapshot:literal) => {{
2638 uv_snapshot!($crate::INSTA_FILTERS.to_vec(), $spawnable, @$snapshot)
2639 }};
2640 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2641 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), None);
2643 ::insta::assert_snapshot!(snapshot, @$snapshot);
2644 output
2645 }};
2646 ($filters:expr, $spawnable:expr, input=$input:expr, @$snapshot:literal) => {{
2647 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), Some($input));
2649 ::insta::assert_snapshot!(snapshot, @$snapshot);
2650 output
2651 }};
2652 ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{
2653 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), None, None);
2655 ::insta::assert_snapshot!(snapshot, @$snapshot);
2656 output
2657 }};
2658 ($filters:expr, universal_windows_filters=true, $spawnable:expr, @$snapshot:literal) => {{
2659 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Universal), None);
2661 ::insta::assert_snapshot!(snapshot, @$snapshot);
2662 output
2663 }};
2664}
2665
2666#[cfg(all(test, unix))]
2667mod process_status_tests {
2668 use std::process::Command;
2669
2670 use super::run_and_format_silent;
2671
2672 #[test]
2673 fn reports_signal() {
2674 let mut command = Command::new("sh");
2675 command.args(["-c", "kill -TERM $$"]);
2676 let filters: &[(&str, &str)] = &[];
2677 let (snapshot, _) = run_and_format_silent(command, filters, "reports_signal", None, None);
2678
2679 insta::assert_snapshot!(snapshot, @"
2680 exit_code: -1 (failure)
2681 exit_status: signal: 15 (SIGTERM)
2682 ");
2683 }
2684
2685 #[test]
2686 fn preserves_exit_code() {
2687 let mut command = Command::new("sh");
2688 command.args(["-c", "exit 7"]);
2689 let filters: &[(&str, &str)] = &[];
2690 let (snapshot, _) =
2691 run_and_format_silent(command, filters, "preserves_exit_code", None, None);
2692
2693 insta::assert_snapshot!(snapshot, @"exit_code: 7 (failure)");
2694 }
2695}
2696
2697#[cfg(test)]
2698mod cache_directory_tests {
2699 use std::process::Command;
2700
2701 use uv_static::EnvVars;
2702
2703 use super::assert_effective_cache_directory;
2704
2705 #[test]
2706 #[should_panic(expected = "`UV_CACHE_DIR` is ignored")]
2707 fn rejects_environment_override_with_explicit_cache_argument() {
2708 let mut command = Command::new("uv");
2709 command
2710 .arg("--cache-dir")
2711 .arg("context-cache")
2712 .env(EnvVars::UV_CACHE_DIR, "ignored-cache");
2713
2714 assert_effective_cache_directory(&command);
2715 }
2716
2717 #[test]
2718 #[should_panic(expected = "`UV_CACHE_DIR` is ignored")]
2719 fn rejects_environment_override_with_inline_cache_argument() {
2720 let mut command = Command::new("uv");
2721 command
2722 .arg("--cache-dir=context-cache")
2723 .env(EnvVars::UV_CACHE_DIR, "ignored-cache");
2724
2725 assert_effective_cache_directory(&command);
2726 }
2727
2728 #[test]
2729 fn allows_environment_override_without_explicit_cache_argument() {
2730 let mut command = Command::new("uv");
2731 command
2732 .arg("cache")
2733 .arg("dir")
2734 .env(EnvVars::UV_CACHE_DIR, "effective-cache");
2735
2736 assert_effective_cache_directory(&command);
2737 }
2738
2739 #[test]
2740 fn allows_removed_environment_override_with_explicit_cache_argument() {
2741 let mut command = Command::new("uv");
2742 command
2743 .arg("--cache-dir")
2744 .arg("context-cache")
2745 .env_remove(EnvVars::UV_CACHE_DIR);
2746
2747 assert_effective_cache_directory(&command);
2748 }
2749}