Move virtual environment test context into main context (#4370)

It was becoming problematic that the virtual environment test context
diverged from the other one i.e. we had to implement filtering twice.
This combines the contexts and tweaks the `TestContext` API and
filtering mechanisms for Python versions. Combined with my previous
changes to the test context at #4364 and
https://github.com/astral-sh/uv/pull/4368 this finally unblocks the
snapshots for test cases in #4360 and
https://github.com/astral-sh/uv/pull/4362.
This commit is contained in:
Zanie Blue 2024-06-18 10:11:47 -04:00 committed by GitHub
parent a193834813
commit 76c26db444
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 229 additions and 323 deletions

View file

@ -168,7 +168,7 @@ jobs:
steps:
- name: Create Dev Drive using ReFS
run: |
$Volume = New-VHD -Path C:/uv_dev_drive.vhdx -SizeBytes 10GB |
$Volume = New-VHD -Path C:/uv_dev_drive.vhdx -SizeBytes 12GB |
Mount-VHD -Passthru |
Initialize-Disk -Passthru |
New-Partition -AssignDriveLetter -UseMaximumSize |

View file

@ -5,9 +5,9 @@ use assert_cmd::assert::{Assert, OutputAssertExt};
use assert_cmd::Command;
use assert_fs::assert::PathAssert;
use assert_fs::fixture::{ChildPath, PathChild};
use predicates::prelude::predicate;
use regex::Regex;
use std::borrow::BorrowMut;
use std::collections::VecDeque;
use std::env;
use std::ffi::OsString;
use std::iter::Iterator;
@ -54,28 +54,55 @@ pub const INSTA_FILTERS: &[(&str, &str)] = &[
/// * Set the current directory to a temporary directory (`temp_dir`).
/// * Set the cache dir to a different temporary directory (`cache_dir`).
/// * Set a cutoff for versions used in the resolution so the snapshots don't change after a new release.
/// * Set the venv to a fresh `.venv` in `temp_dir`.
#[derive(Debug)]
/// * Set the venv to a fresh `.venv` in `temp_dir`
pub struct TestContext {
pub temp_dir: assert_fs::TempDir,
pub cache_dir: assert_fs::TempDir,
pub venv: PathBuf,
pub python_version: String,
pub venv: ChildPath,
pub workspace_root: PathBuf,
// Additional Python versions
python_versions: Vec<(PythonVersion, PathBuf)>,
/// The Python version used for the virtual environment, if any.
pub python_version: Option<PythonVersion>,
// All the Python versions available during this test context.
pub python_versions: Vec<(PythonVersion, PathBuf)>,
// Standard filters for this test context
filters: VecDeque<(String, String)>,
filters: Vec<(String, String)>,
}
impl TestContext {
/// Create a new test context with a virtual environment.
///
/// See [`TestContext::new_with_versions`] if multiple versions are needed or
/// if creation of the virtual environment should be deferred.
pub fn new(python_version: &str) -> Self {
let new = Self::new_with_versions(&[python_version]);
new.create_venv();
new
}
/// Create a new test context with multiple Python versions.
///
/// Does not create a virtual environment by default, but the first Python version
/// can be used to create a virtual environment with [`TestContext::create_venv`].
///
/// See [`TestContext::new`] if only a single version is desired.
pub fn new_with_versions(python_versions: &[&str]) -> Self {
let temp_dir = assert_fs::TempDir::new().expect("Failed to create temp dir");
let cache_dir = assert_fs::TempDir::new().expect("Failed to create cache dir");
let python = get_toolchain(python_version);
let venv = create_venv_from_executable(&temp_dir, &cache_dir, &python);
// Canonicalize the temp dir for consistent snapshot behavior
let canonical_temp_dir = temp_dir.canonicalize().unwrap();
let venv = ChildPath::new(canonical_temp_dir.join(".venv"));
let python_version = python_versions
.first()
.map(|version| PythonVersion::from_str(version).unwrap());
let site_packages = python_version
.as_ref()
.map(|version| site_packages_path(&venv, &format!("python{version}")));
// The workspace root directory is not available without walking up the tree
// https://github.com/rust-lang/cargo/issues/3946
@ -86,28 +113,51 @@ impl TestContext {
.expect("CARGO_MANIFEST_DIR should be doubly nested in workspace")
.to_path_buf();
let site_packages = site_packages_path(&venv, &format!("python{python_version}"));
let python_versions: Vec<_> = python_versions
.iter()
.map(|version| PythonVersion::from_str(version).unwrap())
.zip(
python_toolchains_for_versions(&temp_dir, python_versions)
.expect("Failed to find test Python versions"),
)
.collect();
let python_version =
PythonVersion::from_str(python_version).expect("Tests must use valid Python versions");
let mut filters = VecDeque::new();
let mut filters = Vec::new();
filters.extend(
Self::path_patterns(&cache_dir)
.into_iter()
.map(|pattern| (pattern, "[CACHE_DIR]/".to_string())),
);
filters.extend(
Self::path_patterns(&site_packages)
.into_iter()
.map(|pattern| (pattern, "[SITE_PACKAGES]/".to_string())),
);
if let Some(ref site_packages) = site_packages {
filters.extend(
Self::path_patterns(site_packages)
.into_iter()
.map(|pattern| (pattern, "[SITE_PACKAGES]/".to_string())),
);
}
filters.extend(
Self::path_patterns(&venv)
.into_iter()
.map(|pattern| (pattern, "[VENV]/".to_string())),
);
for (version, executable) in &python_versions {
// Add filtering for the interpreter path
filters.extend(
Self::path_patterns(executable)
.into_iter()
.map(|pattern| (format!("{pattern}python.*"), format!("[PYTHON-{version}]"))),
);
// Add Python patch version filtering unless explicitly requested to ensure
// snapshots are patch version agnostic when it is not a part of the test.
if version.patch().is_none() {
filters.push((
format!(r"({})\.\d+", regex::escape(version.to_string().as_str())),
"$1.[X]".to_string(),
));
}
}
filters.extend(
Self::path_patterns(&temp_dir)
.into_iter()
@ -119,86 +169,42 @@ impl TestContext {
.map(|pattern| (pattern, "[WORKSPACE]/".to_string())),
);
// Make virtual environment activation cross-platform
filters.push((
r"Activate with: (?:.*)\\Scripts\\activate".to_string(),
"Activate with: source .venv/bin/activate".to_string(),
));
// Account for [`Simplified::user_display`] which is relative to the command working directory
filters.push_back((
Self::path_pattern(
site_packages
.strip_prefix(&temp_dir)
.expect("The test site-packages directory is always in the tempdir"),
),
"[SITE_PACKAGES]/".to_string(),
));
filters.push_back((
Self::path_pattern(
venv.strip_prefix(&temp_dir)
.expect("The test virtual environment directory is always in the tempdir"),
),
"[VENV]/".to_string(),
));
if let Some(site_packages) = site_packages {
filters.push((
Self::path_pattern(
site_packages
.strip_prefix(&canonical_temp_dir)
.expect("The test site-packages directory is always in the tempdir"),
),
"[SITE_PACKAGES]/".to_string(),
));
};
// Filter non-deterministic temporary directory names
// Note we apply this _after_ all the full paths to avoid breaking their matching
filters.push_back((r"(\\|\/)\.tmp.*(\\|\/)".to_string(), "/[TMP]/".to_string()));
filters.push((r"(\\|\/)\.tmp.*(\\|\/)".to_string(), "/[TMP]/".to_string()));
// Account for platform prefix differences `file://` (Unix) vs `file:///` (Windows)
filters.push_back((r"file:///".to_string(), "file://".to_string()));
filters.push((r"file:///".to_string(), "file://".to_string()));
// Destroy any remaining UNC prefixes (Windows only)
filters.push_back((r"\\\\\?\\".to_string(), String::new()));
filters.push((r"\\\\\?\\".to_string(), String::new()));
let mut result = Self {
Self {
temp_dir,
cache_dir,
venv,
python_version: python_version.to_string(),
filters,
python_versions: Vec::new(),
workspace_root,
};
result.add_filters_for_python_version(&python_version, python);
result
}
pub fn new_with_versions(python_versions: &[&str]) -> Self {
let mut context = Self::new(
python_versions
.first()
.expect("At least one test Python version must be provided"),
);
let python_versions: Vec<_> = python_versions
.iter()
.map(|version| PythonVersion::from_str(version).unwrap())
.zip(
python_toolchains_for_versions(&context.temp_dir, python_versions)
.expect("Failed to find test Python versions"),
)
.collect();
for (version, path) in &python_versions {
context.add_filters_for_python_version(version, path.clone());
}
context.python_versions = python_versions;
context
}
fn add_filters_for_python_version(&mut self, version: &PythonVersion, executable: PathBuf) {
// Add filtering for the interpreter path
for pattern in Self::path_patterns(executable) {
self.filters
.push_front((format!("{pattern}python.*"), format!("[PYTHON-{version}]")));
}
// Add Python patch version filtering unless explicitly requested to ensure
// snapshots are patch version agnostic when it is not a part of the test.
if version.patch().is_none() {
self.filters.push_back((
format!(r"({})\.\d+", regex::escape(version.to_string().as_str())),
"$1.[X]".to_string(),
));
python_version,
python_versions,
filters,
}
}
@ -223,6 +229,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(self.temp_dir.path());
@ -235,6 +242,22 @@ impl TestContext {
cmd
}
/// Create a `uv venv` command
pub fn venv(&self) -> std::process::Command {
let mut command = std::process::Command::new(get_bin());
command
.arg("venv")
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("UV_CACHE_DIR", self.cache_dir.path())
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.env("UV_NO_WRAP", "1")
.env("UV_STACK_SIZE", (2 * 1024 * 1024).to_string())
.current_dir(self.temp_dir.as_os_str());
command
}
/// Create a `pip install` command with options shared across scenarios.
pub fn install(&self) -> std::process::Command {
let mut command = self.install_without_exclude_newer();
@ -257,6 +280,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(&self.temp_dir);
@ -277,8 +301,10 @@ impl TestContext {
.arg("--cache-dir")
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.env("UV_NO_WRAP", "1")
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(&self.temp_dir);
@ -297,6 +323,7 @@ impl TestContext {
command
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path());
command
}
@ -315,6 +342,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(&self.temp_dir);
@ -327,10 +355,6 @@ impl TestContext {
command
}
pub fn toolchains_dir(&self) -> ChildPath {
self.temp_dir.child("toolchains")
}
/// Create a `uv toolchain find` command with options shared across scenarios.
pub fn toolchain_find(&self) -> std::process::Command {
let mut command = std::process::Command::new(get_bin());
@ -341,9 +365,10 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.env("UV_PREVIEW", "1")
.env("UV_TOOLCHAIN_DIR", self.toolchains_dir().as_os_str())
.env("UV_TOOLCHAIN_DIR", "")
.current_dir(&self.temp_dir);
if cfg!(all(windows, debug_assertions)) {
@ -376,6 +401,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TOOLCHAIN_DIR", "")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(&self.temp_dir);
@ -497,7 +523,7 @@ impl TestContext {
)
}
fn python_path(&self) -> OsString {
pub fn python_path(&self) -> OsString {
std::env::join_paths(self.python_versions.iter().map(|(_, path)| path)).unwrap()
}
@ -522,33 +548,29 @@ impl TestContext {
pub fn site_packages(&self) -> PathBuf {
site_packages_path(
&self.venv,
&format!("{}{}", self.python_kind(), self.python_version),
&format!(
"{}{}",
self.python_kind(),
self.python_version.as_ref().expect(
"A Python version must be provided to retrieve the test site packages path"
)
),
)
}
/// Reset the virtual environment in the test context.
pub fn reset_venv(&self) {
create_venv_from_executable(
&self.temp_dir,
&self.cache_dir,
&get_toolchain(&self.python_version),
);
self.create_venv();
}
/// Create a new virtual environment named `.venv` in the test context.
fn create_venv(&mut self, python: &str) -> PathBuf {
let parent = self.temp_dir.to_path_buf();
self.create_venv_in_parent(parent, python)
}
/// Create a new virtual environment named `.venv` in the given directory.
fn create_venv_in_parent<P: AsRef<Path>>(&mut self, path: P, python: &str) -> PathBuf {
let executable = get_toolchain(python);
self.add_filters_for_python_version(
&PythonVersion::from_str(python).unwrap(),
executable.clone(),
fn create_venv(&self) {
let executable = get_toolchain(
self.python_version
.as_ref()
.expect("A Python version must be provided to create a test virtual environment"),
);
create_venv_from_executable(&ChildPath::new(path.as_ref()), &self.cache_dir, &executable)
create_venv_from_executable(&self.venv, &self.cache_dir, &executable);
}
}
@ -562,11 +584,11 @@ pub fn site_packages_path(venv: &Path, python: &str) -> PathBuf {
}
}
pub fn venv_bin_path(venv: &Path) -> PathBuf {
pub fn venv_bin_path<P: AsRef<Path>>(venv: &P) -> PathBuf {
if cfg!(unix) {
venv.join("bin")
venv.as_ref().join("bin")
} else if cfg!(windows) {
venv.join("Scripts")
venv.as_ref().join("Scripts")
} else {
unimplemented!("Only Windows and Unix are supported")
}
@ -583,14 +605,11 @@ pub fn venv_to_interpreter(venv: &Path) -> PathBuf {
}
/// Get the path to the python interpreter for a specific toolchain version.
pub fn get_toolchain(python: &str) -> PathBuf {
pub fn get_toolchain(version: &PythonVersion) -> PathBuf {
InstalledToolchains::from_settings()
.map(|installed_toolchains| {
installed_toolchains
.find_version(
&PythonVersion::from_str(python)
.expect("Tests should use a valid Python version"),
)
.find_version(version)
.expect("Tests are run on a supported platform")
.next()
.as_ref()
@ -599,31 +618,26 @@ pub fn get_toolchain(python: &str) -> PathBuf {
// We'll search for the request Python on the PATH if not found in the toolchain versions
// We hack this into a `PathBuf` to satisfy the compiler but it's just a string
.unwrap_or_default()
.unwrap_or(PathBuf::from(python))
.unwrap_or(PathBuf::from(version.to_string()))
}
/// Create a virtual environment named `.venv` in a temporary directory with the given
/// Python executable.
pub fn create_venv_from_executable<
Parent: assert_fs::prelude::PathChild + AsRef<std::path::Path>,
>(
temp_dir: &Parent,
/// Create a virtual environment at the given path.
pub fn create_venv_from_executable<P: AsRef<std::path::Path>>(
path: P,
cache_dir: &assert_fs::TempDir,
python: &Path,
) -> PathBuf {
let venv = temp_dir.child(".venv");
) {
Command::new(get_bin())
.arg("venv")
.arg(venv.as_os_str())
.arg(path.as_ref().as_os_str())
.arg("--cache-dir")
.arg(cache_dir.path())
.arg("--python")
.arg(python)
.current_dir(temp_dir)
.current_dir(path.as_ref().parent().unwrap())
.assert()
.success();
venv.assert(predicates::path::is_dir());
venv.to_path_buf()
ChildPath::new(path.as_ref()).assert(predicate::path::is_dir());
}
/// Returns the uv binary that cargo built before launching the tests.

View file

@ -1894,7 +1894,7 @@ fn lock_requires_python() -> Result<()> {
----- stderr -----
warning: `uv sync` is experimental and may change without warning.
Removing virtual environment at: [VENV]/
Removing virtual environment at: .venv
error: No interpreter found for Python >=3.12 in provided path, active virtual environment, or search path
"###);

View file

@ -127,24 +127,23 @@ fn missing_requirements_in() {
fn missing_venv() -> Result<()> {
let context = TestContext::new("3.12");
context.temp_dir.child("requirements.in").touch()?;
fs_err::remove_dir_all(context.temp_dir.child(".venv").path())?;
fs_err::remove_dir_all(context.venv.path())?;
uv_snapshot!(context.filters(), context.compile()
.arg("requirements.in"), @r###"
success: false
exit_code: 2
success: true
exit_code: 0
----- stdout -----
# This file was autogenerated by uv via the following command:
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in
----- stderr -----
warning: Requirements file requirements.in does not contain any dependencies
error: No Python interpreters found in provided path, active virtual environment, or search path
Resolved 0 packages in [TIME]
"###
);
context
.temp_dir
.child(".venv")
.assert(predicates::path::missing());
context.venv.assert(predicates::path::missing());
Ok(())
}

View file

@ -3954,7 +3954,7 @@ fn install_package_basic_auth_from_keyring() {
.arg("subprocess")
.arg("--strict")
.env("KEYRING_TEST_CREDENTIALS", r#"{"pypi-proxy.fly.dev": {"public": "heron"}}"#)
.env("PATH", venv_bin_path(context.venv.as_path())), @r###"
.env("PATH", venv_bin_path(&context.venv)), @r###"
success: true
exit_code: 0
----- stdout -----
@ -4001,7 +4001,7 @@ fn install_package_basic_auth_from_keyring_wrong_password() {
.arg("subprocess")
.arg("--strict")
.env("KEYRING_TEST_CREDENTIALS", r#"{"pypi-proxy.fly.dev": {"public": "foobar"}}"#)
.env("PATH", venv_bin_path(context.venv.as_path())), @r###"
.env("PATH", venv_bin_path(&context.venv)), @r###"
success: false
exit_code: 2
----- stdout -----
@ -4042,7 +4042,7 @@ fn install_package_basic_auth_from_keyring_wrong_username() {
.arg("subprocess")
.arg("--strict")
.env("KEYRING_TEST_CREDENTIALS", r#"{"pypi-proxy.fly.dev": {"other": "heron"}}"#)
.env("PATH", venv_bin_path(context.venv.as_path())), @r###"
.env("PATH", venv_bin_path(&context.venv)), @r###"
success: false
exit_code: 2
----- stdout -----

View file

@ -51,6 +51,8 @@ fn run_with_python_version() -> Result<()> {
3.7.0
----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Resolved 5 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]
@ -100,9 +102,9 @@ fn run_with_python_version() -> Result<()> {
3.6.0
----- stderr -----
Removing virtual environment at: [VENV]/
Removing virtual environment at: .venv
Using Python 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtualenv at: [VENV]/
Creating virtualenv at: .venv
Resolved 5 packages in [TIME]
Downloaded 4 packages in [TIME]
Installed 4 packages in [TIME]

View file

@ -1,117 +1,19 @@
#![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};
use crate::common::{uv_snapshot, TestContext};
mod common;
struct VenvTestContext {
cache_dir: assert_fs::TempDir,
temp_dir: assert_fs::TempDir,
venv: ChildPath,
toolchain_dir: 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");
let toolchain_dir = temp_dir.child("toolchains");
toolchain_dir.create_dir_all().unwrap();
// 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,
toolchain_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_TOOLCHAIN_DIR", self.toolchain_dir.as_os_str())
.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"]);
let context = TestContext::new_with_versions(&["3.12"]);
// Create a virtual environment at `.venv`.
uv_snapshot!(context.filters(), context.venv_command()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
@ -120,7 +22,7 @@ fn create_venv() {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -129,7 +31,7 @@ fn create_venv() {
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()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
@ -138,7 +40,7 @@ fn create_venv() {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -149,8 +51,8 @@ fn create_venv() {
#[test]
fn create_venv_defaults_to_cwd() {
let context = VenvTestContext::new(&["3.12"]);
uv_snapshot!(context.filters(), context.venv_command()
let context = TestContext::new_with_versions(&["3.12"]);
uv_snapshot!(context.filters(), context.venv()
.arg("--python")
.arg("3.12"), @r###"
success: true
@ -158,7 +60,7 @@ fn create_venv_defaults_to_cwd() {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -169,17 +71,17 @@ fn create_venv_defaults_to_cwd() {
#[test]
fn create_venv_ignores_virtual_env_variable() {
let context = VenvTestContext::new(&["3.12"]);
let context = TestContext::new_with_versions(&["3.12"]);
// We shouldn't care if `VIRTUAL_ENV` is set to an non-existent directory
// because we ignore virtual environment interpreter sources (we require a system interpreter)
uv_snapshot!(context.filters(), context.venv_command()
uv_snapshot!(context.filters(), context.venv()
.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]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -188,8 +90,8 @@ fn create_venv_ignores_virtual_env_variable() {
#[test]
fn seed() {
let context = VenvTestContext::new(&["3.12"]);
uv_snapshot!(context.filters(), context.venv_command()
let context = TestContext::new_with_versions(&["3.12"]);
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--seed")
.arg("--python")
@ -199,7 +101,7 @@ fn seed() {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
+ pip==24.0
Activate with: source .venv/bin/activate
@ -211,8 +113,8 @@ fn seed() {
#[test]
fn seed_older_python_version() {
let context = VenvTestContext::new(&["3.10"]);
uv_snapshot!(context.filters(), context.venv_command()
let context = TestContext::new_with_versions(&["3.10"]);
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--seed")
.arg("--python")
@ -222,7 +124,7 @@ fn seed_older_python_version() {
----- stdout -----
----- stderr -----
Using Python 3.10.[X] interpreter at: [PATH]
Using Python 3.10.[X] interpreter at: [PYTHON-3.10]
Creating virtualenv at: .venv
+ pip==24.0
+ setuptools==69.2.0
@ -236,9 +138,9 @@ fn seed_older_python_version() {
#[test]
fn create_venv_unknown_python_minor() {
let context = VenvTestContext::new(&["3.12"]);
let context = TestContext::new_with_versions(&["3.12"]);
let mut command = context.venv_command();
let mut command = context.venv();
command
.arg(context.venv.as_os_str())
// Request a version we know we'll never see
@ -274,9 +176,9 @@ fn create_venv_unknown_python_minor() {
#[test]
fn create_venv_unknown_python_patch() {
let context = VenvTestContext::new(&["3.12"]);
let context = TestContext::new_with_versions(&["3.12"]);
let mut command = context.venv_command();
let mut command = context.venv();
command
.arg(context.venv.as_os_str())
// Request a version we know we'll never see
@ -313,9 +215,9 @@ fn create_venv_unknown_python_patch() {
#[cfg(feature = "python-patch")]
#[test]
fn create_venv_python_patch() {
let context = VenvTestContext::new(&["3.12.1"]);
let context = TestContext::new_with_versions(&["3.12.1"]);
uv_snapshot!(context.filters(), context.venv_command()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12.1"), @r###"
@ -324,7 +226,7 @@ fn create_venv_python_patch() {
----- stdout -----
----- stderr -----
Using Python 3.12.1 interpreter at: [PATH]
Using Python 3.12.1 interpreter at: [PYTHON-3.12.1]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -335,12 +237,12 @@ fn create_venv_python_patch() {
#[test]
fn file_exists() -> Result<()> {
let context = VenvTestContext::new(&["3.12"]);
let context = TestContext::new_with_versions(&["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()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
@ -349,7 +251,7 @@ fn file_exists() -> Result<()> {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
uv::venv::creation
@ -363,11 +265,11 @@ fn file_exists() -> Result<()> {
#[test]
fn empty_dir_exists() -> Result<()> {
let context = VenvTestContext::new(&["3.12"]);
let context = TestContext::new_with_versions(&["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()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
@ -376,7 +278,7 @@ fn empty_dir_exists() -> Result<()> {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -389,13 +291,13 @@ fn empty_dir_exists() -> Result<()> {
#[test]
fn non_empty_dir_exists() -> Result<()> {
let context = VenvTestContext::new(&["3.12"]);
let context = TestContext::new_with_versions(&["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()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
@ -404,7 +306,7 @@ fn non_empty_dir_exists() -> Result<()> {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
uv::venv::creation
@ -418,14 +320,14 @@ fn non_empty_dir_exists() -> Result<()> {
#[test]
fn non_empty_dir_exists_allow_existing() -> Result<()> {
let context = VenvTestContext::new(&["3.12"]);
let context = TestContext::new_with_versions(&["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()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12"), @r###"
@ -434,7 +336,7 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
uv::venv::creation
@ -443,7 +345,7 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> {
"###
);
uv_snapshot!(context.filters(), context.venv_command()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--allow-existing")
.arg("--python")
@ -453,7 +355,7 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -461,7 +363,7 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> {
// Running again should _also_ succeed, overwriting existing symlinks and respecting existing
// directories.
uv_snapshot!(context.filters(), context.venv_command()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--allow-existing")
.arg("--python")
@ -471,7 +373,7 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> {
----- stdout -----
----- stderr -----
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -483,37 +385,37 @@ fn non_empty_dir_exists_allow_existing() -> Result<()> {
#[test]
#[cfg(windows)]
fn windows_shims() -> Result<()> {
let context = VenvTestContext::new(&["3.9", "3.8"]);
let context = TestContext::new_with_versions(&["3.9", "3.8"]);
let shim_path = context.temp_dir.child("shim");
let py38 = std::env::split_paths(&context.python_path)
let py38 = context
.python_versions
.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"));
assert!(py38.0.to_string().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()),
format!("@echo off\r\n{}/python.exe %*", py38.1.display()),
)?;
// Create a virtual environment at `.venv`, passing the redundant `--clear` flag.
uv_snapshot!(context.filters(), context.venv_command()
// Create a virtual environment at `.venv` with the shim
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--clear")
.env("UV_TEST_PYTHON_PATH", format!("{};{}", shim_path.display(), context.python_path.simplified_display())), @r###"
.env("UV_TEST_PYTHON_PATH", format!("{};{}", shim_path.display(), context.python_path().to_string_lossy())), @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]
Using Python 3.8.[X] interpreter at: [PYTHON-3.8]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -526,10 +428,10 @@ fn windows_shims() -> Result<()> {
#[test]
fn virtualenv_compatibility() {
let context = VenvTestContext::new(&["3.12"]);
let context = TestContext::new_with_versions(&["3.12"]);
// Create a virtual environment at `.venv`, passing the redundant `--clear` flag.
uv_snapshot!(context.filters(), context.venv_command()
uv_snapshot!(context.filters(), context.venv()
.arg(context.venv.as_os_str())
.arg("--clear")
.arg("--python")
@ -540,7 +442,7 @@ fn virtualenv_compatibility() {
----- stderr -----
warning: virtualenv's `--clear` has no effect (uv always clears the virtual environment).
Using Python 3.12.[X] interpreter at: [PATH]
Using Python 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtualenv at: .venv
Activate with: source .venv/bin/activate
"###
@ -552,10 +454,9 @@ fn virtualenv_compatibility() {
#[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");
let pyvenv_cfg = context.venv.child("pyvenv.cfg");
venv.assert(predicates::path::is_dir());
context.venv.assert(predicates::path::is_dir());
// Check pyvenv.cfg exists
pyvenv_cfg.assert(predicates::path::is_file());
@ -569,11 +470,11 @@ fn verify_pyvenv_cfg() {
/// 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"]);
let context = TestContext::new_with_versions(&["3.12"]);
// Create a virtual environment at `.venv`.
context
.venv_command()
.venv()
.arg(context.venv.as_os_str())
.arg("--python")
.arg("3.12")
@ -595,7 +496,7 @@ fn verify_nested_pyvenv_cfg() -> Result<()> {
// Now, create a virtual environment from within the virtual environment.
let subvenv = context.temp_dir.child(".subvenv");
context
.venv_command()
.venv()
.arg(subvenv.as_os_str())
.arg("--python")
.arg("3.12")
@ -622,29 +523,19 @@ fn verify_nested_pyvenv_cfg() -> Result<()> {
#[test]
#[cfg(windows)]
fn path_with_trailing_space_gives_proper_error() {
let context = VenvTestContext::new(&["3.12"]);
let context = TestContext::new_with_versions(&["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###"
// Set a custom cache directory with a trailing space
uv_snapshot!(context.filters(), context.venv()
.env("UV_CACHE_DIR", format!("{} ", context.cache_dir.path().display())), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: failed to open file `C:\Path\to\Cache\dir \CACHEDIR.TAG`
error: failed to open file `[CACHE_DIR]/ /CACHEDIR.TAG`
Caused by: The system cannot find the path specified. (os error 3)
"###
);
// Note the extra trailing `/` in the snapshot is due to the filters, not the actual output.
}

View file

@ -419,7 +419,7 @@ fn test_uv_run_with_package_virtual_workspace() -> Result<()> {
----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON]
Creating virtualenv at: [VENV]/
Creating virtualenv at: .venv
Resolved 8 packages in [TIME]
Downloaded 5 packages in [TIME]
Installed 5 packages in [TIME]
@ -479,7 +479,7 @@ fn test_uv_run_with_package_root_workspace() -> Result<()> {
----- stderr -----
Using Python 3.12.[X] interpreter at: [PYTHON]
Creating virtualenv at: [VENV]/
Creating virtualenv at: .venv
Resolved 8 packages in [TIME]
Downloaded 5 packages in [TIME]
Installed 5 packages in [TIME]