uv/crates/uv/tests/venv.rs

645 lines
18 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#![cfg(feature = "python")]
use std::process::Command;
use std::{ffi::OsString, str::FromStr};
use anyhow::Result;
use assert_cmd::prelude::*;
use assert_fs::fixture::ChildPath;
use assert_fs::prelude::*;
#[cfg(windows)]
use uv_fs::Simplified;
use uv_toolchain::PythonVersion;
use crate::common::{get_bin, python_path_with_versions, uv_snapshot, TestContext, EXCLUDE_NEWER};
mod common;
struct VenvTestContext {
cache_dir: assert_fs::TempDir,
temp_dir: assert_fs::TempDir,
venv: ChildPath,
python_path: OsString,
python_versions: Vec<PythonVersion>,
}
impl VenvTestContext {
fn new(python_versions: &[&str]) -> Self {
let temp_dir = assert_fs::TempDir::new().unwrap();
let python_path = python_path_with_versions(&temp_dir, python_versions)
.expect("Failed to create Python test path");
// Canonicalize the virtual environment path for consistent snapshots across platforms
let venv = ChildPath::new(temp_dir.canonicalize().unwrap().join(".venv"));
let python_versions = python_versions
.iter()
.map(|version| {
PythonVersion::from_str(version).expect("Tests should use valid Python versions")
})
.collect::<Vec<_>>();
Self {
cache_dir: assert_fs::TempDir::new().unwrap(),
temp_dir,
venv,
python_path,
python_versions,
}
}
fn venv_command(&self) -> Command {
let mut command = Command::new(get_bin());
command
.arg("venv")
.arg("--cache-dir")
.arg(self.cache_dir.path())
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("UV_TEST_PYTHON_PATH", self.python_path.clone())
.env("UV_NO_WRAP", "1")
.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string())
.current_dir(self.temp_dir.as_os_str());
command
}
fn filters(&self) -> Vec<(String, String)> {
let mut filters = Vec::new();
filters.extend(
TestContext::path_patterns(&self.temp_dir)
.into_iter()
.map(|pattern| (pattern, "[TEMP_DIR]/".to_string())),
);
filters.push((
r"interpreter at: .+".to_string(),
"interpreter at: [PATH]".to_string(),
));
filters.push((
r"Activate with: (?:.*)\\Scripts\\activate".to_string(),
"Activate with: source .venv/bin/activate".to_string(),
));
// Add Python patch version filtering unless one was explicitly requested to ensure
// snapshots are patch version agnostic when it is not a part of the test.
if self
.python_versions
.iter()
.all(|version| version.patch().is_none())
{
for python_version in &self.python_versions {
filters.push((
format!(
r"({})\.\d+",
regex::escape(python_version.to_string().as_str())
),
"$1.[X]".to_string(),
));
}
}
filters
}
}
#[test]
fn create_venv() {
let context = VenvTestContext::new(&["3.12"]);
// Create a virtual environment at `.venv`.
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
context.venv.assert(predicates::path::is_dir());
// Create a virtual environment at the same location, which should replace it.
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
context.venv.assert(predicates::path::is_dir());
}
#[test]
fn create_venv_defaults_to_cwd() {
let context = VenvTestContext::new(&["3.12"]);
uv_snapshot!(context.filters(), context.venv_command()
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
context.venv.assert(predicates::path::is_dir());
}
#[test]
fn create_venv_ignores_virtual_env_variable() {
let context = VenvTestContext::new(&["3.12"]);
// We shouldn't care if `VIRTUAL_ENV` is set to an non-existant directory
// because we ignore virtual environment interpreter sources (we require a system interpreter)
uv_snapshot!(context.filters(), context.venv_command()
.env("VIRTUAL_ENV", context.temp_dir.child("does-not-exist").as_os_str()), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
}
#[test]
fn seed() {
let context = VenvTestContext::new(&["3.12"]);
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--seed")
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
+ pip==24.0
Activate with: source .venv/bin/activate
"###
);
context.venv.assert(predicates::path::is_dir());
}
#[test]
fn seed_older_python_version() {
let context = VenvTestContext::new(&["3.10"]);
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--seed")
.arg("--python")
.arg("3.10"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.10.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
+ pip==24.0
+ setuptools==69.2.0
+ wheel==0.43.0
Activate with: source .venv/bin/activate
"###
);
context.venv.assert(predicates::path::is_dir());
}
#[test]
fn create_venv_unknown_python_minor() {
let context = VenvTestContext::new(&["3.12"]);
let mut command = context.venv_command();
command
.arg(context.venv.as_os_str())
// Request a version we know we'll never see
.arg("--python")
.arg("3.100")
// Unset this variable to force what the user would see
.env_remove("UV_TEST_PYTHON_PATH");
if cfg!(windows) {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.100 in search path or `py` launcher output
"###
);
} else {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.100 in search path
"###
);
}
context.venv.assert(predicates::path::missing());
}
#[test]
fn create_venv_unknown_python_patch() {
let context = VenvTestContext::new(&["3.12"]);
let mut command = context.venv_command();
command
.arg(context.venv.as_os_str())
// Request a version we know we'll never see
.arg("--python")
.arg("3.12.100")
// Unset this variable to force what the user would see
.env_remove("UV_TEST_PYTHON_PATH");
if cfg!(windows) {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.12.100 in search path or `py` launcher output
"###
);
} else {
uv_snapshot!(&mut command, @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
× No interpreter found for Python 3.12.100 in search path
"###
);
}
context.venv.assert(predicates::path::missing());
}
#[cfg(feature = "python-patch")]
#[test]
fn create_venv_python_patch() {
let context = VenvTestContext::new(&["3.12.1"]);
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12.1"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.1 interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
context.venv.assert(predicates::path::is_dir());
}
#[test]
fn file_exists() -> Result<()> {
let context = VenvTestContext::new(&["3.12"]);
// Create a file at `.venv`. Creating a virtualenv at the same path should fail.
context.venv.touch()?;
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
uv::venv::creation
× Failed to create virtualenv
╰─▶ File exists at `.venv`
"###
);
Ok(())
}
#[test]
fn empty_dir_exists() -> Result<()> {
let context = VenvTestContext::new(&["3.12"]);
// Create an empty directory at `.venv`. Creating a virtualenv at the same path should succeed.
context.venv.create_dir_all()?;
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
context.venv.assert(predicates::path::is_dir());
Ok(())
}
#[test]
fn non_empty_dir_exists() -> Result<()> {
let context = VenvTestContext::new(&["3.12"]);
// Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should fail.
context.venv.create_dir_all()?;
context.venv.child("file").touch()?;
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
uv::venv::creation
× Failed to create virtualenv
╰─▶ The directory `.venv` exists, but it's not a virtualenv
"###
);
Ok(())
}
#[test]
fn non_empty_dir_exists_allow_existing() -> Result<()> {
let context = VenvTestContext::new(&["3.12"]);
// Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should
// succeed when `--allow-existing` is specified, but fail when it is not.
context.venv.create_dir_all()?;
context.venv.child("file").touch()?;
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
uv::venv::creation
× Failed to create virtualenv
╰─▶ The directory `.venv` exists, but it's not a virtualenv
"###
);
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--allow-existing")
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
// Running again should _also_ succeed, overwriting existing symlinks and respecting existing
// directories.
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--allow-existing")
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
Ok(())
}
#[test]
#[cfg(windows)]
fn windows_shims() -> Result<()> {
let context = VenvTestContext::new(&["3.9", "3.8"]);
let shim_path = context.temp_dir.child("shim");
let py38 = std::env::split_paths(&context.python_path)
.last()
.expect("python_path_with_versions to set up the python versions");
// We want 3.8 and the first version should be 3.9.
// Picking the last is necessary to prove that shims work because the python version selects
// the python version from the first path segment by default, so we take the last to prove it's not
// returning that version.
assert!(py38.to_str().unwrap().contains("3.8"));
// Write the shim script that forwards the arguments to the python3.8 installation.
fs_err::create_dir(&shim_path)?;
fs_err::write(
shim_path.child("python.bat"),
format!("@echo off\r\n{}/python.exe %*", py38.display()),
)?;
// Create a virtual environment at `.venv`, passing the redundant `--clear` flag.
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--clear")
.env("UV_TEST_PYTHON_PATH", format!("{};{}", shim_path.display(), context.python_path.simplified_display())), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: virtualenv's `--clear` has no effect (uv always clears the virtual environment).
Using Python 3.8.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
context.venv.assert(predicates::path::is_dir());
Ok(())
}
#[test]
fn virtualenv_compatibility() {
let context = VenvTestContext::new(&["3.12"]);
// Create a virtual environment at `.venv`, passing the redundant `--clear` flag.
uv_snapshot!(context.filters(), context.venv_command()
.arg(context.venv.as_os_str())
.arg("--clear")
.arg("--python")
.arg("3.12"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: virtualenv's `--clear` has no effect (uv always clears the virtual environment).
Using Python 3.12.[X] interpreter at: [PATH]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
);
context.venv.assert(predicates::path::is_dir());
}
#[test]
fn verify_pyvenv_cfg() {
let context = TestContext::new("3.12");
let venv = context.temp_dir.child(".venv");
let pyvenv_cfg = venv.child("pyvenv.cfg");
venv.assert(predicates::path::is_dir());
// Check pyvenv.cfg exists
pyvenv_cfg.assert(predicates::path::is_file());
// Check if "uv = version" is present in the file
let version = env!("CARGO_PKG_VERSION").to_string();
let search_string = format!("uv = {version}");
pyvenv_cfg.assert(predicates::str::contains(search_string));
}
/// Ensure that a nested virtual environment uses the same `home` directory as the parent.
#[test]
fn verify_nested_pyvenv_cfg() -> Result<()> {
let context = VenvTestContext::new(&["3.12"]);
// Create a virtual environment at `.venv`.
context
.venv_command()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12")
.assert()
.success();
let pyvenv_cfg = context.venv.child("pyvenv.cfg");
// Check pyvenv.cfg exists
pyvenv_cfg.assert(predicates::path::is_file());
// Extract the "home" line from the pyvenv.cfg file.
let contents = fs_err::read_to_string(pyvenv_cfg.path())?;
let venv_home = contents
.lines()
.find(|line| line.starts_with("home"))
.expect("home line not found");
// Now, create a virtual environment from within the virtual environment.
let subvenv = context.temp_dir.child(".subvenv");
context
.venv_command()
.arg(subvenv.as_os_str())
.arg("--python")
.arg("3.12")
.env("VIRTUAL_ENV", context.venv.as_os_str())
.assert()
.success();
let sub_pyvenv_cfg = subvenv.child("pyvenv.cfg");
// Extract the "home" line from the pyvenv.cfg file.
let contents = fs_err::read_to_string(sub_pyvenv_cfg.path())?;
let sub_venv_home = contents
.lines()
.find(|line| line.starts_with("home"))
.expect("home line not found");
// Check that both directories point to the same home.
assert_eq!(sub_venv_home, venv_home);
Ok(())
}
/// See <https://github.com/astral-sh/uv/issues/3280>
#[test]
#[cfg(windows)]
fn path_with_trailing_space_gives_proper_error() {
let context = VenvTestContext::new(&["3.12"]);
let mut filters = context.filters();
filters.push((
regex::escape(&context.cache_dir.path().display().to_string()).to_string(),
r"C:\Path\to\Cache\dir".to_string(),
));
// Create a virtual environment at `.venv`.
uv_snapshot!(filters, Command::new(get_bin())
.arg("venv")
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12")
.env("UV_CACHE_DIR", format!("{} ", context.cache_dir.path().display()))
.env("UV_TEST_PYTHON_PATH", context.python_path.clone())
.current_dir(context.temp_dir.path()), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: failed to open file `C:\Path\to\Cache\dir \CACHEDIR.TAG`
Caused by: The system cannot find the path specified. (os error 3)
"###
);
}