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]
62macro_rules! test_context {
63 ($python_version:expr) => {
64 $crate::TestContext::new_with_bin($python_version, $crate::get_bin!())
65 };
66}
67
68#[macro_export]
74macro_rules! test_context_with_versions {
75 ($python_versions:expr) => {
76 $crate::TestContext::new_with_versions_and_bin($python_versions, $crate::get_bin!())
77 };
78}
79
80#[macro_export]
87macro_rules! get_bin {
88 () => {
89 std::path::PathBuf::from(
90 std::env::var_os("NEXTEST_BIN_EXE_uv")
91 .or_else(|| std::env::var_os("CARGO_BIN_EXE_uv"))
92 .expect("Cargo or nextest should provide the uv binary path"),
93 )
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 pub fn cache_files(&self, bucket: CacheBucket) -> anyhow::Result<Vec<PathBuf>> {
201 let cache = Cache::from_path(self.cache_dir.path());
202 let mut files = Vec::new();
203 for entry in WalkDir::new(cache.bucket(bucket)).min_depth(1) {
204 let entry = entry?;
205 if entry.file_type().is_file() {
206 files.push(entry.path().to_path_buf());
207 }
208 }
209 files.sort();
210 Ok(files)
211 }
212
213 #[must_use]
215 pub fn with_env(mut self, key: impl Into<OsString>, value: impl Into<OsString>) -> Self {
216 self.extra_env.push((key.into(), value.into()));
217 self
218 }
219
220 #[must_use]
222 pub fn with_exclude_newer(mut self, exclude_newer: &str) -> Self {
223 self.extra_env
224 .push((EnvVars::UV_EXCLUDE_NEWER.into(), exclude_newer.into()));
225 self
226 }
227
228 #[must_use]
230 pub fn with_http_timeout(mut self, http_timeout: &str) -> Self {
231 self.extra_env
232 .push((EnvVars::UV_HTTP_TIMEOUT.into(), http_timeout.into()));
233 self
234 }
235
236 #[must_use]
238 pub fn with_http_retries(mut self, http_retries: &str) -> Self {
239 self.extra_env
240 .push((EnvVars::UV_HTTP_RETRIES.into(), http_retries.into()));
241 self
242 }
243
244 #[must_use]
246 pub fn with_fast_http_retry(self) -> Self {
247 self.with_http_timeout("1").with_http_retries("1")
248 }
249
250 #[must_use]
252 pub fn with_concurrent_installs(mut self, concurrent_installs: &str) -> Self {
253 self.extra_env.push((
254 EnvVars::UV_CONCURRENT_INSTALLS.into(),
255 concurrent_installs.into(),
256 ));
257 self
258 }
259
260 #[must_use]
265 pub fn with_filtered_counts(mut self) -> Self {
266 for verb in &[
267 "Resolved",
268 "Prepared",
269 "Installed",
270 "Uninstalled",
271 "Checked",
272 ] {
273 self.filters.push((
274 format!("{verb} \\d+ packages?"),
275 format!("{verb} [N] packages"),
276 ));
277 }
278 self.with_filtered_file_counts()
279 }
280
281 #[must_use]
283 pub fn with_filtered_file_counts(mut self) -> Self {
284 self.filters.push((
285 "Removed \\d+ files?".to_string(),
286 "Removed [N] files".to_string(),
287 ));
288 self
289 }
290
291 #[must_use]
293 pub fn with_filtered_sizes(mut self) -> Self {
294 self.filters.push((
295 r"(\s|\()(\d+\.)?\d+(([KMGT]i)?B)".to_string(),
296 "$1[SIZE]$3".to_string(),
297 ));
298 self
299 }
300
301 #[must_use]
303 pub fn with_filtered_sizes_and_units(mut self) -> Self {
304 self.filters.push((
305 r"(\s|\()(\d+\.)?\d+([KMGT]i)?B".to_string(),
306 "$1[SIZE]".to_string(),
307 ));
308 self
309 }
310
311 #[must_use]
313 pub fn with_filtered_cache_size(mut self) -> Self {
314 self.filters
316 .push((r"(?m)^\d+\n".to_string(), "[SIZE]\n".to_string()));
317 self.filters.push((
319 r"(?m)^\d+(\.\d+)?( ?[KMGT]i?B)\n".to_string(),
320 "[SIZE]$2\n".to_string(),
321 ));
322 self
323 }
324
325 #[must_use]
327 pub fn with_filtered_centralized_environment_hashes(mut self) -> Self {
328 self.filters.push((
329 r"`([\w.\[\]-]+)-[a-f0-9]{16}`".to_string(),
330 "`$1-[HASH]`".to_string(),
331 ));
332 self
333 }
334
335 #[must_use]
337 pub fn with_filtered_missing_file_error(mut self) -> Self {
338 self.filters.push((
341 r"[^:\n]* \(os error 2\)".to_string(),
342 " [OS ERROR 2]".to_string(),
343 ));
344 self.filters.push((
348 r"[^:\n]* \(os error 3\)".to_string(),
349 " [OS ERROR 2]".to_string(),
350 ));
351 self
352 }
353
354 #[must_use]
357 pub fn with_filtered_exe_suffix(mut self) -> Self {
358 self.filters
359 .push((regex::escape(env::consts::EXE_SUFFIX), String::new()));
360 self
361 }
362
363 #[must_use]
365 pub fn with_filtered_python_sources(mut self) -> Self {
366 self.filters.push((
367 "virtual environments, managed installations, or search path".to_string(),
368 "[PYTHON SOURCES]".to_string(),
369 ));
370 self.filters.push((
371 "virtual environments, managed installations, search path, or registry".to_string(),
372 "[PYTHON SOURCES]".to_string(),
373 ));
374 self.filters.push((
375 "virtual environments, search path, or registry".to_string(),
376 "[PYTHON SOURCES]".to_string(),
377 ));
378 self.filters.push((
379 "virtual environments, registry, or search path".to_string(),
380 "[PYTHON SOURCES]".to_string(),
381 ));
382 self.filters.push((
383 "virtual environments or search path".to_string(),
384 "[PYTHON SOURCES]".to_string(),
385 ));
386 self.filters.push((
387 "managed installations or search path".to_string(),
388 "[PYTHON SOURCES]".to_string(),
389 ));
390 self.filters.push((
391 "managed installations, search path, or registry".to_string(),
392 "[PYTHON SOURCES]".to_string(),
393 ));
394 self.filters.push((
395 "search path or registry".to_string(),
396 "[PYTHON SOURCES]".to_string(),
397 ));
398 self.filters.push((
399 "registry or search path".to_string(),
400 "[PYTHON SOURCES]".to_string(),
401 ));
402 self.filters
403 .push(("search path".to_string(), "[PYTHON SOURCES]".to_string()));
404 self
405 }
406
407 #[must_use]
410 pub fn with_filtered_python_names(mut self) -> Self {
411 for name in ["python", "pypy"] {
412 let suffix = if cfg!(windows) {
415 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
419 format!(r"(\d\.\d+|\d)?{exe_suffix}")
420 } else {
421 if name == "python" {
423 r"(\d\.\d+|\d)?(t|d|td)?".to_string()
425 } else {
426 r"(\d\.\d+|\d)(t|d|td)?".to_string()
428 }
429 };
430
431 self.filters.push((
432 format!(r"[\\/]{name}{suffix}"),
435 format!("/[{}]", name.to_uppercase()),
436 ));
437 }
438
439 self
440 }
441
442 #[must_use]
445 pub fn with_filtered_virtualenv_bin(mut self) -> Self {
446 self.filters.push((
447 format!(
448 r"[\\/]{}[\\/]",
449 venv_bin_path(PathBuf::new()).to_string_lossy()
450 ),
451 "/[BIN]/".to_string(),
452 ));
453 self.filters.push((
454 format!(
455 r"[\\/]{}\b",
456 venv_bin_path(PathBuf::new()).to_string_lossy()
457 ),
458 "/[BIN]".to_string(),
459 ));
460 self
461 }
462
463 #[must_use]
467 pub fn with_filtered_python_install_bin(mut self) -> Self {
468 let suffix = if cfg!(windows) {
471 let exe_suffix = regex::escape(env::consts::EXE_SUFFIX);
472 format!(r"(\d\.\d+|\d)?{exe_suffix}")
474 } else {
475 r"\d\.\d+|\d".to_string()
477 };
478
479 if cfg!(unix) {
480 self.filters.push((
481 format!(r"[\\/]bin/python({suffix})"),
482 "/[INSTALL-BIN]/python$1".to_string(),
483 ));
484 self.filters.push((
485 format!(r"[\\/]bin/pypy({suffix})"),
486 "/[INSTALL-BIN]/pypy$1".to_string(),
487 ));
488 } else {
489 self.filters.push((
490 format!(r"[\\/]python({suffix})"),
491 "/[INSTALL-BIN]/python$1".to_string(),
492 ));
493 self.filters.push((
494 format!(r"[\\/]pypy({suffix})"),
495 "/[INSTALL-BIN]/pypy$1".to_string(),
496 ));
497 }
498 self
499 }
500
501 #[must_use]
506 pub fn with_pyvenv_cfg_filters(mut self) -> Self {
507 let added_filters = [
508 (r"home = .+".to_string(), "home = [PYTHON_HOME]".to_string()),
509 (
510 r"uv = \d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?".to_string(),
511 "uv = [UV_VERSION]".to_string(),
512 ),
513 ];
514 for filter in added_filters {
515 self.filters.insert(0, filter);
516 }
517 self
518 }
519
520 #[must_use]
523 pub fn with_filtered_python_symlinks(mut self) -> Self {
524 for (version, executable) in &self.python_versions {
525 if fs_err::symlink_metadata(executable).unwrap().is_symlink() {
526 self.filters.extend(
527 Self::path_patterns(executable.read_link().unwrap())
528 .into_iter()
529 .map(|pattern| (format! {" -> {pattern}"}, String::new())),
530 );
531 }
532 self.filters.push((
534 regex::escape(&format!(" -> [PYTHON-{version}]")),
535 String::new(),
536 ));
537 }
538 self
539 }
540
541 #[must_use]
543 pub fn with_filtered_path(mut self, path: &Path, name: &str) -> Self {
544 for pattern in Self::path_patterns(path)
548 .into_iter()
549 .map(|pattern| (pattern, format!("[{name}]/")))
550 {
551 self.filters.insert(0, pattern);
552 }
553 self
554 }
555
556 #[inline]
564 #[must_use]
565 pub fn with_filtered_link_mode_warning(mut self) -> Self {
566 let pattern = "warning: Failed to hardlink files; .*\n.*\n.*\n";
567 self.filters.push((pattern.to_string(), String::new()));
568 self
569 }
570
571 #[inline]
573 #[must_use]
574 pub fn with_filtered_not_executable(mut self) -> Self {
575 let pattern = if cfg!(unix) {
576 r"Permission denied \(os error 13\)"
577 } else {
578 r"\%1 is not a valid Win32 application. \(os error 193\)"
579 };
580 self.filters
581 .push((pattern.to_string(), "[PERMISSION DENIED]".to_string()));
582 self
583 }
584
585 #[must_use]
587 pub fn with_filtered_python_keys(mut self) -> Self {
588 let platform_re = r"(?x)
590 ( # We capture the group before the platform
591 (?:cpython|pypy|graalpy)# Python implementation
592 -
593 \d+\.\d+ # Major and minor version
594 (?: # The patch version is handled separately
595 \.
596 (?:
597 \[X\] # A previously filtered patch version [X]
598 | # OR
599 \[LATEST\] # A previously filtered latest patch version [LATEST]
600 | # OR
601 \d+ # An actual patch version
602 )
603 )? # (we allow the patch version to be missing entirely, e.g., in a request)
604 (?:(?:a|b|rc)[0-9]+)? # Pre-release version component, e.g., `a6` or `rc2`
605 (?:[td])? # A short variant, such as `t` (for freethreaded) or `d` (for debug)
606 (?:(\+[a-z]+)+)? # A long variant, such as `+freethreaded` or `+freethreaded+debug`
607 )
608 -
609 [a-z0-9]+ # Operating system (e.g., 'macos')
610 -
611 [a-z0-9_]+ # Architecture (e.g., 'aarch64')
612 -
613 [a-z]+ # Libc (e.g., 'none')
614";
615 self.filters
616 .push((platform_re.to_string(), "$1-[PLATFORM]".to_string()));
617 self
618 }
619
620 #[must_use]
622 pub fn with_filtered_latest_python_versions(mut self) -> Self {
623 for (minor, patch) in [
626 ("3.15", LATEST_PYTHON_3_15.strip_prefix("3.15.").unwrap()),
627 ("3.14", LATEST_PYTHON_3_14.strip_prefix("3.14.").unwrap()),
628 ("3.13", LATEST_PYTHON_3_13.strip_prefix("3.13.").unwrap()),
629 ("3.12", LATEST_PYTHON_3_12.strip_prefix("3.12.").unwrap()),
630 ("3.11", LATEST_PYTHON_3_11.strip_prefix("3.11.").unwrap()),
631 ("3.10", LATEST_PYTHON_3_10.strip_prefix("3.10.").unwrap()),
632 ] {
633 let pattern = format!(r"(\b){minor}\.{patch}(\b)");
635 let replacement = format!("${{1}}{minor}.[LATEST]${{2}}");
636 self.filters.push((pattern, replacement));
637 }
638 self
639 }
640
641 #[must_use]
643 #[cfg(windows)]
644 pub fn with_filtered_windows_temp_dir(mut self) -> Self {
645 let pattern = regex::escape(
646 &self
647 .temp_dir
648 .simplified_display()
649 .to_string()
650 .replace('/', "\\"),
651 );
652 self.filters.push((pattern, "[TEMP_DIR]".to_string()));
653 self
654 }
655
656 #[must_use]
658 pub fn with_filtered_compiled_file_count(mut self) -> Self {
659 self.filters.push((
660 r"compiled \d+ files".to_string(),
661 "compiled [COUNT] files".to_string(),
662 ));
663 self
664 }
665
666 #[must_use]
668 pub fn with_filtered_current_version(mut self) -> Self {
669 self.filters.push((
670 regex::escape(&format!("v{}", env!("CARGO_PKG_VERSION"))),
671 "v[CURRENT_VERSION]".to_string(),
672 ));
673 self
674 }
675
676 #[must_use]
678 pub fn with_cyclonedx_filters(mut self) -> Self {
679 self.filters.push((
680 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(),
681 "[SERIAL_NUMBER]".to_string(),
682 ));
683 self.filters.push((
684 r#""timestamp": "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z""#
685 .to_string(),
686 r#""timestamp": "[TIMESTAMP]""#.to_string(),
687 ));
688 self.filters.push((
689 r#""name": "uv",\s*"version": "\d+\.\d+\.\d+(-(alpha|beta|rc)\.\d+)?(\+\d+)?""#
690 .to_string(),
691 r#""name": "uv",
692 "version": "[VERSION]""#
693 .to_string(),
694 ));
695 self
696 }
697
698 #[must_use]
700 pub fn with_collapsed_whitespace(mut self) -> Self {
701 self.filters.push((r"[ \t]+".to_string(), " ".to_string()));
702 self
703 }
704
705 #[must_use]
707 pub fn with_python_download_cache(mut self) -> Self {
708 self.extra_env.push((
709 EnvVars::UV_PYTHON_CACHE_DIR.into(),
710 env::var_os(EnvVars::UV_PYTHON_CACHE_DIR).unwrap_or_else(|| {
712 uv_cache::Cache::from_settings(false, None)
713 .unwrap()
714 .bucket(CacheBucket::Python)
715 .into()
716 }),
717 ));
718 self
719 }
720
721 #[must_use]
722 pub fn with_empty_python_install_mirror(mut self) -> Self {
723 self.extra_env.push((
724 EnvVars::UV_PYTHON_INSTALL_MIRROR.into(),
725 String::new().into(),
726 ));
727 self
728 }
729
730 #[must_use]
732 pub fn with_managed_python_dirs(mut self) -> Self {
733 let managed = self.temp_dir.join("managed");
734
735 self.extra_env.push((
736 EnvVars::UV_PYTHON_BIN_DIR.into(),
737 self.bin_dir.as_os_str().to_owned(),
738 ));
739 self.extra_env
740 .push((EnvVars::UV_PYTHON_INSTALL_DIR.into(), managed.into()));
741 self.extra_env
742 .push((EnvVars::UV_PYTHON_DOWNLOADS.into(), "automatic".into()));
743
744 self
745 }
746
747 #[must_use]
749 pub fn with_tool_dirs(mut self) -> Self {
750 self.extra_env.push((
751 EnvVars::UV_TOOL_DIR.into(),
752 self.temp_dir.join("tools").into(),
753 ));
754 self.extra_env.push((
755 EnvVars::XDG_BIN_HOME.into(),
756 self.temp_dir.join("bin").into(),
757 ));
758
759 self
760 }
761
762 #[must_use]
763 pub fn with_versions_as_managed(mut self, versions: &[&str]) -> Self {
764 self.extra_env.push((
765 EnvVars::UV_INTERNAL__TEST_PYTHON_MANAGED.into(),
766 versions.iter().join(" ").into(),
767 ));
768
769 self
770 }
771
772 #[must_use]
774 pub fn with_filter(mut self, filter: (impl Into<String>, impl Into<String>)) -> Self {
775 self.filters.push((filter.0.into(), filter.1.into()));
776 self
777 }
778
779 #[must_use]
781 pub fn with_unset_git_credential_helper(self) -> Self {
782 let git_config = self.home_dir.child(".gitconfig");
783 git_config
784 .write_str(indoc! {r"
785 [credential]
786 helper =
787 "})
788 .expect("Failed to unset git credential helper");
789
790 self
791 }
792
793 #[must_use]
795 #[cfg(windows)]
796 pub fn clear_filters(mut self) -> Self {
797 self.filters.clear();
798 self
799 }
800
801 pub fn with_cache_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
806 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
807 return Ok(None);
808 };
809 self.with_cache_on_fs(&dir, "COW_FS").map(Some)
810 }
811
812 pub fn with_cache_on_alt_fs(self) -> anyhow::Result<Option<Self>> {
817 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_ALT_FS).ok() else {
818 return Ok(None);
819 };
820 self.with_cache_on_fs(&dir, "ALT_FS").map(Some)
821 }
822
823 pub fn with_cache_on_lowlinks_fs(self) -> anyhow::Result<Option<Self>> {
828 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_LOWLINKS_FS).ok() else {
829 return Ok(None);
830 };
831 self.with_cache_on_fs(&dir, "LOWLINKS_FS").map(Some)
832 }
833
834 pub fn with_cache_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
839 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
840 return Ok(None);
841 };
842 self.with_cache_on_fs(&dir, "NOCOW_FS").map(Some)
843 }
844
845 pub fn with_working_dir_on_cow_fs(self) -> anyhow::Result<Option<Self>> {
852 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_COW_FS).ok() else {
853 return Ok(None);
854 };
855 self.with_working_dir_on_fs(&dir, "COW_FS").map(Some)
856 }
857
858 pub fn with_working_dir_on_nocow_fs(self) -> anyhow::Result<Option<Self>> {
865 let Some(dir) = env::var(EnvVars::UV_INTERNAL__TEST_NOCOW_FS).ok() else {
866 return Ok(None);
867 };
868 self.with_working_dir_on_fs(&dir, "NOCOW_FS").map(Some)
869 }
870
871 fn with_cache_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
872 fs_err::create_dir_all(dir)?;
873 let tmp = tempfile::TempDir::new_in(dir)?;
874 self.cache_dir = ChildPath::new(tmp.path()).child("cache");
875 fs_err::create_dir_all(&self.cache_dir)?;
876 let replacement = format!("[{name}]/[CACHE_DIR]/");
877 for pattern in Self::path_patterns(&self.cache_dir) {
878 self.filters.insert(0, (pattern, replacement.clone()));
879 }
880 self._extra_tempdirs.push(tmp);
881 Ok(self)
882 }
883
884 fn with_working_dir_on_fs(mut self, dir: &str, name: &str) -> anyhow::Result<Self> {
885 fs_err::create_dir_all(dir)?;
886 let tmp = tempfile::TempDir::new_in(dir)?;
887 self.temp_dir = ChildPath::new(tmp.path()).child("temp");
888 fs_err::create_dir_all(&self.temp_dir)?;
889 let canonical_temp_dir = self.temp_dir.canonicalize()?;
892 self.venv = ChildPath::new(canonical_temp_dir.join(".venv"));
893 let temp_replacement = format!("[{name}]/[TEMP_DIR]/");
894 self.filters.extend(
895 Self::path_patterns(&self.temp_dir)
896 .into_iter()
897 .map(|pattern| (pattern, temp_replacement.clone())),
898 );
899 let venv_replacement = format!("[{name}]/[VENV]/");
900 self.filters.extend(
901 Self::path_patterns(&self.venv)
902 .into_iter()
903 .map(|pattern| (pattern, venv_replacement.clone())),
904 );
905 self._extra_tempdirs.push(tmp);
906 Ok(self)
907 }
908
909 pub fn test_bucket_dir() -> PathBuf {
918 std::env::temp_dir()
919 .simple_canonicalize()
920 .expect("failed to canonicalize temp dir")
921 .join("uv")
922 .join("tests")
923 }
924
925 pub fn new_with_versions_and_bin(python_versions: &[&str], uv_bin: PathBuf) -> Self {
932 let bucket = Self::test_bucket_dir();
933 fs_err::create_dir_all(&bucket).expect("Failed to create test bucket");
934
935 let root = tempfile::TempDir::new_in(bucket).expect("Failed to create test root directory");
936
937 fs_err::create_dir_all(root.path().join(".git"))
940 .expect("Failed to create `.git` placeholder in test root directory");
941
942 let temp_dir = ChildPath::new(root.path()).child("temp");
943 fs_err::create_dir_all(&temp_dir).expect("Failed to create test working directory");
944
945 let cache_dir = ChildPath::new(root.path()).child("cache");
946 fs_err::create_dir_all(&cache_dir).expect("Failed to create test cache directory");
947
948 let python_dir = ChildPath::new(root.path()).child("python");
949 fs_err::create_dir_all(&python_dir).expect("Failed to create test Python directory");
950
951 let bin_dir = ChildPath::new(root.path()).child("bin");
952 fs_err::create_dir_all(&bin_dir).expect("Failed to create test bin directory");
953
954 if cfg!(not(feature = "git")) {
956 Self::disallow_git_cli(&bin_dir).expect("Failed to setup disallowed `git` command");
957 }
958
959 let home_dir = ChildPath::new(root.path()).child("home");
960 fs_err::create_dir_all(&home_dir).expect("Failed to create test home directory");
961
962 let user_config_dir = if cfg!(windows) {
963 ChildPath::new(home_dir.path())
964 } else {
965 ChildPath::new(home_dir.path()).child(".config")
966 };
967
968 let canonical_temp_dir = temp_dir.canonicalize().unwrap();
970 let venv = ChildPath::new(canonical_temp_dir.join(".venv"));
971
972 let python_version = python_versions
973 .first()
974 .map(|version| PythonVersion::from_str(version).unwrap());
975
976 let site_packages = python_version
977 .as_ref()
978 .map(|version| site_packages_path(&venv, &format!("python{version}")));
979
980 let workspace_root = Path::new(&env::var(EnvVars::CARGO_MANIFEST_DIR).unwrap())
983 .parent()
984 .expect("CARGO_MANIFEST_DIR should be nested in workspace")
985 .parent()
986 .expect("CARGO_MANIFEST_DIR should be doubly nested in workspace")
987 .to_path_buf();
988
989 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
990
991 let python_versions: Vec<_> = python_versions
992 .iter()
993 .map(|version| PythonVersion::from_str(version).unwrap())
994 .zip(
995 python_installations_for_versions(&temp_dir, python_versions, &download_list)
996 .expect("Failed to find test Python versions"),
997 )
998 .collect();
999
1000 if cfg!(unix) {
1003 for (version, executable) in &python_versions {
1004 let parent = python_dir.child(version.to_string());
1005 parent.create_dir_all().unwrap();
1006 parent.child("python3").symlink_to_file(executable).unwrap();
1007 }
1008 }
1009
1010 let mut filters = Vec::new();
1011
1012 filters.extend(
1013 Self::path_patterns(&uv_bin)
1014 .into_iter()
1015 .map(|pattern| (pattern, "[UV]".to_string())),
1016 );
1017
1018 if cfg!(windows) {
1020 filters.push((" --link-mode <LINK_MODE>".to_string(), String::new()));
1021 filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new()));
1022 filters.push((r"exit code: ".to_string(), "exit status: ".to_string()));
1024 }
1025
1026 for (version, executable) in &python_versions {
1027 filters.extend(
1029 Self::path_patterns(executable)
1030 .into_iter()
1031 .map(|pattern| (pattern, format!("[PYTHON-{version}]"))),
1032 );
1033
1034 filters.extend(
1036 Self::path_patterns(python_dir.join(version.to_string()))
1037 .into_iter()
1038 .map(|pattern| {
1039 (
1040 format!("{pattern}[a-zA-Z0-9]*"),
1041 format!("[PYTHON-{version}]"),
1042 )
1043 }),
1044 );
1045
1046 if version.patch().is_none() {
1049 filters.push((
1050 format!(r"({})\.\d+", regex::escape(version.to_string().as_str())),
1051 "$1.[X]".to_string(),
1052 ));
1053 }
1054 }
1055
1056 filters.extend(
1057 Self::path_patterns(&bin_dir)
1058 .into_iter()
1059 .map(|pattern| (pattern, "[BIN]/".to_string())),
1060 );
1061 filters.extend(
1062 Self::path_patterns(&cache_dir)
1063 .into_iter()
1064 .map(|pattern| (pattern, "[CACHE_DIR]/".to_string())),
1065 );
1066 if let Some(ref site_packages) = site_packages {
1067 filters.extend(
1068 Self::path_patterns(site_packages)
1069 .into_iter()
1070 .map(|pattern| (pattern, "[SITE_PACKAGES]/".to_string())),
1071 );
1072 }
1073 filters.extend(
1074 Self::path_patterns(&venv)
1075 .into_iter()
1076 .map(|pattern| (pattern, "[VENV]/".to_string())),
1077 );
1078
1079 if let Some(site_packages) = site_packages {
1081 filters.push((
1082 Self::path_pattern(
1083 site_packages
1084 .strip_prefix(&canonical_temp_dir)
1085 .expect("The test site-packages directory is always in the tempdir"),
1086 ),
1087 "[SITE_PACKAGES]/".to_string(),
1088 ));
1089 }
1090
1091 filters.push((
1093 r"[\\/]lib[\\/]python\d+\.\d+[\\/]".to_string(),
1094 "/[PYTHON-LIB]/".to_string(),
1095 ));
1096 filters.push((r"[\\/]Lib[\\/]".to_string(), "/[PYTHON-LIB]/".to_string()));
1097
1098 filters.extend(
1099 Self::path_patterns(&temp_dir)
1100 .into_iter()
1101 .map(|pattern| (pattern, "[TEMP_DIR]/".to_string())),
1102 );
1103 filters.extend(
1104 Self::path_patterns(&python_dir)
1105 .into_iter()
1106 .map(|pattern| (pattern, "[PYTHON_DIR]/".to_string())),
1107 );
1108 let mut uv_user_config_dir = PathBuf::from(user_config_dir.path());
1109 uv_user_config_dir.push("uv");
1110 filters.extend(
1111 Self::path_patterns(&uv_user_config_dir)
1112 .into_iter()
1113 .map(|pattern| (pattern, "[UV_USER_CONFIG_DIR]/".to_string())),
1114 );
1115 filters.extend(
1116 Self::path_patterns(&user_config_dir)
1117 .into_iter()
1118 .map(|pattern| (pattern, "[USER_CONFIG_DIR]/".to_string())),
1119 );
1120 filters.extend(
1121 Self::path_patterns(&home_dir)
1122 .into_iter()
1123 .map(|pattern| (pattern, "[HOME]/".to_string())),
1124 );
1125 filters.extend(
1126 Self::path_patterns(&workspace_root)
1127 .into_iter()
1128 .map(|pattern| (pattern, "[WORKSPACE]/".to_string())),
1129 );
1130
1131 filters.push((
1133 r"Activate with: (.*)\\Scripts\\activate".to_string(),
1134 "Activate with: source $1/[BIN]/activate".to_string(),
1135 ));
1136 filters.push((
1137 r"Activate with: Scripts\\activate".to_string(),
1138 "Activate with: source [BIN]/activate".to_string(),
1139 ));
1140 filters.push((
1141 r"Activate with: source (.*/|)bin/activate(?:\.\w+)?".to_string(),
1142 "Activate with: source $1[BIN]/activate".to_string(),
1143 ));
1144
1145 filters.push((
1148 r#"(\\|/)\.tmp[^\\/\s"'`]*"#.to_string(),
1149 "/[TMP]".to_string(),
1150 ));
1151
1152 filters.push((r"file:///".to_string(), "file://".to_string()));
1154
1155 filters.push((r"\\\\\?\\".to_string(), String::new()));
1157
1158 filters.push((r"127\.0\.0\.1:\d*".to_string(), "[LOCALHOST]".to_string()));
1160 filters.push((
1162 format!(
1163 r#"requires = \["uv_build>={},<[0-9.]+"\]"#,
1164 uv_version::version()
1165 ),
1166 r#"requires = ["uv_build>=[CURRENT_VERSION],<[NEXT_BREAKING]"]"#.to_string(),
1167 ));
1168 filters.push((
1170 r"environments-v(\d+)[\\/]([\w.\[\]-]+)-[a-f0-9]{16}".to_string(),
1171 "environments-v$1/$2-[HASH]".to_string(),
1172 ));
1173 filters.push((
1175 r"archive-v(\d+)[\\/][A-Za-z0-9\-\_]+".to_string(),
1176 "archive-v$1/[HASH]".to_string(),
1177 ));
1178
1179 Self {
1180 root: ChildPath::new(root.path()),
1181 temp_dir,
1182 cache_dir,
1183 python_dir,
1184 home_dir,
1185 user_config_dir,
1186 bin_dir,
1187 venv,
1188 workspace_root,
1189 python_version,
1190 python_versions,
1191 uv_bin,
1192 filters,
1193 extra_env: vec![],
1194 _root: root,
1195 _extra_tempdirs: vec![],
1196 }
1197 }
1198
1199 pub fn command(&self) -> Command {
1201 let mut command = self.new_command();
1202 self.add_shared_options(&mut command, true);
1203 command
1204 }
1205
1206 pub fn external_command(&self, program: impl AsRef<Path>) -> Command {
1208 let mut command = Self::new_command_with(program.as_ref());
1209 self.add_shared_env(&mut command, false);
1210 command
1211 }
1212
1213 pub fn disallow_git_cli(bin_dir: &Path) -> std::io::Result<()> {
1214 let contents = r"#!/bin/sh
1215 echo 'error: `git` operations are not allowed — are you missing a cfg for the `git` feature?' >&2
1216 exit 127";
1217 let git = bin_dir.join(format!("git{}", env::consts::EXE_SUFFIX));
1218 fs_err::write(&git, contents)?;
1219
1220 #[cfg(unix)]
1221 {
1222 use std::os::unix::fs::PermissionsExt;
1223 let mut perms = fs_err::metadata(&git)?.permissions();
1224 perms.set_mode(0o755);
1225 fs_err::set_permissions(&git, perms)?;
1226 }
1227
1228 Ok(())
1229 }
1230
1231 #[must_use]
1236 pub fn with_git_lfs_config(mut self) -> Self {
1237 let git_lfs_config = self.root.child(".gitconfig");
1238 git_lfs_config
1239 .write_str(indoc! {r#"
1240 [filter "lfs"]
1241 clean = git-lfs clean -- %f
1242 smudge = git-lfs smudge -- %f
1243 process = git-lfs filter-process
1244 required = true
1245 "#})
1246 .expect("Failed to setup `git-lfs` filters");
1247
1248 self.extra_env.push((
1251 EnvVars::GIT_CONFIG_GLOBAL.into(),
1252 git_lfs_config.as_os_str().into(),
1253 ));
1254 self
1255 }
1256
1257 pub fn add_shared_options(&self, command: &mut Command, activate_venv: bool) {
1269 self.add_shared_args(command);
1270 self.add_shared_env(command, activate_venv);
1271 }
1272
1273 fn add_shared_args(&self, command: &mut Command) {
1275 command.arg("--cache-dir").arg(self.cache_dir.path());
1276 }
1277
1278 pub fn add_shared_env(&self, command: &mut Command, activate_venv: bool) {
1280 let path = env::join_paths(std::iter::once(self.bin_dir.to_path_buf()).chain(
1282 env::split_paths(&env::var(EnvVars::PATH).unwrap_or_default()),
1283 ))
1284 .unwrap();
1285
1286 if cfg!(not(windows)) {
1289 command.env(EnvVars::SHELL, "bash");
1290 }
1291
1292 command
1293 .env_remove(EnvVars::VIRTUAL_ENV)
1295 .env(EnvVars::UV_NO_WRAP, "1")
1297 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1299 .env(EnvVars::COLUMNS, "100")
1302 .env(EnvVars::PATH, path)
1303 .env(EnvVars::HOME, self.home_dir.as_os_str())
1304 .env(EnvVars::APPDATA, self.home_dir.as_os_str())
1305 .env(EnvVars::USERPROFILE, self.home_dir.as_os_str())
1306 .env(
1307 EnvVars::XDG_CONFIG_DIRS,
1308 self.home_dir.join("config").as_os_str(),
1309 )
1310 .env(
1311 EnvVars::XDG_DATA_HOME,
1312 self.home_dir.join("data").as_os_str(),
1313 )
1314 .env(EnvVars::UV_NO_SYSTEM_CONFIG, "1")
1315 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "")
1316 .env(EnvVars::UV_PYTHON_DOWNLOADS, "never")
1318 .env(EnvVars::UV_PYTHON_SEARCH_PATH, self.python_path())
1319 .env(EnvVars::UV_EXCLUDE_NEWER, TEST_TIMESTAMP)
1320 .env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, TEST_TIMESTAMP)
1321 .env(EnvVars::UV_TEST_AVAILABLE_VERSION_CUTOFF, TEST_TIMESTAMP)
1322 .env(EnvVars::UV_PYTHON_NO_REGISTRY, "1")
1325 .env(EnvVars::UV_PYTHON_INSTALL_REGISTRY, "0")
1326 .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1")
1329 .env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path())
1343 .current_dir(self.temp_dir.path());
1344
1345 for (key, value) in &self.extra_env {
1346 command.env(key, value);
1347 }
1348
1349 if activate_venv {
1350 command.env(EnvVars::VIRTUAL_ENV, self.venv.as_os_str());
1351 }
1352
1353 if cfg!(unix) {
1354 command.env(EnvVars::LC_ALL, "C");
1356 }
1357 }
1358
1359 pub fn pip_compile(&self) -> Command {
1361 let mut command = self.new_command();
1362 command.arg("pip").arg("compile");
1363 self.add_shared_options(&mut command, true);
1364 command
1365 }
1366
1367 pub fn pip_sync(&self) -> Command {
1369 let mut command = self.new_command();
1370 command.arg("pip").arg("sync");
1371 self.add_shared_options(&mut command, true);
1372 command
1373 }
1374
1375 pub fn pip_show(&self) -> Command {
1376 let mut command = self.new_command();
1377 command.arg("pip").arg("show");
1378 self.add_shared_options(&mut command, true);
1379 command
1380 }
1381
1382 pub fn pip_freeze(&self) -> Command {
1384 let mut command = self.new_command();
1385 command.arg("pip").arg("freeze");
1386 self.add_shared_options(&mut command, true);
1387 command
1388 }
1389
1390 pub fn pip_check(&self) -> Command {
1392 let mut command = self.new_command();
1393 command.arg("pip").arg("check");
1394 self.add_shared_options(&mut command, true);
1395 command
1396 }
1397
1398 pub fn pip_list(&self) -> Command {
1399 let mut command = self.new_command();
1400 command.arg("pip").arg("list");
1401 self.add_shared_options(&mut command, true);
1402 command
1403 }
1404
1405 pub fn venv(&self) -> Command {
1407 let mut command = self.new_command();
1408 command.arg("venv");
1409 self.add_shared_options(&mut command, false);
1410 command
1411 }
1412
1413 pub fn pip_install(&self) -> Command {
1415 let mut command = self.new_command();
1416 command.arg("pip").arg("install");
1417 self.add_shared_options(&mut command, true);
1418 command
1419 }
1420
1421 pub fn pip_uninstall(&self) -> Command {
1423 let mut command = self.new_command();
1424 command.arg("pip").arg("uninstall");
1425 self.add_shared_options(&mut command, true);
1426 command
1427 }
1428
1429 pub fn pip_tree(&self) -> Command {
1431 let mut command = self.new_command();
1432 command.arg("pip").arg("tree");
1433 self.add_shared_options(&mut command, true);
1434 command
1435 }
1436
1437 pub fn pip_debug(&self) -> Command {
1439 let mut command = self.new_command();
1440 command.arg("pip").arg("debug");
1441 self.add_shared_options(&mut command, true);
1442 command
1443 }
1444
1445 pub fn help(&self) -> Command {
1447 let mut command = self.new_command();
1448 command.arg("help");
1449 self.add_shared_env(&mut command, false);
1450 command
1451 }
1452
1453 pub fn init(&self) -> Command {
1456 let mut command = self.new_command();
1457 command.arg("init");
1458 self.add_shared_options(&mut command, false);
1459 command
1460 }
1461
1462 pub fn sync(&self) -> Command {
1464 let mut command = self.new_command();
1465 command.arg("sync");
1466 self.add_shared_options(&mut command, false);
1467 command
1468 }
1469
1470 pub fn lock(&self) -> Command {
1472 let mut command = self.new_command();
1473 command.arg("lock");
1474 self.add_shared_options(&mut command, false);
1475 command
1476 }
1477
1478 pub fn upgrade(&self) -> Command {
1480 let mut command = self.new_command();
1481 command.arg("upgrade");
1482 self.add_shared_options(&mut command, false);
1483 command
1484 }
1485
1486 pub fn audit(&self) -> Command {
1488 let mut command = self.new_command();
1489 command.arg("audit");
1490 self.add_shared_options(&mut command, false);
1491 command
1492 }
1493
1494 pub fn workspace_metadata(&self) -> Command {
1496 let mut command = self.new_command();
1497 command.arg("workspace").arg("metadata");
1498 self.add_shared_options(&mut command, false);
1499 command
1500 }
1501
1502 pub fn workspace_dir(&self) -> Command {
1504 let mut command = self.new_command();
1505 command.arg("workspace").arg("dir");
1506 self.add_shared_options(&mut command, false);
1507 command
1508 }
1509
1510 pub fn workspace_list(&self) -> Command {
1512 let mut command = self.new_command();
1513 command.arg("workspace").arg("list");
1514 self.add_shared_options(&mut command, false);
1515 command
1516 }
1517
1518 pub fn export(&self) -> Command {
1520 let mut command = self.new_command();
1521 command.arg("export");
1522 self.add_shared_options(&mut command, false);
1523 command
1524 }
1525
1526 pub fn format(&self) -> Command {
1528 let mut command = self.new_command();
1529 command.arg("format");
1530 self.add_shared_options(&mut command, false);
1531 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1533 command
1534 }
1535
1536 pub fn check(&self) -> Command {
1538 let mut command = self.new_command();
1539 command.arg("check");
1540 self.add_shared_options(&mut command, false);
1541 command.env(EnvVars::UV_EXCLUDE_NEWER, "2026-02-15T00:00:00Z");
1543 command
1544 }
1545
1546 pub fn build(&self) -> Command {
1548 let mut command = self.new_command();
1549 command.arg("build");
1550 self.add_shared_options(&mut command, false);
1551 command
1552 }
1553
1554 pub fn version(&self) -> Command {
1555 let mut command = self.new_command();
1556 command.arg("version");
1557 self.add_shared_options(&mut command, false);
1558 command
1559 }
1560
1561 pub fn self_version(&self) -> Command {
1562 let mut command = self.new_command();
1563 command.arg("self").arg("version");
1564 self.add_shared_options(&mut command, false);
1565 command
1566 }
1567
1568 pub fn self_update(&self) -> Command {
1569 let mut command = self.new_command();
1570 command.arg("self").arg("update");
1571 self.add_shared_options(&mut command, false);
1572 command
1573 }
1574
1575 pub fn publish(&self) -> Command {
1577 let mut command = self.new_command();
1578 command.arg("publish");
1579 self.add_shared_options(&mut command, false);
1580 command
1581 }
1582
1583 pub fn python_find(&self) -> Command {
1585 let mut command = self.new_command();
1586 command
1587 .arg("python")
1588 .arg("find")
1589 .env(EnvVars::UV_PREVIEW, "1")
1590 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1591 self.add_shared_options(&mut command, false);
1592 command
1593 }
1594
1595 pub fn python_list(&self) -> Command {
1597 let mut command = self.new_command();
1598 command
1599 .arg("python")
1600 .arg("list")
1601 .env(EnvVars::UV_PYTHON_INSTALL_DIR, "");
1602 self.add_shared_options(&mut command, false);
1603 command
1604 }
1605
1606 pub fn python_install(&self) -> Command {
1608 let mut command = self.new_command();
1609 command.arg("python").arg("install");
1610 self.add_shared_options(&mut command, true);
1611 command
1612 }
1613
1614 pub fn python_uninstall(&self) -> Command {
1616 let mut command = self.new_command();
1617 command.arg("python").arg("uninstall");
1618 self.add_shared_options(&mut command, true);
1619 command
1620 }
1621
1622 pub fn python_upgrade(&self) -> Command {
1624 let mut command = self.new_command();
1625 command.arg("python").arg("upgrade");
1626 self.add_shared_options(&mut command, true);
1627 command
1628 }
1629
1630 pub fn python_pin(&self) -> Command {
1632 let mut command = self.new_command();
1633 command.arg("python").arg("pin");
1634 self.add_shared_options(&mut command, true);
1635 command
1636 }
1637
1638 pub fn python_dir(&self) -> Command {
1640 let mut command = self.new_command();
1641 command.arg("python").arg("dir");
1642 self.add_shared_options(&mut command, true);
1643 command
1644 }
1645
1646 pub fn run(&self) -> Command {
1648 let mut command = self.new_command();
1649 command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1");
1650 self.add_shared_options(&mut command, true);
1651 command
1652 }
1653
1654 pub fn tool_run(&self) -> Command {
1656 let mut command = self.new_command();
1657 command
1658 .arg("tool")
1659 .arg("run")
1660 .env(EnvVars::UV_SHOW_RESOLUTION, "1");
1661 self.add_shared_options(&mut command, false);
1662 command
1663 }
1664
1665 pub fn tool_upgrade(&self) -> Command {
1667 let mut command = self.new_command();
1668 command.arg("tool").arg("upgrade");
1669 self.add_shared_options(&mut command, false);
1670 command
1671 }
1672
1673 pub fn tool_install(&self) -> Command {
1675 let mut command = self.new_command();
1676 command.arg("tool").arg("install");
1677 self.add_shared_options(&mut command, false);
1678 command
1679 }
1680
1681 pub fn tool_list(&self) -> Command {
1683 let mut command = self.new_command();
1684 command.arg("tool").arg("list");
1685 self.add_shared_options(&mut command, false);
1686 command
1687 }
1688
1689 pub fn tool_audit(&self) -> Command {
1691 let mut command = self.new_command();
1692 command.arg("tool").arg("audit");
1693 self.add_shared_options(&mut command, false);
1694 command
1695 }
1696
1697 pub fn tool_dir(&self) -> Command {
1699 let mut command = self.new_command();
1700 command.arg("tool").arg("dir");
1701 self.add_shared_options(&mut command, false);
1702 command
1703 }
1704
1705 pub fn tool_uninstall(&self) -> Command {
1707 let mut command = self.new_command();
1708 command.arg("tool").arg("uninstall");
1709 self.add_shared_options(&mut command, false);
1710 command
1711 }
1712
1713 pub fn add(&self) -> Command {
1715 let mut command = self.new_command();
1716 command.arg("add");
1717 self.add_shared_options(&mut command, false);
1718 command
1719 }
1720
1721 pub fn remove(&self) -> Command {
1723 let mut command = self.new_command();
1724 command.arg("remove");
1725 self.add_shared_options(&mut command, false);
1726 command
1727 }
1728
1729 pub fn tree(&self) -> Command {
1731 let mut command = self.new_command();
1732 command.arg("tree");
1733 self.add_shared_options(&mut command, false);
1734 command
1735 }
1736
1737 pub fn clean(&self) -> Command {
1739 let mut command = self.new_command();
1740 command.arg("cache").arg("clean");
1741 self.add_shared_options(&mut command, false);
1742 command
1743 }
1744
1745 pub fn prune(&self) -> Command {
1747 let mut command = self.new_command();
1748 command.arg("cache").arg("prune");
1749 self.add_shared_options(&mut command, false);
1750 command
1751 }
1752
1753 pub fn cache_size(&self) -> Command {
1755 let mut command = self.new_command();
1756 command.arg("cache").arg("size");
1757 self.add_shared_options(&mut command, false);
1758 command
1759 }
1760
1761 pub fn build_backend(&self) -> Command {
1765 let mut command = self.new_command();
1766 command.arg("build-backend");
1767 self.add_shared_options(&mut command, false);
1768 command
1769 }
1770
1771 pub fn interpreter(&self) -> PathBuf {
1775 let venv = &self.venv;
1776 if cfg!(unix) {
1777 venv.join("bin").join("python")
1778 } else if cfg!(windows) {
1779 venv.join("Scripts").join("python.exe")
1780 } else {
1781 unimplemented!("Only Windows and Unix are supported")
1782 }
1783 }
1784
1785 pub fn python_command(&self) -> Command {
1786 let mut interpreter = self.interpreter();
1787
1788 if !interpreter.exists() {
1790 interpreter.clone_from(
1791 &self
1792 .python_versions
1793 .first()
1794 .expect("At least one Python version is required")
1795 .1,
1796 );
1797 }
1798
1799 let mut command = Self::new_command_with(&interpreter);
1800 command
1801 .arg("-B")
1804 .env(EnvVars::PYTHONUTF8, "1");
1806
1807 self.add_shared_env(&mut command, false);
1808
1809 command
1810 }
1811
1812 pub fn auth_login(&self) -> Command {
1814 let mut command = self.new_command();
1815 command.arg("auth").arg("login");
1816 self.add_shared_options(&mut command, false);
1817 command
1818 }
1819
1820 pub fn auth_logout(&self) -> Command {
1822 let mut command = self.new_command();
1823 command.arg("auth").arg("logout");
1824 self.add_shared_options(&mut command, false);
1825 command
1826 }
1827
1828 pub fn auth_helper(&self) -> Command {
1830 let mut command = self.new_command();
1831 command.arg("auth").arg("helper");
1832 self.add_shared_options(&mut command, false);
1833 command
1834 }
1835
1836 pub fn auth_token(&self) -> Command {
1838 let mut command = self.new_command();
1839 command.arg("auth").arg("token");
1840 self.add_shared_options(&mut command, false);
1841 command
1842 }
1843
1844 #[must_use]
1848 pub fn with_real_home(mut self) -> Self {
1849 if let Some(home) = env::var_os(EnvVars::HOME) {
1850 self.extra_env
1851 .push((EnvVars::HOME.to_string().into(), home));
1852 }
1853 self.extra_env.push((
1856 EnvVars::XDG_CONFIG_HOME.into(),
1857 self.user_config_dir.as_os_str().into(),
1858 ));
1859 self
1860 }
1861
1862 pub fn assert_command(&self, command: &str) -> Assert {
1864 self.python_command()
1865 .arg("-c")
1866 .arg(command)
1867 .current_dir(&self.temp_dir)
1868 .assert()
1869 }
1870
1871 pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
1873 self.python_command()
1874 .arg(file.as_ref())
1875 .current_dir(&self.temp_dir)
1876 .assert()
1877 }
1878
1879 pub fn assert_installed(&self, package: &'static str, version: &'static str) {
1881 self.assert_command(
1882 format!("import {package} as package; print(package.__version__, end='')").as_str(),
1883 )
1884 .success()
1885 .stdout(version);
1886 }
1887
1888 pub fn assert_not_installed(&self, package: &'static str) {
1890 self.assert_command(format!("import {package}").as_str())
1891 .failure();
1892 }
1893
1894 pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
1896 let mut patterns = Vec::new();
1897
1898 if path.as_ref().exists() {
1900 patterns.push(Self::path_pattern(
1901 path.as_ref()
1902 .canonicalize()
1903 .expect("Failed to create canonical path"),
1904 ));
1905 }
1906
1907 patterns.push(Self::path_pattern(path));
1909
1910 patterns
1911 }
1912
1913 fn path_pattern(path: impl AsRef<Path>) -> String {
1915 format!(
1916 r"{}\\?/?",
1918 regex::escape(&path.as_ref().simplified_display().to_string())
1919 .replace(r"\\", r"(\\|\/)")
1922 )
1923 }
1924
1925 pub fn python_path(&self) -> OsString {
1926 if cfg!(unix) {
1927 env::join_paths(
1929 self.python_versions
1930 .iter()
1931 .map(|(version, _)| self.python_dir.join(version.to_string())),
1932 )
1933 .unwrap()
1934 } else {
1935 env::join_paths(
1937 self.python_versions
1938 .iter()
1939 .map(|(_, executable)| executable.parent().unwrap().to_path_buf()),
1940 )
1941 .unwrap()
1942 }
1943 }
1944
1945 pub fn filters(&self) -> Vec<(&str, &str)> {
1947 self.filters
1950 .iter()
1951 .map(|(p, r)| (p.as_str(), r.as_str()))
1952 .chain(INSTA_FILTERS.iter().copied())
1953 .collect()
1954 }
1955
1956 #[cfg(windows)]
1958 pub fn filters_without_standard_filters(&self) -> Vec<(&str, &str)> {
1959 self.filters
1960 .iter()
1961 .map(|(p, r)| (p.as_str(), r.as_str()))
1962 .collect()
1963 }
1964
1965 pub fn python_kind(&self) -> &'static str {
1967 "python"
1968 }
1969
1970 pub fn site_packages(&self) -> PathBuf {
1972 site_packages_path(
1973 &self.venv,
1974 &format!(
1975 "{}{}",
1976 self.python_kind(),
1977 self.python_version.as_ref().expect(
1978 "A Python version must be provided to retrieve the test site packages path"
1979 )
1980 ),
1981 )
1982 }
1983
1984 pub fn reset_venv(&self) {
1986 self.create_venv();
1987 }
1988
1989 fn create_venv(&self) {
1991 let executable = get_python(
1992 self.python_version
1993 .as_ref()
1994 .expect("A Python version must be provided to create a test virtual environment"),
1995 );
1996 create_venv_from_executable(&self.venv, &self.cache_dir, &executable, &self.uv_bin);
1997 }
1998
1999 pub fn copy_ecosystem_project(&self, name: &str) {
2010 let project_dir = PathBuf::from(format!("../../test/ecosystem/{name}"));
2011 self.temp_dir.copy_from(project_dir, &["**/*"]).unwrap();
2012 if let Err(err) = fs_err::remove_file(self.temp_dir.join("uv.lock")) {
2014 assert_eq!(
2015 err.kind(),
2016 io::ErrorKind::NotFound,
2017 "Failed to remove uv.lock: {err}"
2018 );
2019 }
2020 }
2021
2022 pub fn diff_lock(&self, change: impl Fn(&Self) -> Command) -> String {
2031 let lock_path = ChildPath::new(self.temp_dir.join("uv.lock"));
2032 let old_lock = fs_err::read_to_string(&lock_path).unwrap();
2033 let (snapshot, output) = run_and_format(
2034 change(self),
2035 self.filters(),
2036 "diff_lock",
2037 Some(WindowsFilters::Platform),
2038 None,
2039 );
2040 assert!(output.status.success(), "{snapshot}");
2041 let new_lock = fs_err::read_to_string(&lock_path).unwrap();
2042 diff_snapshot(&old_lock, &new_lock, 10)
2043 }
2044
2045 pub fn read(&self, file: impl AsRef<Path>) -> String {
2047 fs_err::read_to_string(self.temp_dir.join(&file))
2048 .unwrap_or_else(|_| panic!("Missing file: `{}`", file.user_display()))
2049 }
2050
2051 fn new_command(&self) -> Command {
2054 Self::new_command_with(&self.uv_bin)
2055 }
2056
2057 fn new_command_with(bin: &Path) -> Command {
2063 let mut command = Command::new(bin);
2064
2065 let passthrough = [
2066 EnvVars::PATH,
2068 EnvVars::RUST_LOG,
2070 EnvVars::RUST_BACKTRACE,
2071 EnvVars::SYSTEMDRIVE,
2073 EnvVars::RUST_MIN_STACK,
2075 EnvVars::UV_STACK_SIZE,
2076 EnvVars::ALL_PROXY,
2078 EnvVars::HTTPS_PROXY,
2079 EnvVars::HTTP_PROXY,
2080 EnvVars::NO_PROXY,
2081 EnvVars::SSL_CERT_DIR,
2082 EnvVars::SSL_CERT_FILE,
2083 EnvVars::UV_NATIVE_TLS,
2084 EnvVars::UV_SYSTEM_CERTS,
2085 ];
2086
2087 for env_var in EnvVars::all_names()
2088 .iter()
2089 .filter(|name| !passthrough.contains(name))
2090 {
2091 command.env_remove(env_var);
2092 }
2093
2094 command
2095 }
2096}
2097
2098pub fn diff_snapshot(old: &str, new: &str, context_radius: usize) -> String {
2101 let diff = similar::TextDiff::from_lines(old, new);
2102 let unified = diff
2103 .unified_diff()
2104 .context_radius(context_radius)
2105 .header("old", "new")
2106 .to_string();
2107 regex!(r"(?m)^\s+$").replace_all(&unified, "").into_owned()
2111}
2112
2113#[macro_export]
2117macro_rules! diff_uv_snapshot {
2118 ($filters:expr, $old:expr, $spawnable:expr, @$snapshot:literal) => {{
2119 let new = $crate::capture_uv_snapshot!($filters, $spawnable);
2120 let snapshot = $crate::diff_snapshot($old, &new, 3);
2121 let mut settings = ::insta::Settings::clone_current();
2122 let description = match settings.description() {
2124 Some(description) => format!("{description}\n\nUnfiltered diff:\n{snapshot}"),
2125 None => format!("Unfiltered diff:\n{snapshot}"),
2126 };
2127 settings.set_description(description);
2128 settings.add_filter(r"^--- old\n\+\+\+ new\n", "");
2129 settings.add_filter(r"(?m)^@@.*$", "...");
2130 settings.add_filter(r"\n$", "\n...\n");
2131 settings.bind(|| {
2132 ::insta::assert_snapshot!(snapshot, @$snapshot);
2133 });
2134 new
2135 }};
2136}
2137
2138#[macro_export]
2140macro_rules! capture_uv_snapshot {
2141 ($filters:expr, $spawnable:expr) => {{
2142 let (snapshot, _) = $crate::run_and_format_silent(
2144 $spawnable,
2145 &$filters,
2146 $crate::function_name!(),
2147 Some($crate::WindowsFilters::Platform),
2148 None,
2149 );
2150 snapshot
2151 }};
2152 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2153 let (snapshot, _) = $crate::run_and_format(
2154 $spawnable,
2155 &$filters,
2156 $crate::function_name!(),
2157 Some($crate::WindowsFilters::Platform),
2158 None,
2159 );
2160 ::insta::assert_snapshot!(snapshot, @$snapshot);
2161 snapshot
2162 }};
2163}
2164
2165pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
2166 if cfg!(unix) {
2167 venv.join("lib").join(python).join("site-packages")
2168 } else if cfg!(windows) {
2169 venv.join("Lib").join("site-packages")
2170 } else {
2171 unimplemented!("Only Windows and Unix are supported")
2172 }
2173}
2174
2175pub fn venv_bin_path(venv: impl AsRef<Path>) -> PathBuf {
2176 if cfg!(unix) {
2177 venv.as_ref().join("bin")
2178 } else if cfg!(windows) {
2179 venv.as_ref().join("Scripts")
2180 } else {
2181 unimplemented!("Only Windows and Unix are supported")
2182 }
2183}
2184
2185fn get_python(version: &PythonVersion) -> PathBuf {
2187 ManagedPythonInstallations::from_settings(None)
2188 .map(|installed_pythons| {
2189 installed_pythons
2190 .find_version(version)
2191 .expect("Tests are run on a supported platform")
2192 .next()
2193 .as_ref()
2194 .map(|python| python.executable(false))
2195 })
2196 .unwrap_or_default()
2199 .unwrap_or(PathBuf::from(version.to_string()))
2200}
2201
2202fn create_venv_from_executable<P: AsRef<Path>>(
2204 path: P,
2205 cache_dir: &ChildPath,
2206 python: &Path,
2207 uv_bin: &Path,
2208) {
2209 TestContext::new_command_with(uv_bin)
2210 .arg("venv")
2211 .arg(path.as_ref().as_os_str())
2212 .arg("--clear")
2213 .arg("--cache-dir")
2214 .arg(cache_dir.path())
2215 .arg("--python")
2216 .arg(python)
2217 .current_dir(path.as_ref().parent().unwrap())
2218 .assert()
2219 .success();
2220 ChildPath::new(path.as_ref()).assert(predicate::path::is_dir());
2221}
2222
2223pub fn python_path_with_versions(
2227 temp_dir: &ChildPath,
2228 python_versions: &[&str],
2229) -> anyhow::Result<OsString> {
2230 let download_list = ManagedPythonDownloadList::new_only_embedded().unwrap();
2231 Ok(env::join_paths(
2232 python_installations_for_versions(temp_dir, python_versions, &download_list)?
2233 .into_iter()
2234 .map(|path| path.parent().unwrap().to_path_buf()),
2235 )?)
2236}
2237
2238fn python_installations_for_versions(
2242 temp_dir: &ChildPath,
2243 python_versions: &[&str],
2244 download_list: &ManagedPythonDownloadList,
2245) -> anyhow::Result<Vec<PathBuf>> {
2246 let cache = Cache::from_path(temp_dir.child("cache").to_path_buf())
2247 .init_no_wait()?
2248 .expect("No cache contention when setting up Python in tests");
2249 let _preview = uv_preview::test::with_features(&[]);
2250 let selected_pythons = python_versions
2251 .iter()
2252 .map(|python_version| {
2253 if let Ok(python) = PythonInstallation::find(
2254 &PythonRequest::parse(python_version),
2255 EnvironmentPreference::OnlySystem,
2256 PythonPreference::Managed,
2257 download_list,
2258 &cache,
2259 ) {
2260 python.into_interpreter().sys_executable().to_owned()
2261 } else {
2262 panic!("Could not find Python {python_version} for test\nTry `cargo run python install` first, or refer to CONTRIBUTING.md");
2263 }
2264 })
2265 .collect::<Vec<_>>();
2266
2267 assert!(
2268 python_versions.is_empty() || !selected_pythons.is_empty(),
2269 "Failed to fulfill requested test Python versions: {selected_pythons:?}"
2270 );
2271
2272 Ok(selected_pythons)
2273}
2274
2275#[derive(Debug, Copy, Clone)]
2276pub enum WindowsFilters {
2277 Platform,
2278 Universal,
2279}
2280
2281pub fn apply_filters<T: AsRef<str>>(mut snapshot: String, filters: impl AsRef<[(T, T)]>) -> String {
2283 for (matcher, replacement) in filters.as_ref() {
2284 let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?");
2286 if re.is_match(&snapshot) {
2287 snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string();
2288 }
2289 }
2290 snapshot
2291}
2292
2293#[expect(clippy::print_stderr)]
2297pub fn run_and_format<T: AsRef<str>>(
2298 command: impl BorrowMut<Command>,
2299 filters: impl AsRef<[(T, T)]>,
2300 function_name: &str,
2301 windows_filters: Option<WindowsFilters>,
2302 input: Option<&str>,
2303) -> (String, Output) {
2304 let (snapshot, output) =
2305 run_and_format_silent(command, filters, function_name, windows_filters, input);
2306 eprintln!("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Unfiltered output ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2307 eprintln!(
2308 "----- exit status -----\n{}\n----- stdout -----\n{}\n----- stderr -----\n{}",
2309 output.status,
2310 String::from_utf8_lossy(&output.stdout),
2311 String::from_utf8_lossy(&output.stderr),
2312 );
2313 eprintln!("────────────────────────────────────────────────────────────────────────────────\n");
2314 (snapshot, output)
2315}
2316
2317#[doc(hidden)]
2319pub fn run_and_format_silent<T: AsRef<str>>(
2320 mut command: impl BorrowMut<Command>,
2321 filters: impl AsRef<[(T, T)]>,
2322 function_name: &str,
2323 windows_filters: Option<WindowsFilters>,
2324 input: Option<&str>,
2325) -> (String, Output) {
2326 assert_effective_cache_directory(command.borrow_mut());
2327
2328 let program = command
2329 .borrow_mut()
2330 .get_program()
2331 .to_string_lossy()
2332 .to_string();
2333
2334 if let Ok(root) = env::var(EnvVars::TRACING_DURATIONS_TEST_ROOT) {
2336 #[expect(clippy::assertions_on_constants)]
2338 {
2339 assert!(
2340 cfg!(feature = "tracing-durations-export"),
2341 "You need to enable the tracing-durations-export feature to use `TRACING_DURATIONS_TEST_ROOT`"
2342 );
2343 }
2344 command.borrow_mut().env(
2345 EnvVars::TRACING_DURATIONS_FILE,
2346 Path::new(&root).join(function_name).with_extension("jsonl"),
2347 );
2348 }
2349
2350 let output = if let Some(input) = input {
2351 let mut child = command
2352 .borrow_mut()
2353 .stdin(Stdio::piped())
2354 .stdout(Stdio::piped())
2355 .stderr(Stdio::piped())
2356 .spawn()
2357 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"));
2358 child
2359 .stdin
2360 .as_mut()
2361 .expect("Failed to open stdin")
2362 .write_all(input.as_bytes())
2363 .expect("Failed to write to stdin");
2364
2365 child
2366 .wait_with_output()
2367 .unwrap_or_else(|err| panic!("Failed to read output from {program}: {err}"))
2368 } else {
2369 command
2370 .borrow_mut()
2371 .output()
2372 .unwrap_or_else(|err| panic!("Failed to spawn {program}: {err}"))
2373 };
2374
2375 let mut snapshot = format!(
2376 "exit_code: {} ({})\n",
2377 output.status.code().unwrap_or(!0),
2378 if output.status.success() {
2379 "success"
2380 } else {
2381 "failure"
2382 },
2383 );
2384 if output.status.code().is_none() {
2385 snapshot.push_str("exit_status: ");
2386 snapshot.push_str(&output.status.to_string());
2387 snapshot.push('\n');
2388 }
2389 if !output.stdout.is_empty() {
2390 snapshot.push_str("----- stdout -----\n");
2391 snapshot.push_str(&String::from_utf8_lossy(&output.stdout));
2392 }
2393 if !output.stderr.is_empty() {
2394 if !output.stdout.is_empty() {
2395 snapshot.push('\n');
2396 }
2397 snapshot.push_str("----- stderr -----\n");
2398 snapshot.push_str(&String::from_utf8_lossy(&output.stderr));
2399 }
2400 let mut snapshot = apply_filters(snapshot, filters);
2401
2402 if cfg!(windows) {
2407 if let Some(windows_filters) = windows_filters {
2408 let windows_only_deps = [
2410 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2411 (r"( ?[-+~] ?)?colorama==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2412 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+( [\\]\n\s+--hash=.*)?\n(\s+# via .*\n)?"),
2413 (r"( ?[-+~] ?)?tzdata==\d+(\.\d+)+(\s+[-+~]?\s+# via .*)?\n"),
2414 ];
2415 let mut removed_packages = 0;
2416 for windows_only_dep in windows_only_deps {
2417 let re = Regex::new(windows_only_dep).unwrap();
2419 if re.is_match(&snapshot) {
2420 snapshot = re.replace(&snapshot, "").to_string();
2421 removed_packages += 1;
2422 }
2423 }
2424 if removed_packages > 0 {
2425 for i in 1..20 {
2426 for verb in match windows_filters {
2427 WindowsFilters::Platform => [
2428 "Resolved",
2429 "Prepared",
2430 "Installed",
2431 "Checked",
2432 "Uninstalled",
2433 ]
2434 .iter(),
2435 WindowsFilters::Universal => {
2436 ["Prepared", "Installed", "Checked", "Uninstalled"].iter()
2437 }
2438 } {
2439 snapshot = snapshot.replace(
2440 &format!("{verb} {} packages", i + removed_packages),
2441 &format!("{verb} {} package{}", i, if i > 1 { "s" } else { "" }),
2442 );
2443 }
2444 }
2445 }
2446 }
2447 }
2448
2449 (snapshot, output)
2450}
2451
2452fn assert_effective_cache_directory(command: &Command) {
2458 let cache_directory_override = command
2459 .get_envs()
2460 .find(|(name, value)| *name == EnvVars::UV_CACHE_DIR && value.is_some());
2461
2462 if cache_directory_override.is_none() {
2463 return;
2464 }
2465
2466 let explicit_cache_directory = command.get_args().any(|argument| {
2467 argument == "--cache-dir"
2468 || argument
2469 .to_str()
2470 .is_some_and(|argument| argument.starts_with("--cache-dir="))
2471 });
2472
2473 assert!(
2474 !explicit_cache_directory,
2475 "`UV_CACHE_DIR` is ignored because this command already supplies `--cache-dir`; configure `TestContext::cache_dir` instead"
2476 );
2477}
2478
2479pub fn copy_dir_ignore(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> anyhow::Result<()> {
2481 for entry in ignore::Walk::new(&src) {
2482 let entry = entry?;
2483 let relative = entry.path().strip_prefix(&src)?;
2484 let ty = entry.file_type().unwrap();
2485 if ty.is_dir() {
2486 fs_err::create_dir(dst.as_ref().join(relative))?;
2487 } else {
2488 fs_err::copy(entry.path(), dst.as_ref().join(relative))?;
2489 }
2490 }
2491 Ok(())
2492}
2493
2494pub fn make_project(dir: &Path, name: &str, body: &str) -> anyhow::Result<()> {
2496 let pyproject_toml = formatdoc! {r#"
2497 [project]
2498 name = "{name}"
2499 version = "0.1.0"
2500 requires-python = ">=3.11,<3.13"
2501 {body}
2502
2503 [build-system]
2504 requires = ["uv_build>=0.9.0,<10000"]
2505 build-backend = "uv_build"
2506 "#
2507 };
2508 fs_err::create_dir_all(dir)?;
2509 fs_err::write(dir.join("pyproject.toml"), pyproject_toml)?;
2510 fs_err::create_dir_all(dir.join("src").join(name))?;
2511 fs_err::write(dir.join("src").join(name).join("__init__.py"), "")?;
2512 Ok(())
2513}
2514
2515pub const READ_ONLY_GITHUB_TOKEN: &[&str] = &[
2517 "Z2l0aHViCg==",
2518 "cGF0Cg==",
2519 "MTFBQlVDUjZBMERMUTQ3aVphN3hPdV9qQmhTMkZUeHZ4ZE13OHczakxuZndsV2ZlZjc2cE53eHBWS2tiRUFwdnpmUk8zV0dDSUhicDFsT01aago=",
2520];
2521
2522#[cfg(not(windows))]
2524pub const READ_ONLY_GITHUB_TOKEN_2: &[&str] = &[
2525 "Z2l0aHViCg==",
2526 "cGF0Cg==",
2527 "MTFBQlVDUjZBMDJTOFYwMTM4YmQ0bV9uTXpueWhxZDBrcllROTQ5SERTeTI0dENKZ2lmdzIybDFSR2s1SE04QW8xTUVYQ1I0Q1YxYUdPRGpvZQo=",
2528];
2529
2530pub const READ_ONLY_GITHUB_SSH_DEPLOY_KEY: &str = "LS0tLS1CRUdJTiBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0KYjNCbGJuTnphQzFyWlhrdGRqRUFBQUFBQkc1dmJtVUFBQUFFYm05dVpRQUFBQUFBQUFBQkFBQUFNd0FBQUF0emMyZ3RaVwpReU5UVXhPUUFBQUNBeTF1SnNZK1JXcWp1NkdIY3Z6a3AwS21yWDEwdmo3RUZqTkpNTkRqSGZPZ0FBQUpqWUpwVnAyQ2FWCmFRQUFBQXR6YzJndFpXUXlOVFV4T1FBQUFDQXkxdUpzWStSV3FqdTZHSGN2emtwMEttclgxMHZqN0VGak5KTU5EakhmT2cKQUFBRUMwbzBnd1BxbGl6TFBJOEFXWDVaS2dVZHJyQ2ptMDhIQm9FenB4VDg3MXBqTFc0bXhqNUZhcU83b1lkeS9PU25RcQphdGZYUytQc1FXTTBrdzBPTWQ4NkFBQUFFR3R2Ym5OMGFVQmhjM1J5WVd3dWMyZ0JBZ01FQlE9PQotLS0tLUVORCBPUEVOU1NIIFBSSVZBVEUgS0VZLS0tLS0K";
2531
2532pub fn decode_token(content: &[&str]) -> String {
2535 content
2536 .iter()
2537 .map(|part| base64.decode(part).unwrap())
2538 .map(|decoded| {
2539 std::str::from_utf8(decoded.as_slice())
2540 .unwrap()
2541 .trim_end()
2542 .to_string()
2543 })
2544 .join("_")
2545}
2546
2547#[tokio::main(flavor = "current_thread")]
2550pub async fn download_to_disk(url: &str, path: &Path) {
2551 let trusted_hosts: Vec<_> = env::var(EnvVars::UV_INSECURE_HOST)
2552 .unwrap_or_default()
2553 .split(' ')
2554 .map(|h| uv_configuration::TrustedHost::from_str(h).unwrap())
2555 .collect();
2556
2557 let client = uv_client::BaseClientBuilder::default()
2558 .allow_insecure_host(trusted_hosts)
2559 .build()
2560 .expect("failed to build base client");
2561 let url = url.parse().unwrap();
2562 let response = client
2563 .for_host(&url)
2564 .get(reqwest::Url::from(url))
2565 .send()
2566 .await
2567 .unwrap();
2568
2569 let mut file = fs_err::tokio::File::create(path).await.unwrap();
2570 let mut stream = response.bytes_stream();
2571 while let Some(chunk) = stream.next().await {
2572 file.write_all(&chunk.unwrap()).await.unwrap();
2573 }
2574 file.sync_all().await.unwrap();
2575}
2576
2577#[cfg(unix)]
2582pub struct ReadOnlyDirectoryGuard {
2583 path: PathBuf,
2584 original_mode: u32,
2585}
2586
2587#[cfg(unix)]
2588impl ReadOnlyDirectoryGuard {
2589 pub fn new(path: impl Into<PathBuf>) -> std::io::Result<Self> {
2592 use std::os::unix::fs::PermissionsExt;
2593 let path = path.into();
2594 let metadata = fs_err::metadata(&path)?;
2595 let original_mode = metadata.permissions().mode();
2596 let readonly_mode = original_mode & !0o222;
2598 fs_err::set_permissions(&path, std::fs::Permissions::from_mode(readonly_mode))?;
2599 Ok(Self {
2600 path,
2601 original_mode,
2602 })
2603 }
2604}
2605
2606#[cfg(unix)]
2607impl Drop for ReadOnlyDirectoryGuard {
2608 fn drop(&mut self) {
2609 use std::os::unix::fs::PermissionsExt;
2610 let _ = fs_err::set_permissions(
2611 &self.path,
2612 std::fs::Permissions::from_mode(self.original_mode),
2613 );
2614 }
2615}
2616
2617#[doc(hidden)]
2621#[macro_export]
2622macro_rules! function_name {
2623 () => {{
2624 fn f() {}
2625 fn type_name_of_val<T>(_: T) -> &'static str {
2626 std::any::type_name::<T>()
2627 }
2628 let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
2629 while let Some(rest) = name.strip_suffix("::{{closure}}") {
2630 name = rest;
2631 }
2632 name
2633 }};
2634}
2635
2636#[macro_export]
2641macro_rules! uv_snapshot {
2642 ($spawnable:expr, @$snapshot:literal) => {{
2643 uv_snapshot!($crate::INSTA_FILTERS.to_vec(), $spawnable, @$snapshot)
2644 }};
2645 ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{
2646 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), None);
2648 ::insta::assert_snapshot!(snapshot, @$snapshot);
2649 output
2650 }};
2651 ($filters:expr, $spawnable:expr, input=$input:expr, @$snapshot:literal) => {{
2652 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Platform), Some($input));
2654 ::insta::assert_snapshot!(snapshot, @$snapshot);
2655 output
2656 }};
2657 ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{
2658 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), None, None);
2660 ::insta::assert_snapshot!(snapshot, @$snapshot);
2661 output
2662 }};
2663 ($filters:expr, universal_windows_filters=true, $spawnable:expr, @$snapshot:literal) => {{
2664 let (snapshot, output) = $crate::run_and_format($spawnable, &$filters, $crate::function_name!(), Some($crate::WindowsFilters::Universal), None);
2666 ::insta::assert_snapshot!(snapshot, @$snapshot);
2667 output
2668 }};
2669}
2670
2671#[cfg(all(test, unix))]
2672mod process_status_tests {
2673 use std::process::Command;
2674
2675 use super::run_and_format_silent;
2676
2677 #[test]
2678 fn reports_signal() {
2679 let mut command = Command::new("sh");
2680 command.args(["-c", "kill -TERM $$"]);
2681 let filters: &[(&str, &str)] = &[];
2682 let (snapshot, _) = run_and_format_silent(command, filters, "reports_signal", None, None);
2683
2684 insta::assert_snapshot!(snapshot, @"
2685 exit_code: -1 (failure)
2686 exit_status: signal: 15 (SIGTERM)
2687 ");
2688 }
2689
2690 #[test]
2691 fn preserves_exit_code() {
2692 let mut command = Command::new("sh");
2693 command.args(["-c", "exit 7"]);
2694 let filters: &[(&str, &str)] = &[];
2695 let (snapshot, _) =
2696 run_and_format_silent(command, filters, "preserves_exit_code", None, None);
2697
2698 insta::assert_snapshot!(snapshot, @"exit_code: 7 (failure)");
2699 }
2700}
2701
2702#[cfg(test)]
2703mod cache_directory_tests {
2704 use std::process::Command;
2705
2706 use uv_static::EnvVars;
2707
2708 use super::assert_effective_cache_directory;
2709
2710 #[test]
2711 #[should_panic(expected = "`UV_CACHE_DIR` is ignored")]
2712 fn rejects_environment_override_with_explicit_cache_argument() {
2713 let mut command = Command::new("uv");
2714 command
2715 .arg("--cache-dir")
2716 .arg("context-cache")
2717 .env(EnvVars::UV_CACHE_DIR, "ignored-cache");
2718
2719 assert_effective_cache_directory(&command);
2720 }
2721
2722 #[test]
2723 #[should_panic(expected = "`UV_CACHE_DIR` is ignored")]
2724 fn rejects_environment_override_with_inline_cache_argument() {
2725 let mut command = Command::new("uv");
2726 command
2727 .arg("--cache-dir=context-cache")
2728 .env(EnvVars::UV_CACHE_DIR, "ignored-cache");
2729
2730 assert_effective_cache_directory(&command);
2731 }
2732
2733 #[test]
2734 fn allows_environment_override_without_explicit_cache_argument() {
2735 let mut command = Command::new("uv");
2736 command
2737 .arg("cache")
2738 .arg("dir")
2739 .env(EnvVars::UV_CACHE_DIR, "effective-cache");
2740
2741 assert_effective_cache_directory(&command);
2742 }
2743
2744 #[test]
2745 fn allows_removed_environment_override_with_explicit_cache_argument() {
2746 let mut command = Command::new("uv");
2747 command
2748 .arg("--cache-dir")
2749 .arg("context-cache")
2750 .env_remove(EnvVars::UV_CACHE_DIR);
2751
2752 assert_effective_cache_directory(&command);
2753 }
2754}