From e9043f2f9f8954117f9f77e3b1a6caec9367449f Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 23 Jan 2025 15:06:27 -0600 Subject: [PATCH] Add test case for automatic installs --- crates/uv/tests/it/common/mod.rs | 33 +++--- crates/uv/tests/it/python_install.rs | 144 +++++++++++++++++++++++---- 2 files changed, 143 insertions(+), 34 deletions(-) diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index 24c0fb61e516..ae5fbab48ad8 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -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(), @@ -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 @@ -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() @@ -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") diff --git a/crates/uv/tests/it/python_install.rs b/crates/uv/tests/it/python_install.rs index ec2e8ed8fce2..33d05733b97d 100644 --- a/crates/uv/tests/it/python_install.rs +++ b/crates/uv/tests/it/python_install.rs @@ -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) @@ -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(&[]) @@ -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 @@ -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###" @@ -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 @@ -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 @@ -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 @@ -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` @@ -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 @@ -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