Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is extra clean-up, I could split into a separate change but it's not particularly important.

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
Loading