Skip to content

Commit

Permalink
Add test case for automatic installs (#10913)
Browse files Browse the repository at this point in the history
  • Loading branch information
zanieb authored Jan 23, 2025
1 parent 10654cb commit 2a0fa8a
Show file tree
Hide file tree
Showing 2 changed files with 143 additions and 34 deletions.
33 changes: 20 additions & 13 deletions crates/uv/tests/it/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,17 @@ impl TestContext {
self
}

/// Add extra standard filtering for executable suffixes on the current platform e.g.
/// drops `.exe` on Windows.
/// Add extra standard filtering for Python interpreter sources
#[must_use]
pub fn with_filtered_python_sources(mut self) -> Self {
self.filters.push((
"virtual environments, managed installations, or search path".to_string(),
"[PYTHON SOURCES]".to_string(),
));
self.filters.push((
"virtual environments, managed installations, search path, or registry".to_string(),
"[PYTHON SOURCES]".to_string(),
));
self.filters.push((
"managed installations or search path".to_string(),
"[PYTHON SOURCES]".to_string(),
Expand Down Expand Up @@ -240,17 +247,11 @@ impl TestContext {
#[must_use]
pub fn with_managed_python_dirs(mut self) -> Self {
let managed = self.temp_dir.join("managed");
let bin = self.temp_dir.join("bin");

self.extra_env.push((
EnvVars::PATH.into(),
env::join_paths(std::iter::once(bin.clone()).chain(env::split_paths(
&env::var(EnvVars::PATH).unwrap_or_default(),
)))
.unwrap(),
EnvVars::UV_PYTHON_BIN_DIR.into(),
self.bin_dir.as_os_str().to_owned(),
));
self.extra_env
.push((EnvVars::UV_PYTHON_BIN_DIR.into(), bin.into()));
self.extra_env
.push((EnvVars::UV_PYTHON_INSTALL_DIR.into(), managed.into()));
self.extra_env
Expand Down Expand Up @@ -360,6 +361,11 @@ impl TestContext {
filters.push((r#"link-mode = "copy"\n"#.to_string(), String::new()));
}

filters.extend(
Self::path_patterns(&bin_dir)
.into_iter()
.map(|pattern| (pattern, "[BIN]/".to_string())),
);
filters.extend(
Self::path_patterns(&cache_dir)
.into_iter()
Expand Down Expand Up @@ -524,9 +530,10 @@ impl TestContext {
/// * Increase the stack size to avoid stack overflows on windows due to large async functions.
pub fn add_shared_args(&self, command: &mut Command, activate_venv: bool) {
// Push the test context bin to the front of the PATH
let mut path = OsString::from(self.bin_dir.as_ref());
path.push(if cfg!(windows) { ";" } else { ":" });
path.push(env::var(EnvVars::PATH).unwrap_or_default());
let path = env::join_paths(std::iter::once(self.bin_dir.to_path_buf()).chain(
env::split_paths(&env::var(EnvVars::PATH).unwrap_or_default()),
))
.unwrap();

command
.arg("--cache-dir")
Expand Down
144 changes: 123 additions & 21 deletions crates/uv/tests/it/python_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ fn python_install() {
"###);

let bin_python = context
.temp_dir
.child("bin")
.bin_dir
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));

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

#[test]
fn python_install_automatic() {
let context: TestContext = TestContext::new_with_versions(&[])
.with_filtered_python_keys()
.with_filtered_exe_suffix()
.with_filtered_python_sources()
.with_managed_python_dirs();

// With downloads disabled, the automatic install should fail
uv_snapshot!(context.filters(), context.run()
.env_remove("VIRTUAL_ENV")
.arg("--no-python-downloads")
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No interpreter found in [PYTHON SOURCES]
"###);

// Otherwise, we should fetch the latest Python version
uv_snapshot!(context.filters(), context.run()
.env_remove("VIRTUAL_ENV")
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
success: true
exit_code: 0
----- stdout -----
(3, 13)
----- stderr -----
"###);

// Subsequently, we can use the interpreter even with downloads disabled
uv_snapshot!(context.filters(), context.run()
.env_remove("VIRTUAL_ENV")
.arg("--no-python-downloads")
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
success: true
exit_code: 0
----- stdout -----
(3, 13)
----- stderr -----
"###);

// We should respect the Python request
uv_snapshot!(context.filters(), context.run()
.env_remove("VIRTUAL_ENV")
.arg("-p").arg("3.12")
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
success: true
exit_code: 0
----- stdout -----
(3, 12)
----- stderr -----
"###);

// But some requests cannot be mapped to a download
uv_snapshot!(context.filters(), context.run()
.env_remove("VIRTUAL_ENV")
.arg("-p").arg("foobar")
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No interpreter found for executable name `foobar` in [PYTHON SOURCES]
"###);

// Create a "broken" Python executable in the test context `bin`
// (the snapshot is different on Windows so we just test on Unix)
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;

let contents = r"#!/bin/sh
echo 'error: intentionally broken python executable' >&2
exit 1";
let python = context
.bin_dir
.join(format!("python3{}", std::env::consts::EXE_SUFFIX));
fs_err::write(&python, contents).unwrap();

let mut perms = fs_err::metadata(&python).unwrap().permissions();
perms.set_mode(0o755);
fs_err::set_permissions(&python, perms).unwrap();

// We should ignore the broken executable and download a version still
uv_snapshot!(context.filters(), context.run()
.env_remove("VIRTUAL_ENV")
// In tests, we ignore `PATH` during Python discovery so we need to add the context `bin`
.env("UV_TEST_PYTHON_PATH", context.bin_dir.as_os_str())
.arg("-p").arg("3.11")
.arg("python").arg("-c").arg("import sys; print(sys.version_info[:2])"), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: Failed to inspect Python interpreter from search path at `[BIN]/python3`
Caused by: Querying Python at `[BIN]/python3` failed with exit status exit status: 1
[stderr]
error: intentionally broken python executable
"###);
}
}

#[test]
fn python_install_preview() {
let context: TestContext = TestContext::new_with_versions(&[])
Expand All @@ -111,8 +221,7 @@ fn python_install_preview() {
"###);

let bin_python = context
.temp_dir
.child("bin")
.bin_dir
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));

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

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

let bin_python = context
.temp_dir
.child("bin")
.bin_dir
.child(format!("python3.12{}", std::env::consts::EXE_SUFFIX));

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

let bin_python = context
.temp_dir
.child("bin")
.bin_dir
.child(format!("python3.12{}", std::env::consts::EXE_SUFFIX));

// Install 3.12.5
Expand Down Expand Up @@ -426,8 +533,7 @@ fn python_install_freethreaded() {
"###);

let bin_python = context
.temp_dir
.child("bin")
.bin_dir
.child(format!("python3.13t{}", std::env::consts::EXE_SUFFIX));

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

let bin_python_minor_13 = context
.temp_dir
.child("bin")
.bin_dir
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));

let bin_python_major = context
.temp_dir
.child("bin")
.bin_dir
.child(format!("python3{}", std::env::consts::EXE_SUFFIX));

let bin_python_default = context
.temp_dir
.child("bin")
.bin_dir
.child(format!("python{}", std::env::consts::EXE_SUFFIX));

// `--preview` is required for `--default`
Expand Down Expand Up @@ -656,8 +759,7 @@ fn python_install_default() {
"###);

let bin_python_minor_12 = context
.temp_dir
.child("bin")
.bin_dir
.child(format!("python3.12{}", std::env::consts::EXE_SUFFIX));

// All the executables should exist
Expand Down Expand Up @@ -857,10 +959,10 @@ fn python_install_preview_broken_link() {
.with_filtered_exe_suffix()
.with_managed_python_dirs();

let bin_python = context.temp_dir.child("bin").child("python3.13");
let bin_python = context.bin_dir.child("python3.13");

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

// Install
Expand Down

0 comments on commit 2a0fa8a

Please sign in to comment.