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.15";
50pub const LATEST_PYTHON_3_12: &str = "3.12.14";
51const LATEST_PYTHON_3_11: &str = "3.11.16";
52const LATEST_PYTHON_3_10: &str = "3.10.21";
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"tv_sec: \d+", "tv_sec: [TIME]"),
104 (r"tv_nsec: \d+", "tv_nsec: [TIME]"),
105 (r"\\([\w\d]|\.)", "/$1"),
107 (r"uv\.exe", "uv"),
108 (
110 r"uv(-.*)? \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?( \([^)]*\))?",
111 r"uv [VERSION] ([COMMIT] DATE)",
112 ),
113 (r"([^\s])[ \t]+(\r?\n)", "$1$2"),
115 (
117 r"(?ms)^([ \t]*custom_certificates: )(?:None|Some\(\n.*?^[ \t]*\),\n[ \t]*\)),",
118 "${1}[CERTIFICATES],",
119 ),
120 (r"DEBUG Loaded \d+ certificate\(s\) from [^\n]+\n", ""),
122];
123
124pub struct TestContext {
131 pub root: ChildPath,
132 pub temp_dir: ChildPath,
133 pub cache_dir: ChildPath,
134 python_dir: ChildPath,
135 pub home_dir: ChildPath,
136 pub user_config_dir: ChildPath,
137 pub bin_dir: ChildPath,
138 pub venv: ChildPath,
139 pub workspace_root: PathBuf,
140
141 python_version: Option<PythonVersion>,
143
144 pub python_versions: Vec<(PythonVersion, PathBuf)>,
146
147 uv_bin: PathBuf,
149
150 filters: Vec<(String, String)>,
152
153 extra_env: Vec<(OsString, OsString)>,
155
156 #[allow(dead_code)]
157 _root: tempfile::TempDir,
158
159 #[allow(dead_code)]
162 _extra_tempdirs: Vec<tempfile::TempDir>,
163}
164
165impl TestContext {
166 pub fn new_with_bin(python_version: &str, uv_bin: PathBuf) -> Self {
170 let new = Self::new_with_versions_and_bin(&[python_version], uv_bin);
171 new.create_venv();
172 new
173 }
174
175 #[must_use]
179 pub fn with_cache_dir(mut self, cache_dir: impl AsRef<Path>) -> Self {
180 let cache_dir = if cache_dir.as_ref().is_absolute() {
181 cache_dir.as_ref().to_path_buf()
182 } else {
183 self.temp_dir
184 .join(cache_dir.as_ref().components().collect::<PathBuf>())
185 };
186
187 self.filters
188 .retain(|(_, replacement)| replacement != "[CACHE_DIR]/");
189 self.cache_dir = ChildPath::new(cache_dir);
190
191 for pattern in Self::path_patterns(&self.cache_dir) {
192 self.filters
193 .insert(0, (pattern, "[CACHE_DIR]/".to_string()));
194 }
195
196 self
197 }
198
199 #[must_use]
201 pub fn with_exclude_newer(mut self, exclude_newer: &str) -> Self {
202 self.extra_env
203 .push((EnvVars::UV_EXCLUDE_NEWER.into(), exclude_newer.into()));
204 self
205 }
206
207 #[must_use]
209 pub fn with_http_timeout(mut self, http_timeout: &str) -> Self {
210 self.extra_env
211 .push((EnvVars::UV_HTTP_TIMEOUT.into(), http_timeout.into()));
212 self
213 }
214
215 #[must_use]
217 pub fn with_http_retries(mut self, http_retries: &str) -> Self {
218 self.extra_env
219 .push((EnvVars::UV_HTTP_RETRIES.into(), http_retries.into()));
220 self
221 }
222
223 #[must_use]
225 pub fn with_fast_http_retry(self) -> Self {
226 self.with_http_timeout("1").with_http_retries("1")
227 }
228
229 #[must_use]
231 pub fn with_concurrent_installs(mut self, concurrent_installs: &str) -> Self {
232 self.extra_env.push((
233 EnvVars::UV_CONCURRENT_INSTALLS.into(),
234 concurrent_installs.into(),
235 ));
236 self
237 }
238
239 #[must_use]
244 pub fn with_filtered_counts(mut self) -> Self {
245 for verb in &[
246 "Resolved",
247 "Prepared",
248 "Installed",
249 "Uninstalled",
250 "Checked",
251 ] {
252 self.filters.push((
253 format!("{verb} \\d+ packages?"),
254 format!("{verb} [N] packages"),
255 ));
256 }
257 self.with_filtered_file_counts()
258 }
259
260 #[must_use]
262 pub fn with_filtered_file_counts(mut self) -> Self {
263 self.filters.push((
264 "Removed \\d+ files?".to_string(),
265 "Removed [N] files".to_string(),
266 ));
267 self
268 }
269
270 #[must_use]
272 pub fn with_filtered_sizes(mut self) -> Self {
273 self.filters.push((
274 r"(\s|\()(\d+\.)?\d+(([KMGT]i)?B)".to_string(),
275 "$1[SIZE]$3".to_string(),
276 ));
277 self
278 }
279
280 #[must_use]
282 pub fn with_filtered_sizes_and_units(mut self) -> Self {
283 self.filters.push((
284 r"(\s|\()(\d+\.)?\d+([KMGT]i)?B".to_string(),
285 "$1[SIZE]".to_string(),
286 ));
287 self
288 }
289
290 #[must_use]
292 pub fn with_filtered_cache_size(mut self) -> Self {
293 self.filters
295 .push((r"(?m)^\d+\n".to_string(), "[SIZE]\n".to_string()));
296 self.filters.push((
298 r"(?m)^\d+(\.\d+)?( ?[KMGT]i?B)\n".to_string(),
299 "[SIZE]$2\n".to_string(),
300 ));
301 self
302 }
303
304 #[must_use]
306 pub fn with_filtered_centralized_environment_hashes(mut self) -> Self {
307 self.filters.push((
308 r"`([\w.\[\]-]+)-[a-f0-9]{16}`".to_string(),
309 "`$1-[HASH]`".to_string(),
310 ));
311 self
312 }
313
314 #[must_use]
316 pub fn with_filtered_missing_file_error(mut self) -> Self {
317 self.filters.push((
320 r"[^:\n]* \(os error 2\)".to_string(),
321 " [OS ERROR 2]".to_string(),
322 ));
323 self.filters.push((
327 r"[^:\n]* \(os error 3\)".to_string(),
328 " [OS ERROR 2]".to_string(),
329 ));
330 self
331 }
332
333 #[must_use]
336 pub fn with_filtered_exe_suffix(mut self) -> Self {
337 self.filters
338 .push((regex::escape(env::consts::EXE_SUFFIX), String::new()));
339 self
340 }
341
342 #[must_use]
344 pub fn with_filtered_python_sources(mut self) -> Self {
345 self.filters.push((
346 "virtual environments, managed installations, or search path".to_string(),
347 "[PYTHON SOURCES]".to_string(),
348 ));
349 self.filters.push((
350 "virtual environments, managed installations, search path, or registry".to_string(),
351 "[PYTHON SOURCES]".to_string(),
352 ));
353 self.filters.push((
354 "virtual environments, search path, or registry".to_string(),
355 "[PYTHON SOURCES]".to_string(),
356 ));
357 self.filters.push((
358 "virtual environments, registry, or search path".to_string(),
359 "[PYTHON SOURCES]".to_string(),
360 ));
361 self.filters.push((
362 "virtual environments or search path".to_string(),
363 "[PYTHON SOURCES]".to_string(),
364 ));
365 self.filters.push((
366 "managed installations or search path".to_string(),
367 "[PYTHON SOURCES]".to_string(),
368 ));
369 self.filters.push((
370 "managed installations, search path, or registry".to_string(),
371 "[PYTHON SOURCES]".to_string(),
372 ));
373 self.filters.push((
374 "search path or registry".to_string(),
375 "[PYTHON SOURCES]".to_string(),
376 ));
377 self.filters.push((
378 "registry or search path".to_string(),
379 "[PYTHON SOURCES]".to_string(),
380 ));
381 self.filters
382 .push(("search path".to_string(), "[PYTHON SOURCES]".to_string()));
383 self
384 }
385
386 #[must_use]
389 pub fn with_filtered_python_names(mut self) -> Self {
390 for name in ["python", "pypy"] {
391 let suffix = if cfg!(windows) {
394 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
398 format!(r"(\d\.\d+|\d)?{exe_suffix}")
399 } else {
400 if name == "python" {
402 r"(\d\.\d+|\d)?(t|d|td)?".to_string()
404 } else {
405 r"(\d\.\d+|\d)(t|d|td)?".to_string()
407 }
408 };
409
410 self.filters.push((
411 format!(r"[\\/]{name}{suffix}"),
414 format!("/[{}]", name.to_uppercase()),
415 ));
416 }
417
418 self
419 }
420
421 #[must_use]
424 pub fn with_filtered_virtualenv_bin(mut self) -> Self {
425 self.filters.push((
426 format!(
427 r"[\\/]{}[\\/]",
428 venv_bin_path(PathBuf::new()).to_string_lossy()
429 ),
430 "/[BIN]/".to_string(),
431 ));
432 self.filters.push((
433 format!(r"[\\/]{}", venv_bin_path(PathBuf::new()).to_string_lossy()),
434 "/[BIN]".to_string(),
435 ));
436 self
437 }
438
439 #[must_use]
443 pub fn with_filtered_python_install_bin(mut self) -> Self {
444 let suffix = if cfg!(windows) {
447 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
448 format!(r"(\d\.\d+|\d)?{exe_suffix}")
450 } else {
451 r"\d\.\d+|\d".to_string()
453 };
454
455 if cfg!(unix) {
456 self.filters.push((
457 format!(r"[\\/]bin/python({suffix})"),
458 "/[INSTALL-BIN]/python$1".to_string(),
459 ));
460 self.filters.push((
461 format!(r"[\\/]bin/pypy({suffix})"),
462 "/[INSTALL-BIN]/pypy$1".to_string(),
463 ));
464 } else {
465 self.filters.push((
466 format!(r"[\\/]python({suffix})"),
467 "/[INSTALL-BIN]/python$1".to_string(),
468 ));
469 self.filters.push((
470 format!(r"[\\/]pypy({suffix})"),
471 "/[INSTALL-BIN]/pypy$1".to_string(),
472 ));
473 }
474 self
475 }
476
477 #[must_use]
482 pub fn with_pyvenv_cfg_filters(mut self) -> Self {
483 let added_filters = [
484 (r"home = .+".to_string(), "home = [PYTHON_HOME]".to_string()),
485 (
486 r"uv = \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?".to_string(),
487 "uv = [UV_VERSION]".to_string(),
488 ),
489 ];
490 for filter in added_filters {
491 self.filters.insert(0, filter);
492 }
493 self
494 }
495
496 #[must_use]
499 pub fn with_filtered_python_symlinks(mut self) -> Self {
500 for (version, executable) in &self.python_versions {
501 if fs_err::symlink_metadata(executable).unwrap().is_symlink() {
502 self.filters.extend(
503 Self::path_patterns(executable.read_link().unwrap())
504 .into_iter()
505 .map(|pattern| (format! {" -> {pattern}"}, String::new())),
506 );
507 }
508 self.filters.push((
510 regex::escape(&format!(" -> [PYTHON-{version}]")),
511 String::new(),
512 ));
513 }
514 self
515 }
516
517 #[must_use]
519 pub fn with_filtered_path(mut self, path: &Path, name: &str) -> Self {
520 for pattern in Self::path_patterns(path)
524 .into_iter()
525 .map(|pattern| (pattern, format!("[{name}]/")))
526 {
527 self.filters.insert(0, pattern);
528 }
529 self
530 }
531
532 #[inline]
540 #[must_use]
541 pub fn with_filtered_link_mode_warning(mut self) -> Self {
542 let pattern = "warning: Failed to hardlink files; .*\n.*\n.*\n";
543 self.filters.push((pattern.to_string(), String::new()));
544 self
545 }
546
547 #[inline]
549 #[must_use]
550 pub fn with_filtered_not_executable(mut self) -> Self {
551 let pattern = if cfg!(unix) {
552 r"Permission denied \(os error 13\)"
553 } else {
554 r"\%1 is not a valid Win32 application. \(os error 193\)"
555 };
556 self.filters
557 .push((pattern.to_string(), "[PERMISSION DENIED]".to_string()));
558 self
559 }
560
561 #[must_use]
563 pub fn with_filtered_python_keys(mut self) -> Self {
564 let platform_re = r"(?x)
566 ( # We capture the group before the platform
567 (?:cpython|pypy|graalpy)# Python implementation
568 -
569 \d+\.\d+ # Major and minor version
570 (?: # The patch version is handled separately
571 \.
572 (?:
573 \[X\] # A previously filtered patch version [X]
574 | # OR
575 \[LATEST\] # A previously filtered latest patch version [LATEST]
576 | # OR
577 \d+ # An actual patch version
578 )
579 )? # (we allow the patch version to be missing entirely, e.g., in a request)
580 (?:(?:a|b|rc)[0-9]+)? # Pre-release version component, e.g., `a6` or `rc2`
581 (?:[td])? # A short variant, such as `t` (for freethreaded) or `d` (for debug)
582 (?:(\+[a-z]+)+)? # A long variant, such as `+freethreaded` or `+freethreaded+debug`
583 )
584 -
585 [a-z0-9]+ # Operating system (e.g., 'macos')
586 -
587 [a-z0-9_]+ # Architecture (e.g., 'aarch64')
588 -
589 [a-z]+ # Libc (e.g., 'none')
590";
591 self.filters
592 .push((platform_re.to_string(), "$1-[PLATFORM]".to_string()));
593 self
594 }
595
596 #[must_use]
598 pub fn with_filtered_latest_python_versions(mut self) -> Self {
599 for (minor, patch) in [
602 ("3.15", LATEST_PYTHON_3_15.strip_prefix("3.15.").unwrap()),
603 ("3.14", LATEST_PYTHON_3_14.strip_prefix("3.14.").unwrap()),
604 ("3.13", LATEST_PYTHON_3_13.strip_prefix("3.13.").unwrap()),
605 ("3.12", LATEST_PYTHON_3_12.strip_prefix("3.12.").unwrap()),
606 ("3.11", LATEST_PYTHON_3_11.strip_prefix("3.11.").unwrap()),
607 ("3.10", LATEST_PYTHON_3_10.strip_prefix("3.10.").unwrap()),
608 ] {
609 let pattern = format!(r"(\b){minor}\.{patch}(\b)");
611 let replacement = format!("${{1}}{minor}.[LATEST]${{2}}");
612 self.filters.push((pattern, replacement));
613 }
614 self
615 }
616
617 #[must_use]
619 #[cfg(windows)]
620 pub fn with_filtered_windows_temp_dir(mut self) -> Self {
621 let pattern = regex::escape(
622 &self
623 .temp_dir
624 .simplified_display()
625 .to_string()
626 .replace('/', "\\"),
627 );
628 self.filters.push((pattern, "[TEMP_DIR]".to_string()));
629 self
630 }
631
632 #[must_use]
634 pub fn with_filtered_compiled_file_count(mut self) -> Self {
635 self.filters.push((
636 r"compiled \d+ files".to_string(),
637 "compiled [COUNT] files".to_string(),
638 ));
639 self
640 }
641
642 #[must_use]
644 pub fn with_filtered_current_version(mut self) -> Self {
645 self.filters.push((
646 regex::escape(&format!("v{}", env!("CARGO_PKG_VERSION"))),
647 "v[CURRENT_VERSION]".to_string(),
648 ));
649 self
650 }
651
652 #[must_use]
654 pub fn with_cyclonedx_filters(mut self) -> Self {
655 self.filters.push((
656 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(),
657 "[SERIAL_NUMBER]".to_string(),
658 ));
659 self.filters.push((
660 r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#
661 .to_string(),
662 r#""timestamp": "[TIMESTAMP]""#.to_string(),
663 ));
664 self.filters.push((
665 r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#
666 .to_string(),
667 r#""name": "uv",
668 "version": "[VERSION]""#
669 .to_string(),
670 ));
671 self
672 }
673
674 #[must_use]
676 pub fn with_collapsed_whitespace(mut self) -> Self {
677 self.filters.push((r"[ \t]+".to_string(), " ".to_string()));
678 self
679 }
680
681 #[must_use]
683 pub fn with_python_download_cache(mut self) -> Self {
684 self.extra_env.push((
685 EnvVars::UV_PYTHON_CACHE_DIR.into(),
686 env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).unwrap_or_else(|| {
688 uv_cache::Cache::from_settings(false, None)
689 .unwrap()
690 .bucket(CacheBucket::Python)
691 .into()
692 }),
693 ));
694 self
695 }
696
697 #[must_use]
698 pub fn with_empty_python_install_mirror(mut self) -> Self {
699 self.extra_env.push((
700 EnvVars::UV_PYTHON_INSTALL_MIRROR.into(),
701 String::new().into(),
702 ));
703 self
704 }
705
706 #[must_use]
708 pub fn with_managed_python_dirs(mut self) -> Self {
709 let managed = self.temp_dir.join("managed");
710
711 self.extra_env.push((
712 EnvVars::UV_PYTHON_BIN_DIR.into(),
713 self.bin_dir.as_os_str().to_owned(),
714 ));
715 self.extra_env
716 .push((EnvVars::UV_PYTHON_INSTALL_DIR.into(), managed.into()));
717 self.extra_env
718 .push((EnvVars::UV_PYTHON_DOWNLOADS.into(), "automatic".into()));
719
720 self
721 }
722
723 #[must_use]
725 pub fn with_tool_dirs(mut self) -> Self {
726 self.extra_env.push((
727 EnvVars::UV_TOOL_DIR.into(),
728 self.temp_dir.join("tools").into(),
729 ));
730 self.extra_env.push((
731 EnvVars::XDG_BIN_HOME.into(),
732 self.temp_dir.join("bin").into(),
733 ));
734
735 self
736 }
737
738 #[must_use]
739 pub fn with_versions_as_managed(mut self, versions: &[&str]) -> Self {
740 self.extra_env.push((
741 EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED.into(),
742 versions.iter().join(" ").into(),
743 ));
744
745 self
746 }
747
748 #[must_use]
750 pub fn with_filter(mut self, filter: (impl Into<String>, impl Into<String>)) -> Self {
751 self.filters.push((filter.0.into(), filter.1.into()));
752 self
753 }
754
755 #[must_use]
757 pub fn with_unset_git_credential_helper(self) -> Self {
758 let git_config = self.home_dir.child(".gitconfig");
759 git_config
760 .write_str(indoc! {r"
761 [credential]
762 helper =
763 "})
764 .expect("Failed to unset git credential helper");
765
766 self
767 }
768
769 #[must_use]
771 #[cfg(windows)]
772 pub fn clear_filters(mut self) -> Self {
773 self.filters.clear();
774 self
775 }
776
777 pub fn with_cache_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
782 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
783 return Ok(None);
784 };
785 self.with_cache_on_fs(&dir, "COW_FS").map(Some)
786 }
787
788 pub fn with_cache_on_alt_fs(self) -> anyhow::Result<Option<Self>> {
793 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_ALT_FS).ok() else {
794 return Ok(None);
795 };
796 self.with_cache_on_fs(&dir, "ALT_FS").map(Some)
797 }
798
799 pub fn with_cache_on_lowlinks_fs(self) -> anyhow::Result<Option<Self>> {
804 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_LOWLINKS_FS).ok() else {
805 return Ok(None);
806 };
807 self.with_cache_on_fs(&dir, "LOWLINKS_FS").map(Some)
808 }
809
810 pub fn with_cache_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
815 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
816 return Ok(None);
817 };
818 self.with_cache_on_fs(&dir, "NOCOW_FS").map(Some)
819 }
820
821 pub fn with_working_dir_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
828 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
829 return Ok(None);
830 };
831 self.with_working_dir_on_fs(&dir, "COW_FS").map(Some)
832 }
833
834 pub fn with_working_dir_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_working_dir_on_fs(&dir, "NOCOW_FS").map(Some)
845 }
846
847 fn with_cache_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
848 fs_err::create_dir_all(dir)?;
849 let tmp = tempfile::TempDir::new_in(dir)?;
850 self.cache_dir = ChildPath::new(tmp.path()).child("cache");
851 fs_err::create_dir_all(&self.cache_dir)?;
852 let replacement = format!("[{name}]/[CACHE_DIR]/");
853 for pattern in Self::path_patterns(&self.cache_dir) {
854 self.filters.insert(0, (pattern, replacement.clone()));
855 }
856 self._extra_tempdirs.push(tmp);
857 Ok(self)
858 }
859
860 fn with_working_dir_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
861 fs_err::create_dir_all(dir)?;
862 let tmp = tempfile::TempDir::new_in(dir)?;
863 self.temp_dir = ChildPath::new(tmp.path()).child("temp");
864 fs_err::create_dir_all(&self.temp_dir)?;
865 let canonical_temp_dir = self.temp_dir.canonicalize()?;
868 self.venv = ChildPath::new(canonical_temp_dir.join(".venv"));
869 let temp_replacement = format!("[{name}]/[TEMP_DIR]/");
870 self.filters.extend(
871 Self::path_patterns(&self.temp_dir)
872 .into_iter()
873 .map(|pattern| (pattern, temp_replacement.clone())),
874 );
875 let venv_replacement = format!("[{name}]/[VENV]/");
876 self.filters.extend(
877 Self::path_patterns(&self.venv)
878 .into_iter()
879 .map(|pattern| (pattern, venv_replacement.clone())),
880 );
881 self._extra_tempdirs.push(tmp);
882 Ok(self)
883 }
884
885 pub fn test_bucket_dir() -> PathBuf {
894 std::env::temp_dir()
895 .simple_canonicalize()
896 .expect("failed to canonicalize temp dir")
897 .join("uv")
898 .join("tests")
899 }
900
901 pub fn new_with_versions_and_bin(python_versions: &[&str], uv_bin: PathBuf) -> Self {
908 let bucket = Self::test_bucket_dir();
909 fs_err::create_dir_all(&bucket).expect("Failed to create test bucket");
910
911 let root = tempfile::TempDir::new_in(bucket).expect("Failed to create test root directory");
912
913 fs_err::create_dir_all(root.path().join(".git"))
916 .expect("Failed to create `.git` placeholder in test root directory");
917
918 let temp_dir = ChildPath::new(root.path()).child("temp");
919 fs_err::create_dir_all(&temp_dir).expect("Failed to create test working directory");
920
921 let cache_dir = ChildPath::new(root.path()).child("cache");
922 fs_err::create_dir_all(&cache_dir).expect("Failed to create test cache directory");
923
924 let python_dir = ChildPath::new(root.path()).child("python");
925 fs_err::create_dir_all(&python_dir).expect("Failed to create test Python directory");
926
927 let bin_dir = ChildPath::new(root.path()).child("bin");
928 fs_err::create_dir_all(&bin_dir).expect("Failed to create test bin directory");
929
930 if cfg!(not(feature = "git")) {
932 Self::disallow_git_cli(&bin_dir).expect("Failed to setup disallowed `git` command");
933 }
934
935 let home_dir = ChildPath::new(root.path()).child("home");
936 fs_err::create_dir_all(&home_dir).expect("Failed to create test home directory");
937
938 let user_config_dir = if cfg!(windows) {
939 ChildPath::new(home_dir.path())
940 } else {
941 ChildPath::new(home_dir.path()).child(".config")
942 };
943
944 let canonical_temp_dir = temp_dir.canonicalize().unwrap();
946 let venv = ChildPath::new(canonical_temp_dir.join(".venv"));
947
948 let python_version = python_versions
949 .first()
950 .map(|version| PythonVersion::from_str(version).unwrap());
951
952 let site_packages = python_version
953 .as_ref()
954 .map(|version| site_packages_path(&venv, &format!("python{version}")));
955
956 let workspace_root = Path::new(&env::var(EnvVars::CARGO_MANIFEST_DIR).unwrap())
959 .parent()
960 .expect("CARGO_MANIFEST_DIR should be nested in workspace")
961 .parent()
962 .expect("CARGO_MANIFEST_DIR should be doubly nested in workspace")
963 .to_path_buf();
964
965 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
966
967 let python_versions: Vec<_> = python_versions
968 .iter()
969 .map(|version| PythonVersion::from_str(version).unwrap())
970 .zip(
971 python_installations_for_versions(&temp_dir, python_versions, &download_list)
972 .expect("Failed to find test Python versions"),
973 )
974 .collect();
975
976 if cfg!(unix) {
979 for (version, executable) in &python_versions {
980 let parent = python_dir.child(version.to_string());
981 parent.create_dir_all().unwrap();
982 parent.child("python3").symlink_to_file(executable).unwrap();
983 }
984 }
985
986 let mut filters = Vec::new();
987
988 filters.extend(
989 Self::path_patterns(&uv_bin)
990 .into_iter()
991 .map(|pattern| (pattern, "[UV]".to_string())),
992 );
993
994 if cfg!(windows) {
996 filters.push((" --link-mode <LINK_MODE>".to_string(), String::new()));
997 filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new()));
998 filters.push((r"exit code: ".to_string(), "exit status: ".to_string()));
1000 }
1001
1002 for (version, executable) in &python_versions {
1003 filters.extend(
1005 Self::path_patterns(executable)
1006 .into_iter()
1007 .map(|pattern| (pattern, format!("[PYTHON-{version}]"))),
1008 );
1009
1010 filters.extend(
1012 Self::path_patterns(python_dir.join(version.to_string()))
1013 .into_iter()
1014 .map(|pattern| {
1015 (
1016 format!("{pattern}[a-zA-Z0-9]*"),
1017 format!("[PYTHON-{version}]"),
1018 )
1019 }),
1020 );
1021
1022 if version.patch().is_none() {
1025 filters.push((
1026 format!(r"({})\.\d+", regex::escape(version.to_string().as_str())),
1027 "$1.[X]".to_string(),
1028 ));
1029 }
1030 }
1031
1032 filters.extend(
1033 Self::path_patterns(&bin_dir)
1034 .into_iter()
1035 .map(|pattern| (pattern, "[BIN]/".to_string())),
1036 );
1037 filters.extend(
1038 Self::path_patterns(&cache_dir)
1039 .into_iter()
1040 .map(|pattern| (pattern, "[CACHE_DIR]/".to_string())),
1041 );
1042 if let Some(ref site_packages) = site_packages {
1043 filters.extend(
1044 Self::path_patterns(site_packages)
1045 .into_iter()
1046 .map(|pattern| (pattern, "[SITE_PACKAGES]/".to_string())),
1047 );
1048 }
1049 filters.extend(
1050 Self::path_patterns(&venv)
1051 .into_iter()
1052 .map(|pattern| (pattern, "[VENV]/".to_string())),
1053 );
1054
1055 if let Some(site_packages) = site_packages {
1057 filters.push((
1058 Self::path_pattern(
1059 site_packages
1060 .strip_prefix(&canonical_temp_dir)
1061 .expect("The test site-packages directory is always in the tempdir"),
1062 ),
1063 "[SITE_PACKAGES]/".to_string(),
1064 ));
1065 }
1066
1067 filters.push((
1069 r"[\\/]lib[\\/]python\d+\.\d+[\\/]".to_string(),
1070 "/[PYTHON-LIB]/".to_string(),
1071 ));
1072 filters.push((r"[\\/]Lib[\\/]".to_string(), "/[PYTHON-LIB]/".to_string()));
1073
1074 filters.extend(
1075 Self::path_patterns(&temp_dir)
1076 .into_iter()
1077 .map(|pattern| (pattern, "[TEMP_DIR]/".to_string())),
1078 );
1079 filters.extend(
1080 Self::path_patterns(&python_dir)
1081 .into_iter()
1082 .map(|pattern| (pattern, "[PYTHON_DIR]/".to_string())),
1083 );
1084 let mut uv_user_config_dir = PathBuf::from(user_config_dir.path());
1085 uv_user_config_dir.push("uv");
1086 filters.extend(
1087 Self::path_patterns(&uv_user_config_dir)
1088 .into_iter()
1089 .map(|pattern| (pattern, "[UV_USER_CONFIG_DIR]/".to_string())),
1090 );
1091 filters.extend(
1092 Self::path_patterns(&user_config_dir)
1093 .into_iter()
1094 .map(|pattern| (pattern, "[USER_CONFIG_DIR]/".to_string())),
1095 );
1096 filters.extend(
1097 Self::path_patterns(&home_dir)
1098 .into_iter()
1099 .map(|pattern| (pattern, "[HOME]/".to_string())),
1100 );
1101 filters.extend(
1102 Self::path_patterns(&workspace_root)
1103 .into_iter()
1104 .map(|pattern| (pattern, "[WORKSPACE]/".to_string())),
1105 );
1106
1107 filters.push((
1109 r"Activate with: (.*)\\Scripts\\activate".to_string(),
1110 "Activate with: source $1/[BIN]/activate".to_string(),
1111 ));
1112 filters.push((
1113 r"Activate with: Scripts\\activate".to_string(),
1114 "Activate with: source [BIN]/activate".to_string(),
1115 ));
1116 filters.push((
1117 r"Activate with: source (.*/|)bin/activate(?:\.\w+)?".to_string(),
1118 "Activate with: source $1[BIN]/activate".to_string(),
1119 ));
1120
1121 filters.push((
1124 r#"(\\|/)\.tmp[^\\/\s"'`]*"#.to_string(),
1125 "/[TMP]".to_string(),
1126 ));
1127
1128 filters.push((r"file:///".to_string(), "file://".to_string()));
1130
1131 filters.push((r"\\\\\?\\".to_string(), String::new()));
1133
1134 filters.push((r"127\.0\.0\.1:\d*".to_string(), "[LOCALHOST]".to_string()));
1136 filters.push((
1138 format!(
1139 r#"requires = \["uv_build>={},<[0-9.]+"\]"#,
1140 uv_version::version()
1141 ),
1142 r#"requires = ["uv_build>=[CURRENT_VERSION],<[NEXT_BREAKING]"]"#.to_string(),
1143 ));
1144 filters.push((
1146 r"environments-v(\d+)[\\/]([\w.\[\]-]+)-[a-f0-9]{16}".to_string(),
1147 "environments-v$1/$2-[HASH]".to_string(),
1148 ));
1149 filters.push((
1151 r"archive-v(\d+)[\\/][A-Za-z0-9\-\_]+".to_string(),
1152 "archive-v$1/[HASH]".to_string(),
1153 ));
1154
1155 Self {
1156 root: ChildPath::new(root.path()),
1157 temp_dir,
1158 cache_dir,
1159 python_dir,
1160 home_dir,
1161 user_config_dir,
1162 bin_dir,
1163 venv,
1164 workspace_root,
1165 python_version,
1166 python_versions,
1167 uv_bin,
1168 filters,
1169 extra_env: vec![],
1170 _root: root,
1171 _extra_tempdirs: vec![],
1172 }
1173 }
1174
1175 pub fn command(&self) -> Command {
1177 let mut command = self.new_command();
1178 self.add_shared_options(&mut command, true);
1179 command
1180 }
1181
1182 pub fn disallow_git_cli(bin_dir: &Path) -> std::io::Result<()> {
1183 let contents = r"#!/bin/sh
1184 echo 'error: `git` operations are not allowed — are you missing a cfg for the `git` feature?' >&2
1185 exit 127";
1186 let git = bin_dir.join(format!("git{}", env::consts::EXE_SUFFIX));
1187 fs_err::write(&git, contents)?;
1188
1189 #[cfg(unix)]
1190 {
1191 use std::os::unix::fs::PermissionsExt;
1192 let mut perms = fs_err::metadata(&git)?.permissions();
1193 perms.set_mode(0o755);
1194 fs_err::set_permissions(&git, perms)?;
1195 }
1196
1197 Ok(())
1198 }
1199
1200 #[must_use]
1205 pub fn with_git_lfs_config(mut self) -> Self {
1206 let git_lfs_config = self.root.child(".gitconfig");
1207 git_lfs_config
1208 .write_str(indoc! {r#"
1209 [filter "lfs"]
1210 clean = git-lfs clean -- %f
1211 smudge = git-lfs smudge -- %f
1212 process = git-lfs filter-process
1213 required = true
1214 "#})
1215 .expect("Failed to setup `git-lfs` filters");
1216
1217 self.extra_env.push((
1220 EnvVars::GIT_CONFIG_GLOBAL.into(),
1221 git_lfs_config.as_os_str().into(),
1222 ));
1223 self
1224 }
1225
1226 pub fn add_shared_options(&self, command: &mut Command, activate_venv: bool) {
1238 self.add_shared_args(command);
1239 self.add_shared_env(command, activate_venv);
1240 }
1241
1242 fn add_shared_args(&self, command: &mut Command) {
1244 command.arg("--cache-dir").arg(self.cache_dir.path());
1245 }
1246
1247 pub fn add_shared_env(&self, command: &mut Command, activate_venv: bool) {
1249 let path = env::join_paths(std::iter::once(self.bin_dir.to_path_buf()).chain(
1251 env::split_paths(&env::var(EnvVars::PATH).unwrap_or_default()),
1252 ))
1253 .unwrap();
1254
1255 if cfg!(not(windows)) {
1258 command.env(EnvVars::SHELL, "bash");
1259 }
1260
1261 command
1262 .env_remove(EnvVars::VIRTUAL_ENV)
1264 .env(EnvVars::UV_NO_WRAP, "1")
1266 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1268 .env(EnvVars::COLUMNS, "100")
1271 .env(EnvVars::PATH, path)
1272 .env(EnvVars::HOME, self.home_dir.as_os_str())
1273 .env(EnvVars::APPDATA, self.home_dir.as_os_str())
1274 .env(EnvVars::USERPROFILE, self.home_dir.as_os_str())
1275 .env(
1276 EnvVars::XDG_CONFIG_DIRS,
1277 self.home_dir.join("config").as_os_str(),
1278 )
1279 .env(
1280 EnvVars::XDG_DATA_HOME,
1281 self.home_dir.join("data").as_os_str(),
1282 )
1283 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1284 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "")
1285 .env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
1287 .env(EnvVars::UV_PYTHON_SEARCH_PATH, self.python_path())
1288 .env(EnvVars::UV_EXCLUDE_NEWER, TEST_TIMESTAMP)
1289 .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, TEST_TIMESTAMP)
1290 .env(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF, TEST_TIMESTAMP)
1291 .env(EnvVars::UV_PYTHON_NO_REGISTRY, "1")
1294 .env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "0")
1295 .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1")
1298 .env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path())
1312 .current_dir(self.temp_dir.path());
1313
1314 for (key, value) in &self.extra_env {
1315 command.env(key, value);
1316 }
1317
1318 if activate_venv {
1319 command.env(EnvVars::VIRTUAL_ENV, self.venv.as_os_str());
1320 }
1321
1322 if cfg!(unix) {
1323 command.env(EnvVars::LC_ALL, "C");
1325 }
1326 }
1327
1328 pub fn pip_compile(&self) -> Command {
1330 let mut command = self.new_command();
1331 command.arg("pip").arg("compile");
1332 self.add_shared_options(&mut command, true);
1333 command
1334 }
1335
1336 pub fn pip_sync(&self) -> Command {
1338 let mut command = self.new_command();
1339 command.arg("pip").arg("sync");
1340 self.add_shared_options(&mut command, true);
1341 command
1342 }
1343
1344 pub fn pip_show(&self) -> Command {
1345 let mut command = self.new_command();
1346 command.arg("pip").arg("show");
1347 self.add_shared_options(&mut command, true);
1348 command
1349 }
1350
1351 pub fn pip_freeze(&self) -> Command {
1353 let mut command = self.new_command();
1354 command.arg("pip").arg("freeze");
1355 self.add_shared_options(&mut command, true);
1356 command
1357 }
1358
1359 pub fn pip_check(&self) -> Command {
1361 let mut command = self.new_command();
1362 command.arg("pip").arg("check");
1363 self.add_shared_options(&mut command, true);
1364 command
1365 }
1366
1367 pub fn pip_list(&self) -> Command {
1368 let mut command = self.new_command();
1369 command.arg("pip").arg("list");
1370 self.add_shared_options(&mut command, true);
1371 command
1372 }
1373
1374 pub fn venv(&self) -> Command {
1376 let mut command = self.new_command();
1377 command.arg("venv");
1378 self.add_shared_options(&mut command, false);
1379 command
1380 }
1381
1382 pub fn pip_install(&self) -> Command {
1384 let mut command = self.new_command();
1385 command.arg("pip").arg("install");
1386 self.add_shared_options(&mut command, true);
1387 command
1388 }
1389
1390 pub fn pip_uninstall(&self) -> Command {
1392 let mut command = self.new_command();
1393 command.arg("pip").arg("uninstall");
1394 self.add_shared_options(&mut command, true);
1395 command
1396 }
1397
1398 pub fn pip_tree(&self) -> Command {
1400 let mut command = self.new_command();
1401 command.arg("pip").arg("tree");
1402 self.add_shared_options(&mut command, true);
1403 command
1404 }
1405
1406 pub fn pip_debug(&self) -> Command {
1408 let mut command = self.new_command();
1409 command.arg("pip").arg("debug");
1410 self.add_shared_options(&mut command, true);
1411 command
1412 }
1413
1414 pub fn help(&self) -> Command {
1416 let mut command = self.new_command();
1417 command.arg("help");
1418 self.add_shared_env(&mut command, false);
1419 command
1420 }
1421
1422 pub fn init(&self) -> Command {
1425 let mut command = self.new_command();
1426 command.arg("init");
1427 self.add_shared_options(&mut command, false);
1428 command
1429 }
1430
1431 pub fn sync(&self) -> Command {
1433 let mut command = self.new_command();
1434 command.arg("sync");
1435 self.add_shared_options(&mut command, false);
1436 command
1437 }
1438
1439 pub fn lock(&self) -> Command {
1441 let mut command = self.new_command();
1442 command.arg("lock");
1443 self.add_shared_options(&mut command, false);
1444 command
1445 }
1446
1447 pub fn upgrade(&self) -> Command {
1449 let mut command = self.new_command();
1450 command.arg("upgrade");
1451 self.add_shared_options(&mut command, false);
1452 command
1453 }
1454
1455 pub fn audit(&self) -> Command {
1457 let mut command = self.new_command();
1458 command.arg("audit");
1459 self.add_shared_options(&mut command, false);
1460 command
1461 }
1462
1463 pub fn workspace_metadata(&self) -> Command {
1465 let mut command = self.new_command();
1466 command.arg("workspace").arg("metadata");
1467 self.add_shared_options(&mut command, false);
1468 command
1469 }
1470
1471 pub fn workspace_dir(&self) -> Command {
1473 let mut command = self.new_command();
1474 command.arg("workspace").arg("dir");
1475 self.add_shared_options(&mut command, false);
1476 command
1477 }
1478
1479 pub fn workspace_list(&self) -> Command {
1481 let mut command = self.new_command();
1482 command.arg("workspace").arg("list");
1483 self.add_shared_options(&mut command, false);
1484 command
1485 }
1486
1487 pub fn export(&self) -> Command {
1489 let mut command = self.new_command();
1490 command.arg("export");
1491 self.add_shared_options(&mut command, false);
1492 command
1493 }
1494
1495 pub fn format(&self) -> Command {
1497 let mut command = self.new_command();
1498 command.arg("format");
1499 self.add_shared_options(&mut command, false);
1500 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1502 command
1503 }
1504
1505 pub fn check(&self) -> Command {
1507 let mut command = self.new_command();
1508 command.arg("check");
1509 self.add_shared_options(&mut command, false);
1510 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1512 command
1513 }
1514
1515 pub fn build(&self) -> Command {
1517 let mut command = self.new_command();
1518 command.arg("build");
1519 self.add_shared_options(&mut command, false);
1520 command
1521 }
1522
1523 pub fn version(&self) -> Command {
1524 let mut command = self.new_command();
1525 command.arg("version");
1526 self.add_shared_options(&mut command, false);
1527 command
1528 }
1529
1530 pub fn self_version(&self) -> Command {
1531 let mut command = self.new_command();
1532 command.arg("self").arg("version");
1533 self.add_shared_options(&mut command, false);
1534 command
1535 }
1536
1537 pub fn self_update(&self) -> Command {
1538 let mut command = self.new_command();
1539 command.arg("self").arg("update");
1540 self.add_shared_options(&mut command, false);
1541 command
1542 }
1543
1544 pub fn publish(&self) -> Command {
1546 let mut command = self.new_command();
1547 command.arg("publish");
1548 self.add_shared_options(&mut command, false);
1549 command
1550 }
1551
1552 pub fn python_find(&self) -> Command {
1554 let mut command = self.new_command();
1555 command
1556 .arg("python")
1557 .arg("find")
1558 .env(EnvVars::UV_PREVIEW, "1")
1559 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1560 self.add_shared_options(&mut command, false);
1561 command
1562 }
1563
1564 pub fn python_list(&self) -> Command {
1566 let mut command = self.new_command();
1567 command
1568 .arg("python")
1569 .arg("list")
1570 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1571 self.add_shared_options(&mut command, false);
1572 command
1573 }
1574
1575 pub fn python_install(&self) -> Command {
1577 let mut command = self.new_command();
1578 command.arg("python").arg("install");
1579 self.add_shared_options(&mut command, true);
1580 command
1581 }
1582
1583 pub fn python_uninstall(&self) -> Command {
1585 let mut command = self.new_command();
1586 command.arg("python").arg("uninstall");
1587 self.add_shared_options(&mut command, true);
1588 command
1589 }
1590
1591 pub fn python_upgrade(&self) -> Command {
1593 let mut command = self.new_command();
1594 command.arg("python").arg("upgrade");
1595 self.add_shared_options(&mut command, true);
1596 command
1597 }
1598
1599 pub fn python_pin(&self) -> Command {
1601 let mut command = self.new_command();
1602 command.arg("python").arg("pin");
1603 self.add_shared_options(&mut command, true);
1604 command
1605 }
1606
1607 pub fn python_dir(&self) -> Command {
1609 let mut command = self.new_command();
1610 command.arg("python").arg("dir");
1611 self.add_shared_options(&mut command, true);
1612 command
1613 }
1614
1615 pub fn run(&self) -> Command {
1617 let mut command = self.new_command();
1618 command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1");
1619 self.add_shared_options(&mut command, true);
1620 command
1621 }
1622
1623 pub fn tool_run(&self) -> Command {
1625 let mut command = self.new_command();
1626 command
1627 .arg("tool")
1628 .arg("run")
1629 .env(EnvVars::UV_SHOW_RESOLUTION, "1");
1630 self.add_shared_options(&mut command, false);
1631 command
1632 }
1633
1634 pub fn tool_upgrade(&self) -> Command {
1636 let mut command = self.new_command();
1637 command.arg("tool").arg("upgrade");
1638 self.add_shared_options(&mut command, false);
1639 command
1640 }
1641
1642 pub fn tool_install(&self) -> Command {
1644 let mut command = self.new_command();
1645 command.arg("tool").arg("install");
1646 self.add_shared_options(&mut command, false);
1647 command
1648 }
1649
1650 pub fn tool_list(&self) -> Command {
1652 let mut command = self.new_command();
1653 command.arg("tool").arg("list");
1654 self.add_shared_options(&mut command, false);
1655 command
1656 }
1657
1658 pub fn tool_audit(&self) -> Command {
1660 let mut command = self.new_command();
1661 command.arg("tool").arg("audit");
1662 self.add_shared_options(&mut command, false);
1663 command
1664 }
1665
1666 pub fn tool_dir(&self) -> Command {
1668 let mut command = self.new_command();
1669 command.arg("tool").arg("dir");
1670 self.add_shared_options(&mut command, false);
1671 command
1672 }
1673
1674 pub fn tool_uninstall(&self) -> Command {
1676 let mut command = self.new_command();
1677 command.arg("tool").arg("uninstall");
1678 self.add_shared_options(&mut command, false);
1679 command
1680 }
1681
1682 pub fn add(&self) -> Command {
1684 let mut command = self.new_command();
1685 command.arg("add");
1686 self.add_shared_options(&mut command, false);
1687 command
1688 }
1689
1690 pub fn remove(&self) -> Command {
1692 let mut command = self.new_command();
1693 command.arg("remove");
1694 self.add_shared_options(&mut command, false);
1695 command
1696 }
1697
1698 pub fn tree(&self) -> Command {
1700 let mut command = self.new_command();
1701 command.arg("tree");
1702 self.add_shared_options(&mut command, false);
1703 command
1704 }
1705
1706 pub fn clean(&self) -> Command {
1708 let mut command = self.new_command();
1709 command.arg("cache").arg("clean");
1710 self.add_shared_options(&mut command, false);
1711 command
1712 }
1713
1714 pub fn prune(&self) -> Command {
1716 let mut command = self.new_command();
1717 command.arg("cache").arg("prune");
1718 self.add_shared_options(&mut command, false);
1719 command
1720 }
1721
1722 pub fn cache_size(&self) -> Command {
1724 let mut command = self.new_command();
1725 command.arg("cache").arg("size");
1726 self.add_shared_options(&mut command, false);
1727 command
1728 }
1729
1730 pub fn build_backend(&self) -> Command {
1734 let mut command = self.new_command();
1735 command.arg("build-backend");
1736 self.add_shared_options(&mut command, false);
1737 command
1738 }
1739
1740 pub fn interpreter(&self) -> PathBuf {
1744 let venv = &self.venv;
1745 if cfg!(unix) {
1746 venv.join("bin").join("python")
1747 } else if cfg!(windows) {
1748 venv.join("Scripts").join("python.exe")
1749 } else {
1750 unimplemented!("Only Windows and Unix are supported")
1751 }
1752 }
1753
1754 pub fn python_command(&self) -> Command {
1755 let mut interpreter = self.interpreter();
1756
1757 if !interpreter.exists() {
1759 interpreter.clone_from(
1760 &self
1761 .python_versions
1762 .first()
1763 .expect("At least one Python version is required")
1764 .1,
1765 );
1766 }
1767
1768 let mut command = Self::new_command_with(&interpreter);
1769 command
1770 .arg("-B")
1773 .env(EnvVars::PYTHONUTF8, "1");
1775
1776 self.add_shared_env(&mut command, false);
1777
1778 command
1779 }
1780
1781 pub fn auth_login(&self) -> Command {
1783 let mut command = self.new_command();
1784 command.arg("auth").arg("login");
1785 self.add_shared_options(&mut command, false);
1786 command
1787 }
1788
1789 pub fn auth_logout(&self) -> Command {
1791 let mut command = self.new_command();
1792 command.arg("auth").arg("logout");
1793 self.add_shared_options(&mut command, false);
1794 command
1795 }
1796
1797 pub fn auth_helper(&self) -> Command {
1799 let mut command = self.new_command();
1800 command.arg("auth").arg("helper");
1801 self.add_shared_options(&mut command, false);
1802 command
1803 }
1804
1805 pub fn auth_token(&self) -> Command {
1807 let mut command = self.new_command();
1808 command.arg("auth").arg("token");
1809 self.add_shared_options(&mut command, false);
1810 command
1811 }
1812
1813 #[must_use]
1817 pub fn with_real_home(mut self) -> Self {
1818 if let Some(home) = env::var_os(EnvVars::HOME) {
1819 self.extra_env
1820 .push((EnvVars::HOME.to_string().into(), home));
1821 }
1822 self.extra_env.push((
1825 EnvVars::XDG_CONFIG_HOME.into(),
1826 self.user_config_dir.as_os_str().into(),
1827 ));
1828 self
1829 }
1830
1831 pub fn assert_command(&self, command: &str) -> Assert {
1833 self.python_command()
1834 .arg("-c")
1835 .arg(command)
1836 .current_dir(&self.temp_dir)
1837 .assert()
1838 }
1839
1840 pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
1842 self.python_command()
1843 .arg(file.as_ref())
1844 .current_dir(&self.temp_dir)
1845 .assert()
1846 }
1847
1848 pub fn assert_installed(&self, package: &'static str, version: &'static str) {
1850 self.assert_command(
1851 format!("import {package} as package; print(package.__version__, end='')").as_str(),
1852 )
1853 .success()
1854 .stdout(version);
1855 }
1856
1857 pub fn assert_not_installed(&self, package: &'static str) {
1859 self.assert_command(format!("import {package}").as_str())
1860 .failure();
1861 }
1862
1863 pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
1865 let mut patterns = Vec::new();
1866
1867 if path.as_ref().exists() {
1869 patterns.push(Self::path_pattern(
1870 path.as_ref()
1871 .canonicalize()
1872 .expect("Failed to create canonical path"),
1873 ));
1874 }
1875
1876 patterns.push(Self::path_pattern(path));
1878
1879 patterns
1880 }
1881
1882 fn path_pattern(path: impl AsRef<Path>) -> String {
1884 format!(
1885 r"{}\\?/?",
1887 regex::escape(&path.as_ref().simplified_display().to_string())
1888 .replace(r"\\", r"(\\|\/)")
1891 )
1892 }
1893
1894 pub fn python_path(&self) -> OsString {
1895 if cfg!(unix) {
1896 env::join_paths(
1898 self.python_versions
1899 .iter()
1900 .map(|(version, _)| self.python_dir.join(version.to_string())),
1901 )
1902 .unwrap()
1903 } else {
1904 env::join_paths(
1906 self.python_versions
1907 .iter()
1908 .map(|(_, executable)| executable.parent().unwrap().to_path_buf()),
1909 )
1910 .unwrap()
1911 }
1912 }
1913
1914 pub fn filters(&self) -> Vec<(&str, &str)> {
1916 self.filters
1919 .iter()
1920 .map(|(p, r)| (p.as_str(), r.as_str()))
1921 .chain(INSTA_FILTERS.iter().copied())
1922 .collect()
1923 }
1924
1925 #[cfg(windows)]
1927 pub fn filters_without_standard_filters(&self) -> Vec<(&str, &str)> {
1928 self.filters
1929 .iter()
1930 .map(|(p, r)| (p.as_str(), r.as_str()))
1931 .collect()
1932 }
1933
1934 pub fn python_kind(&self) -> &'static str {
1936 "python"
1937 }
1938
1939 pub fn site_packages(&self) -> PathBuf {
1941 site_packages_path(
1942 &self.venv,
1943 &format!(
1944 "{}{}",
1945 self.python_kind(),
1946 self.python_version.as_ref().expect(
1947 "A Python version must be provided to retrieve the test site packages path"
1948 )
1949 ),
1950 )
1951 }
1952
1953 pub fn reset_venv(&self) {
1955 self.create_venv();
1956 }
1957
1958 fn create_venv(&self) {
1960 let executable = get_python(
1961 self.python_version
1962 .as_ref()
1963 .expect("A Python version must be provided to create a test virtual environment"),
1964 );
1965 create_venv_from_executable(&self.venv, &self.cache_dir, &executable, &self.uv_bin);
1966 }
1967
1968 pub fn copy_ecosystem_project(&self, name: &str) {
1979 let project_dir = PathBuf::from(format!("../../test/ecosystem/{name}"));
1980 self.temp_dir.copy_from(project_dir, &["**/*"]).unwrap();
1981 if let Err(err) = fs_err::remove_file(self.temp_dir.join("uv.lock")) {
1983 assert_eq!(
1984 err.kind(),
1985 io::ErrorKind::NotFound,
1986 "Failed to remove uv.lock: {err}"
1987 );
1988 }
1989 }
1990
1991 pub fn diff_lock(&self, change: impl Fn(&Self) -> Command) -> String {
2000 let lock_path = ChildPath::new(self.temp_dir.join("uv.lock"));
2001 let old_lock = fs_err::read_to_string(&lock_path).unwrap();
2002 let (snapshot, output) = run_and_format(
2003 change(self),
2004 self.filters(),
2005 "diff_lock",
2006 Some(WindowsFilters::Platform),
2007 None,
2008 );
2009 assert!(output.status.success(), "{snapshot}");
2010 let new_lock = fs_err::read_to_string(&lock_path).unwrap();
2011 diff_snapshot(&old_lock, &new_lock, 10)
2012 }
2013
2014 pub fn read(&self, file: impl AsRef<Path>) -> String {
2016 fs_err::read_to_string(self.temp_dir.join(&file))
2017 .unwrap_or_else(|_| panic!("Missing file: `{}`", file.user_display()))
2018 }
2019
2020 fn new_command(&self) -> Command {
2023 Self::new_command_with(&self.uv_bin)
2024 }
2025
2026 fn new_command_with(bin: &Path) -> Command {
2032 let mut command = Command::new(bin);
2033
2034 let passthrough = [
2035 EnvVars::PATH,
2037 EnvVars::RUST_LOG,
2039 EnvVars::RUST_BACKTRACE,
2040 EnvVars::SYSTEMDRIVE,
2042 EnvVars::RUST_MIN_STACK,
2044 EnvVars::UV_STACK_SIZE,
2045 EnvVars::ALL_PROXY,
2047 EnvVars::HTTPS_PROXY,
2048 EnvVars::HTTP_PROXY,
2049 EnvVars::NO_PROXY,
2050 EnvVars::SSL_CERT_DIR,
2051 EnvVars::SSL_CERT_FILE,
2052 EnvVars::UV_NATIVE_TLS,
2053 EnvVars::UV_SYSTEM_CERTS,
2054 ];
2055
2056 for env_var in EnvVars::all_names()
2057 .iter()
2058 .filter(|name| !passthrough.contains(name))
2059 {
2060 command.env_remove(env_var);
2061 }
2062
2063 command
2064 }
2065}
2066
2067pub fn diff_snapshot(old: &str, new: &str, context_radius: usize) -> String {
2070 let diff = similar::TextDiff::from_lines(old, new);
2071 let unified = diff
2072 .unified_diff()
2073 .context_radius(context_radius)
2074 .header("old", "new")
2075 .to_string();
2076 regex!(r"(?m)^\s+$").replace_all(&unified, "").into_owned()
2080}
2081
2082#[macro_export]
2086macro_rules! diff_uv_snapshot {
2087 ($filters:expr, $old:expr, $spawnable:expr, @$snapshot:literal) => {{
2088 let new = $crate::capture_uv_snapshot!($filters, $spawnable);
2089 let snapshot = $crate::diff_snapshot($old, &new, 3);
2090 let mut settings = ::insta::Settings::clone_current();
2091 let description = match settings.description() {
2093 Some(description) => format!("{description}\n\nUnfiltered diff:\n{snapshot}"),
2094 None => format!("Unfiltered diff:\n{snapshot}"),
2095 };
2096 settings.set_description(description);
2097 settings.add_filter(r"^--- old\n\+\+\+ new\n", "");
2098 settings.add_filter(r"(?m)^@@.*$", "...");
2099 settings.add_filter(r"\n$", "\n...\n");
2100 settings.bind(|| {
2101 ::insta::assert_snapshot!(snapshot, @$snapshot);
2102 });
2103 new
2104 }};
2105}
2106
2107#[macro_export]
2109macro_rules! capture_uv_snapshot {
2110 ($filters:expr, $spawnable:expr) => {{
2111 let (snapshot, _) = $crate::run_and_format_silent(
2113 $spawnable,
2114 &$filters,
2115 $crate::function_name!(),
2116 Some($crate::WindowsFilters::Platform),
2117 None,
2118 );
2119 snapshot
2120 }};
2121 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2122 let (snapshot, _) = $crate::run_and_format(
2123 $spawnable,
2124 &$filters,
2125 $crate::function_name!(),
2126 Some($crate::WindowsFilters::Platform),
2127 None,
2128 );
2129 ::insta::assert_snapshot!(snapshot, @$snapshot);
2130 snapshot
2131 }};
2132}
2133
2134pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
2135 if cfg!(unix) {
2136 venv.join("lib").join(python).join("site-packages")
2137 } else if cfg!(windows) {
2138 venv.join("Lib").join("site-packages")
2139 } else {
2140 unimplemented!("Only Windows and Unix are supported")
2141 }
2142}
2143
2144pub fn venv_bin_path(venv: impl AsRef<Path>) -> PathBuf {
2145 if cfg!(unix) {
2146 venv.as_ref().join("bin")
2147 } else if cfg!(windows) {
2148 venv.as_ref().join("Scripts")
2149 } else {
2150 unimplemented!("Only Windows and Unix are supported")
2151 }
2152}
2153
2154fn get_python(version: &PythonVersion) -> PathBuf {
2156 ManagedPythonInstallations::from_settings(None)
2157 .map(|installed_pythons| {
2158 installed_pythons
2159 .find_version(version)
2160 .expect("Tests are run on a supported platform")
2161 .next()
2162 .as_ref()
2163 .map(|python| python.executable(false))
2164 })
2165 .unwrap_or_default()
2168 .unwrap_or(PathBuf::from(version.to_string()))
2169}
2170
2171fn create_venv_from_executable<P: AsRef<Path>>(
2173 path: P,
2174 cache_dir: &ChildPath,
2175 python: &Path,
2176 uv_bin: &Path,
2177) {
2178 TestContext::new_command_with(uv_bin)
2179 .arg("venv")
2180 .arg(path.as_ref().as_os_str())
2181 .arg("--clear")
2182 .arg("--cache-dir")
2183 .arg(cache_dir.path())
2184 .arg("--python")
2185 .arg(python)
2186 .current_dir(path.as_ref().parent().unwrap())
2187 .assert()
2188 .success();
2189 ChildPath::new(path.as_ref()).assert(predicate::path::is_dir());
2190}
2191
2192pub fn python_path_with_versions(
2196 temp_dir: &ChildPath,
2197 python_versions: &[&str],
2198) -> anyhow::Result<OsString> {
2199 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
2200 Ok(env::join_paths(
2201 python_installations_for_versions(temp_dir, python_versions, &download_list)?
2202 .into_iter()
2203 .map(|path| path.parent().unwrap().to_path_buf()),
2204 )?)
2205}
2206
2207fn python_installations_for_versions(
2211 temp_dir: &ChildPath,
2212 python_versions: &[&str],
2213 download_list: &ManagedPythonDownloadList,
2214) -> anyhow::Result<Vec<PathBuf>> {
2215 let cache = Cache::from_path(temp_dir.child("cache").to_path_buf())
2216 .init_no_wait()?
2217 .expect("No cache contention when setting up Python in tests");
2218 let _preview = uv_preview::test::with_features(&[]);
2219 let selected_pythons = python_versions
2220 .iter()
2221 .map(|python_version| {
2222 if let Ok(python) = PythonInstallation::find(
2223 &PythonRequest::parse(python_version),
2224 EnvironmentPreference::OnlySystem,
2225 PythonPreference::Managed,
2226 download_list,
2227 &cache,
2228 ) {
2229 python.into_interpreter().sys_executable().to_owned()
2230 } else {
2231 panic!("Could not find Python {python_version} for test\nTry `cargo run python install` first, or refer to CONTRIBUTING.md");
2232 }
2233 })
2234 .collect::<Vec<_>>();
2235
2236 assert!(
2237 python_versions.is_empty() || !selected_pythons.is_empty(),
2238 "Failed to fulfill requested test Python versions: {selected_pythons:?}"
2239 );
2240
2241 Ok(selected_pythons)
2242}
2243
2244#[derive(Debug, Copy, Clone)]
2245pub enum WindowsFilters {
2246 Platform,
2247 Universal,
2248}
2249
2250pub fn apply_filters<T: AsRef<str>>(mut snapshot: String, filters: impl AsRef<[(T, T)]>) -> String {
2252 for (matcher, replacement) in filters.as_ref() {
2253 let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?");
2255 if re.is_match(&snapshot) {
2256 snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string();
2257 }
2258 }
2259 snapshot
2260}
2261
2262#[expect(clippy::print_stderr)]
2266pub fn run_and_format<T: AsRef<str>>(
2267 command: impl BorrowMut<Command>,
2268 filters: impl AsRef<[(T, T)]>,
2269 function_name: &str,
2270 windows_filters: Option<WindowsFilters>,
2271 input: Option<&str>,
2272) -> (String, Output) {
2273 let (snapshot, output) =
2274 run_and_format_silent(command, filters, function_name, windows_filters, input);
2275 eprintln!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Unfiltered output ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2276 eprintln!(
2277 "----- stdout -----\n{}\n----- stderr -----\n{}",
2278 String::from_utf8_lossy(&output.stdout),
2279 String::from_utf8_lossy(&output.stderr),
2280 );
2281 eprintln!("────────────────────────────────────────────────────────────────────────────────\n");
2282 (snapshot, output)
2283}
2284
2285#[doc(hidden)]
2287pub fn run_and_format_silent<T: AsRef<str>>(
2288 mut command: impl BorrowMut<Command>,
2289 filters: impl AsRef<[(T, T)]>,
2290 function_name: &str,
2291 windows_filters: Option<WindowsFilters>,
2292 input: Option<&str>,
2293) -> (String, Output) {
2294 assert_effective_cache_directory(command.borrow_mut());
2295
2296 let program = command
2297 .borrow_mut()
2298 .get_program()
2299 .to_string_lossy()
2300 .to_string();
2301
2302 if let Ok(root) = env::var(EnvVars::TRACING_DURATIONS_TEST_ROOT) {
2304 #[expect(clippy::assertions_on_constants)]
2306 {
2307 assert!(
2308 cfg!(feature = "tracing-durations-export"),
2309 "You need to enable the tracing-durations-export feature to use `TRACING_DURATIONS_TEST_ROOT`"
2310 );
2311 }
2312 command.borrow_mut().env(
2313 EnvVars::TRACING_DURATIONS_FILE,
2314 Path::new(&root).join(function_name).with_extension("jsonl"),
2315 );
2316 }
2317
2318 let output = if let Some(input) = input {
2319 let mut child = command
2320 .borrow_mut()
2321 .stdin(Stdio::piped())
2322 .stdout(Stdio::piped())
2323 .stderr(Stdio::piped())
2324 .spawn()
2325 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"));
2326 child
2327 .stdin
2328 .as_mut()
2329 .expect("Failed to open stdin")
2330 .write_all(input.as_bytes())
2331 .expect("Failed to write to stdin");
2332
2333 child
2334 .wait_with_output()
2335 .unwrap_or_else(|err| panic!("Failed to read output from {program}: {err}"))
2336 } else {
2337 command
2338 .borrow_mut()
2339 .output()
2340 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"))
2341 };
2342
2343 let mut snapshot = format!(
2344 "exit_code: {} ({})\n",
2345 output.status.code().unwrap_or(!0),
2346 if output.status.success() {
2347 "success"
2348 } else {
2349 "failure"
2350 },
2351 );
2352 if !output.stdout.is_empty() {
2353 snapshot.push_str("----- stdout -----\n");
2354 snapshot.push_str(&String::from_utf8_lossy(&output.stdout));
2355 }
2356 if !output.stderr.is_empty() {
2357 if !output.stdout.is_empty() {
2358 snapshot.push('\n');
2359 }
2360 snapshot.push_str("----- stderr -----\n");
2361 snapshot.push_str(&String::from_utf8_lossy(&output.stderr));
2362 }
2363 let mut snapshot = apply_filters(snapshot, filters);
2364
2365 if cfg!(windows) {
2370 if let Some(windows_filters) = windows_filters {
2371 let windows_only_deps = [
2373 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2374 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2375 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2376 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2377 ];
2378 let mut removed_packages = 0;
2379 for windows_only_dep in windows_only_deps {
2380 let re = Regex::new(windows_only_dep).unwrap();
2382 if re.is_match(&snapshot) {
2383 snapshot = re.replace(&snapshot, "").to_string();
2384 removed_packages += 1;
2385 }
2386 }
2387 if removed_packages > 0 {
2388 for i in 1..20 {
2389 for verb in match windows_filters {
2390 WindowsFilters::Platform => [
2391 "Resolved",
2392 "Prepared",
2393 "Installed",
2394 "Checked",
2395 "Uninstalled",
2396 ]
2397 .iter(),
2398 WindowsFilters::Universal => {
2399 ["Prepared", "Installed", "Checked", "Uninstalled"].iter()
2400 }
2401 } {
2402 snapshot = snapshot.replace(
2403 &format!("{verb} {} packages", i + removed_packages),
2404 &format!("{verb} {} package{}", i, if i > 1 { "s" } else { "" }),
2405 );
2406 }
2407 }
2408 }
2409 }
2410 }
2411
2412 (snapshot, output)
2413}
2414
2415fn assert_effective_cache_directory(command: &Command) {
2421 let cache_directory_override = command
2422 .get_envs()
2423 .find(|(name, value)| *name == EnvVars::UV_CACHE_DIR && value.is_some());
2424
2425 if cache_directory_override.is_none() {
2426 return;
2427 }
2428
2429 let explicit_cache_directory = command.get_args().any(|argument| {
2430 argument == "--cache-dir"
2431 || argument
2432 .to_str()
2433 .is_some_and(|argument| argument.starts_with("--cache-dir="))
2434 });
2435
2436 assert!(
2437 !explicit_cache_directory,
2438 "`UV_CACHE_DIR` is ignored because this command already supplies `--cache-dir`; configure `TestContext::cache_dir` instead"
2439 );
2440}
2441
2442pub fn copy_dir_ignore(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {
2444 for entry in ignore::Walk::new(&src) {
2445 let entry = entry?;
2446 let relative = entry.path().strip_prefix(&src)?;
2447 let ty = entry.file_type().unwrap();
2448 if ty.is_dir() {
2449 fs_err::create_dir(dst.as_ref().join(relative))?;
2450 } else {
2451 fs_err::copy(entry.path(), dst.as_ref().join(relative))?;
2452 }
2453 }
2454 Ok(())
2455}
2456
2457pub fn make_project(dir: &Path, name: &str, body: &str) -> anyhow::Result<()> {
2459 let pyproject_toml = formatdoc! {r#"
2460 [project]
2461 name = "{name}"
2462 version = "0.1.0"
2463 requires-python = ">=3.11,<3.13"
2464 {body}
2465
2466 [build-system]
2467 requires = ["uv_build>=0.9.0,<10000"]
2468 build-backend = "uv_build"
2469 "#
2470 };
2471 fs_err::create_dir_all(dir)?;
2472 fs_err::write(dir.join("pyproject.toml"), pyproject_toml)?;
2473 fs_err::create_dir_all(dir.join("src").join(name))?;
2474 fs_err::write(dir.join("src").join(name).join("__init__.py"), "")?;
2475 Ok(())
2476}
2477
2478pub const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
2480 "Z2l0aHViCg==",
2481 "cGF0Cg==",
2482 "MTFBQlVDUjZBMERMUTQ3aVphN3hPdV9qQmhTMkZUeHZ4ZE13OHczakxuZndsV2ZlZjc2cE53eHBWS2tiRUFwdnpmUk8zV0dDSUhicDFsT01aago=",
2483];
2484
2485#[cfg(not(windows))]
2487pub const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[
2488 "Z2l0aHViCg==",
2489 "cGF0Cg==",
2490 "MTFBQlVDUjZBMDJTOFYwMTM4YmQ0bV9uTXpueWhxZDBrcllROTQ5SERTeTI0dENKZ2lmdzIybDFSR2s1SE04QW8xTUVYQ1I0Q1YxYUdPRGpvZQo=",
2491];
2492
2493pub const READ_ONLY_GITHUB_SSH_DEPLOY_KEY: &str = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNBeTF1SnNZK1JXcWp1NkdIY3Z6a3AwS21yWDEwdmo3RUZqTkpNTkRqSGZPZ0FBQUpqWUpwVnAyQ2FWCmFRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQXkxdUpzWStSV3FqdTZHSGN2emtwMEttclgxMHZqN0VGak5KTU5EakhmT2cKQUFBRUMwbzBnd1BxbGl6TFBJOEFXWDVaS2dVZHJyQ2ptMDhIQm9FenB4VDg3MXBqTFc0bXhqNUZhcU83b1lkeS9PU25RcQphdGZYUytQc1FXTTBrdzBPTWQ4NkFBQUFFR3R2Ym5OMGFVQmhjM1J5WVd3dWMyZ0JBZ01FQlE9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K";
2494
2495pub fn decode_token(content: &[&str]) -> String {
2498 content
2499 .iter()
2500 .map(|part| base64.decode(part).unwrap())
2501 .map(|decoded| {
2502 std::str::from_utf8(decoded.as_slice())
2503 .unwrap()
2504 .trim_end()
2505 .to_string()
2506 })
2507 .join("_")
2508}
2509
2510#[tokio::main(flavor = "current_thread")]
2513pub async fn download_to_disk(url: &str, path: &Path) {
2514 let trusted_hosts: Vec<_> = env::var(EnvVars::UV_INSECURE_HOST)
2515 .unwrap_or_default()
2516 .split(' ')
2517 .map(|h| uv_configuration::TrustedHost::from_str(h).unwrap())
2518 .collect();
2519
2520 let client = uv_client::BaseClientBuilder::default()
2521 .allow_insecure_host(trusted_hosts)
2522 .build()
2523 .expect("failed to build base client");
2524 let url = url.parse().unwrap();
2525 let response = client
2526 .for_host(&url)
2527 .get(reqwest::Url::from(url))
2528 .send()
2529 .await
2530 .unwrap();
2531
2532 let mut file = fs_err::tokio::File::create(path).await.unwrap();
2533 let mut stream = response.bytes_stream();
2534 while let Some(chunk) = stream.next().await {
2535 file.write_all(&chunk.unwrap()).await.unwrap();
2536 }
2537 file.sync_all().await.unwrap();
2538}
2539
2540#[cfg(unix)]
2545pub struct ReadOnlyDirectoryGuard {
2546 path: PathBuf,
2547 original_mode: u32,
2548}
2549
2550#[cfg(unix)]
2551impl ReadOnlyDirectoryGuard {
2552 pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
2555 use std::os::unix::fs::PermissionsExt;
2556 let path = path.into();
2557 let metadata = fs_err::metadata(&path)?;
2558 let original_mode = metadata.permissions().mode();
2559 let readonly_mode = original_mode & !0o222;
2561 fs_err::set_permissions(&path, std::fs::Permissions::from_mode(readonly_mode))?;
2562 Ok(Self {
2563 path,
2564 original_mode,
2565 })
2566 }
2567}
2568
2569#[cfg(unix)]
2570impl Drop for ReadOnlyDirectoryGuard {
2571 fn drop(&mut self) {
2572 use std::os::unix::fs::PermissionsExt;
2573 let _ = fs_err::set_permissions(
2574 &self.path,
2575 std::fs::Permissions::from_mode(self.original_mode),
2576 );
2577 }
2578}
2579
2580#[doc(hidden)]
2584#[macro_export]
2585macro_rules! function_name {
2586 () => {{
2587 fn f() {}
2588 fn type_name_of_val<T>(_: T) -> &'static str {
2589 std::any::type_name::<T>()
2590 }
2591 let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
2592 while let Some(rest) = name.strip_suffix("::{{closure}}") {
2593 name = rest;
2594 }
2595 name
2596 }};
2597}
2598
2599#[macro_export]
2604macro_rules! uv_snapshot {
2605 ($spawnable:expr, @$snapshot:literal) => {{
2606 uv_snapshot!($crate::INSTA_FILTERS.to_vec(), $spawnable, @$snapshot)
2607 }};
2608 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2609 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), None);
2611 ::insta::assert_snapshot!(snapshot, @$snapshot);
2612 output
2613 }};
2614 ($filters:expr, $spawnable:expr, input=$input:expr, @$snapshot:literal) => {{
2615 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), Some($input));
2617 ::insta::assert_snapshot!(snapshot, @$snapshot);
2618 output
2619 }};
2620 ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{
2621 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), None, None);
2623 ::insta::assert_snapshot!(snapshot, @$snapshot);
2624 output
2625 }};
2626 ($filters:expr, universal_windows_filters=true, $spawnable:expr, @$snapshot:literal) => {{
2627 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Universal), None);
2629 ::insta::assert_snapshot!(snapshot, @$snapshot);
2630 output
2631 }};
2632}
2633
2634#[cfg(test)]
2635mod cache_directory_tests {
2636 use std::process::Command;
2637
2638 use uv_static::EnvVars;
2639
2640 use super::assert_effective_cache_directory;
2641
2642 #[test]
2643 #[should_panic(expected = "`UV_CACHE_DIR` is ignored")]
2644 fn rejects_environment_override_with_explicit_cache_argument() {
2645 let mut command = Command::new("uv");
2646 command
2647 .arg("--cache-dir")
2648 .arg("context-cache")
2649 .env(EnvVars::UV_CACHE_DIR, "ignored-cache");
2650
2651 assert_effective_cache_directory(&command);
2652 }
2653
2654 #[test]
2655 #[should_panic(expected = "`UV_CACHE_DIR` is ignored")]
2656 fn rejects_environment_override_with_inline_cache_argument() {
2657 let mut command = Command::new("uv");
2658 command
2659 .arg("--cache-dir=context-cache")
2660 .env(EnvVars::UV_CACHE_DIR, "ignored-cache");
2661
2662 assert_effective_cache_directory(&command);
2663 }
2664
2665 #[test]
2666 fn allows_environment_override_without_explicit_cache_argument() {
2667 let mut command = Command::new("uv");
2668 command
2669 .arg("cache")
2670 .arg("dir")
2671 .env(EnvVars::UV_CACHE_DIR, "effective-cache");
2672
2673 assert_effective_cache_directory(&command);
2674 }
2675
2676 #[test]
2677 fn allows_removed_environment_override_with_explicit_cache_argument() {
2678 let mut command = Command::new("uv");
2679 command
2680 .arg("--cache-dir")
2681 .arg("context-cache")
2682 .env_remove(EnvVars::UV_CACHE_DIR);
2683
2684 assert_effective_cache_directory(&command);
2685 }
2686}