Skip to content

Commit e7d5324

Browse files
committed
Add test case for automatic installs
1 parent fd5131c commit e7d5324

File tree

2 files changed

+143
-34
lines changed

2 files changed

+143
-34
lines changed

crates/uv/tests/it/common/mod.rs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,17 @@ impl TestContext {
154154
self
155155
}
156156

157-
/// Add extra standard filtering for executable suffixes on the current platform e.g.
158-
/// drops `.exe` on Windows.
157+
/// Add extra standard filtering for Python interpreter sources
159158
#[must_use]
160159
pub fn with_filtered_python_sources(mut self) -> Self {
160+
self.filters.push((
161+
"virtual environments, managed installations, or search path".to_string(),
162+
"[PYTHON SOURCES]".to_string(),
163+
));
164+
self.filters.push((
165+
"virtual environments, managed installations, search path, or registry".to_string(),
166+
"[PYTHON SOURCES]".to_string(),
167+
));
161168
self.filters.push((
162169
"managed installations or search path".to_string(),
163170
"[PYTHON SOURCES]".to_string(),
@@ -240,17 +247,11 @@ impl TestContext {
240247
#[must_use]
241248
pub fn with_managed_python_dirs(mut self) -> Self {
242249
let managed = self.temp_dir.join("managed");
243-
let bin = self.temp_dir.join("bin");
244250

245251
self.extra_env.push((
246-
EnvVars::PATH.into(),
247-
env::join_paths(std::iter::once(bin.clone()).chain(env::split_paths(
248-
&env::var(EnvVars::PATH).unwrap_or_default(),
249-
)))
250-
.unwrap(),
252+
EnvVars::UV_PYTHON_BIN_DIR.into(),
253+
self.bin_dir.as_os_str().to_owned(),
251254
));
252-
self.extra_env
253-
.push((EnvVars::UV_PYTHON_BIN_DIR.into(), bin.into()));
254255
self.extra_env
255256
.push((EnvVars::UV_PYTHON_INSTALL_DIR.into(), managed.into()));
256257
self.extra_env
@@ -360,6 +361,11 @@ impl TestContext {
360361
filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new()));
361362
}
362363

364+
filters.extend(
365+
Self::path_patterns(&bin_dir)
366+
.into_iter()
367+
.map(|pattern| (pattern, "[BIN]/".to_string())),
368+
);
363369
filters.extend(
364370
Self::path_patterns(&cache_dir)
365371
.into_iter()
@@ -524,9 +530,10 @@ impl TestContext {
524530
/// * Increase the stack size to avoid stack overflows on windows due to large async functions.
525531
pub fn add_shared_args(&self, command: &mut Command, activate_venv: bool) {
526532
// Push the test context bin to the front of the PATH
527-
let mut path = OsString::from(self.bin_dir.as_ref());
528-
path.push(if cfg!(windows) { ";" } else { ":" });
529-
path.push(env::var(EnvVars::PATH).unwrap_or_default());
533+
let path = env::join_paths(std::iter::once(self.bin_dir.to_path_buf()).chain(
534+
env::split_paths(&env::var(EnvVars::PATH).unwrap_or_default()),
535+
))
536+
.unwrap();
530537

531538
command
532539
.arg("--cache-dir")

crates/uv/tests/it/python_install.rs

Lines changed: 123 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,7 @@ fn python_install() {
2828
"###);
2929

3030
let bin_python = context
31-
.temp_dir
32-
.child("bin")
31+
.bin_dir
3332
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
3433

3534
// The executable should not be installed in the bin directory (requires preview)
@@ -92,6 +91,117 @@ fn python_install() {
9291
"###);
9392
}
9493

94+
#[test]
95+
fn python_install_automatic() {
96+
let context: TestContext = TestContext::new_with_versions(&[])
97+
.with_filtered_python_keys()
98+
.with_filtered_exe_suffix()
99+
.with_filtered_python_sources()
100+
.with_managed_python_dirs();
101+
102+
// With downloads disabled, the automatic install should fail
103+
uv_snapshot!(context.filters(), context.run()
104+
.env_remove("VIRTUAL_ENV")
105+
.arg("--no-python-downloads")
106+
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
107+
success: false
108+
exit_code: 2
109+
----- stdout -----
110+
111+
----- stderr -----
112+
error: No interpreter found in [PYTHON SOURCES]
113+
"###);
114+
115+
// Otherwise, we should fetch the latest Python version
116+
uv_snapshot!(context.filters(), context.run()
117+
.env_remove("VIRTUAL_ENV")
118+
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
119+
success: true
120+
exit_code: 0
121+
----- stdout -----
122+
(3, 13)
123+
124+
----- stderr -----
125+
"###);
126+
127+
// Subsequently, we can use the interpreter even with downloads disabled
128+
uv_snapshot!(context.filters(), context.run()
129+
.env_remove("VIRTUAL_ENV")
130+
.arg("--no-python-downloads")
131+
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
132+
success: true
133+
exit_code: 0
134+
----- stdout -----
135+
(3, 13)
136+
137+
----- stderr -----
138+
"###);
139+
140+
// We should respect the Python request
141+
uv_snapshot!(context.filters(), context.run()
142+
.env_remove("VIRTUAL_ENV")
143+
.arg("-p").arg("3.12")
144+
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
145+
success: true
146+
exit_code: 0
147+
----- stdout -----
148+
(3, 12)
149+
150+
----- stderr -----
151+
"###);
152+
153+
// But some requests cannot be mapped to a download
154+
uv_snapshot!(context.filters(), context.run()
155+
.env_remove("VIRTUAL_ENV")
156+
.arg("-p").arg("foobar")
157+
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
158+
success: false
159+
exit_code: 2
160+
----- stdout -----
161+
162+
----- stderr -----
163+
error: No interpreter found for executable name `foobar` in [PYTHON SOURCES]
164+
"###);
165+
166+
// Create a "broken" Python executable in the test context `bin`
167+
// (the snapshot is different on Windows so we just test on Unix)
168+
#[cfg(unix)]
169+
{
170+
use std::os::unix::fs::PermissionsExt;
171+
172+
let contents = r"#!/bin/sh
173+
echo 'error: intentionally broken python executable' >&2
174+
exit 1";
175+
let python = context
176+
.bin_dir
177+
.join(format!("python3{}", std::env::consts::EXE_SUFFIX));
178+
fs_err::write(&python, contents).unwrap();
179+
180+
let mut perms = fs_err::metadata(&python).unwrap().permissions();
181+
perms.set_mode(0o755);
182+
fs_err::set_permissions(&python, perms).unwrap();
183+
184+
// We should ignore the broken executable and download a version still
185+
uv_snapshot!(context.filters(), context.run()
186+
.env_remove("VIRTUAL_ENV")
187+
// In tests, we ignore `PATH` during Python discovery so we need to add the context `bin`
188+
.env("UV_TEST_PYTHON_PATH", context.bin_dir.as_os_str())
189+
.arg("-p").arg("3.11")
190+
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
191+
success: false
192+
exit_code: 2
193+
----- stdout -----
194+
195+
----- stderr -----
196+
error: Failed to inspect Python interpreter from search path at `[BIN]/python3`
197+
Caused by: Querying Python at `[BIN]/python3` failed with exit status exit status: 1
198+
199+
[stderr]
200+
error: intentionally broken python executable
201+
"###);
202+
}
203+
}
204+
95205
#[test]
96206
fn python_install_preview() {
97207
let context: TestContext = TestContext::new_with_versions(&[])
@@ -111,8 +221,7 @@ fn python_install_preview() {
111221
"###);
112222

113223
let bin_python = context
114-
.temp_dir
115-
.child("bin")
224+
.bin_dir
116225
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
117226

118227
// The executable should be installed in the bin directory
@@ -182,7 +291,7 @@ fn python_install_preview() {
182291
183292
----- stderr -----
184293
error: Failed to install cpython-3.13.1-[PLATFORM]
185-
Caused by: Executable already exists at `[TEMP_DIR]/bin/python3.13` but is not managed by uv; use `--force` to replace it
294+
Caused by: Executable already exists at `[BIN]/python3.13` but is not managed by uv; use `--force` to replace it
186295
"###);
187296

188297
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("--force").arg("3.13"), @r###"
@@ -243,8 +352,7 @@ fn python_install_preview() {
243352
"###);
244353

245354
let bin_python = context
246-
.temp_dir
247-
.child("bin")
355+
.bin_dir
248356
.child(format!("python3.12{}", std::env::consts::EXE_SUFFIX));
249357

250358
// The link should be for the newer patch version
@@ -275,8 +383,7 @@ fn python_install_preview_upgrade() {
275383
.with_managed_python_dirs();
276384

277385
let bin_python = context
278-
.temp_dir
279-
.child("bin")
386+
.bin_dir
280387
.child(format!("python3.12{}", std::env::consts::EXE_SUFFIX));
281388

282389
// Install 3.12.5
@@ -426,8 +533,7 @@ fn python_install_freethreaded() {
426533
"###);
427534

428535
let bin_python = context
429-
.temp_dir
430-
.child("bin")
536+
.bin_dir
431537
.child(format!("python3.13t{}", std::env::consts::EXE_SUFFIX));
432538

433539
// The executable should be installed in the bin directory
@@ -528,18 +634,15 @@ fn python_install_default() {
528634
.with_managed_python_dirs();
529635

530636
let bin_python_minor_13 = context
531-
.temp_dir
532-
.child("bin")
637+
.bin_dir
533638
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
534639

535640
let bin_python_major = context
536-
.temp_dir
537-
.child("bin")
641+
.bin_dir
538642
.child(format!("python3{}", std::env::consts::EXE_SUFFIX));
539643

540644
let bin_python_default = context
541-
.temp_dir
542-
.child("bin")
645+
.bin_dir
543646
.child(format!("python{}", std::env::consts::EXE_SUFFIX));
544647

545648
// `--preview` is required for `--default`
@@ -656,8 +759,7 @@ fn python_install_default() {
656759
"###);
657760

658761
let bin_python_minor_12 = context
659-
.temp_dir
660-
.child("bin")
762+
.bin_dir
661763
.child(format!("python3.12{}", std::env::consts::EXE_SUFFIX));
662764

663765
// All the executables should exist
@@ -857,10 +959,10 @@ fn python_install_preview_broken_link() {
857959
.with_filtered_exe_suffix()
858960
.with_managed_python_dirs();
859961

860-
let bin_python = context.temp_dir.child("bin").child("python3.13");
962+
let bin_python = context.bin_dir.child("python3.13");
861963

862964
// Create a broken symlink
863-
context.temp_dir.child("bin").create_dir_all().unwrap();
965+
context.bin_dir.create_dir_all().unwrap();
864966
symlink(context.temp_dir.join("does-not-exist"), &bin_python).unwrap();
865967

866968
// Install

0 commit comments

Comments
 (0)