Unify test venv python command creation (#14117)

Refactoring in preparation for
https://github.com/astral-sh/uv/pull/14080
This commit is contained in:
konsti 2025-06-18 15:06:09 +02:00 committed by GitHub
parent 4d9c9a1e76
commit ee0ba65eb2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 212 additions and 811 deletions

View file

@ -7,7 +7,6 @@ use indoc::indoc;
use insta::assert_snapshot;
use predicates::prelude::predicate;
use std::env::current_dir;
use std::process::Command;
use zip::ZipArchive;
#[test]
@ -1857,7 +1856,7 @@ fn build_unconfigured_setuptools() -> Result<()> {
+ greet==0.1.0 (from file://[TEMP_DIR]/)
"###);
uv_snapshot!(context.filters(), Command::new(context.interpreter()).arg("-c").arg("import greet"), @r###"
uv_snapshot!(context.filters(), context.python_command().arg("-c").arg("import greet"), @r###"
success: true
exit_code: 0
----- stdout -----

View file

@ -50,13 +50,9 @@ fn built_by_uv_direct_wheel() -> Result<()> {
.assert()
.success();
uv_snapshot!(context
.run()
.arg("python")
uv_snapshot!(context.python_command()
.arg("-c")
.arg(BUILT_BY_UV_TEST_SCRIPT)
// Python on windows
.env(EnvVars::PYTHONUTF8, "1"), @r###"
.arg(BUILT_BY_UV_TEST_SCRIPT), @r###"
success: true
exit_code: 0
----- stdout -----
@ -138,13 +134,9 @@ fn built_by_uv_direct() -> Result<()> {
drop(wheel_dir);
uv_snapshot!(context
.run()
.arg("python")
uv_snapshot!(context.python_command()
.arg("-c")
.arg(BUILT_BY_UV_TEST_SCRIPT)
// Python on windows
.env(EnvVars::PYTHONUTF8, "1"), @r###"
.arg(BUILT_BY_UV_TEST_SCRIPT), @r###"
success: true
exit_code: 0
----- stdout -----
@ -169,7 +161,8 @@ fn built_by_uv_editable() -> Result<()> {
// Without the editable, pytest fails.
context.pip_install().arg("pytest").assert().success();
Command::new(context.interpreter())
context
.python_command()
.arg("-m")
.arg("pytest")
.current_dir(built_by_uv)
@ -200,7 +193,7 @@ fn built_by_uv_editable() -> Result<()> {
drop(wheel_dir);
// Now, pytest passes.
uv_snapshot!(Command::new(context.interpreter())
uv_snapshot!(context.python_command()
.arg("-m")
.arg("pytest")
// Avoid showing absolute paths and column dependent layout
@ -340,11 +333,9 @@ fn rename_module() -> Result<()> {
.success();
// Importing the module with the `module-name` name succeeds.
uv_snapshot!(Command::new(context.interpreter())
uv_snapshot!(context.python_command()
.arg("-c")
.arg("import bar")
// Python on windows
.env(EnvVars::PYTHONUTF8, "1"), @r###"
.arg("import bar"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -354,11 +345,9 @@ fn rename_module() -> Result<()> {
"###);
// Importing the package name fails, it was overridden by `module-name`.
uv_snapshot!(Command::new(context.interpreter())
uv_snapshot!(context.python_command()
.arg("-c")
.arg("import foo")
// Python on windows
.env(EnvVars::PYTHONUTF8, "1"), @r###"
.arg("import foo"), @r###"
success: false
exit_code: 1
----- stdout -----
@ -419,11 +408,9 @@ fn rename_module_editable_build() -> Result<()> {
.success();
// Importing the module with the `module-name` name succeeds.
uv_snapshot!(Command::new(context.interpreter())
uv_snapshot!(context.python_command()
.arg("-c")
.arg("import bar")
// Python on windows
.env(EnvVars::PYTHONUTF8, "1"), @r###"
.arg("import bar"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -514,11 +501,9 @@ fn build_module_name_normalization() -> Result<()> {
.assert()
.success();
uv_snapshot!(Command::new(context.interpreter())
uv_snapshot!(context.python_command()
.arg("-c")
.arg("import Django_plugin")
// Python on windows
.env(EnvVars::PYTHONUTF8, "1"), @r"
.arg("import Django_plugin"), @r"
success: true
exit_code: 0
----- stdout -----
@ -728,7 +713,7 @@ fn complex_namespace_packages() -> Result<()> {
"
);
uv_snapshot!(Command::new(context.interpreter())
uv_snapshot!(context.python_command()
.arg("-c")
.arg("from complex_project.part_b import two; print(two())"),
@r"
@ -769,7 +754,7 @@ fn complex_namespace_packages() -> Result<()> {
"
);
uv_snapshot!(Command::new(context.interpreter())
uv_snapshot!(context.python_command()
.arg("-c")
.arg("from complex_project.part_b import two; print(two())"),
@r"

View file

@ -1085,15 +1085,30 @@ impl TestContext {
}
pub fn interpreter(&self) -> PathBuf {
venv_to_interpreter(&self.venv)
let venv = &self.venv;
if cfg!(unix) {
venv.join("bin").join("python")
} else if cfg!(windows) {
venv.join("Scripts").join("python.exe")
} else {
unimplemented!("Only Windows and Unix are supported")
}
}
pub fn python_command(&self) -> Command {
let mut command = self.new_command_with(&self.interpreter());
command
// Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files
// https://github.com/python/cpython/issues/75953
.arg("-B")
// Python on windows
.env(EnvVars::PYTHONUTF8, "1");
command
}
/// Run the given python code and check whether it succeeds.
pub fn assert_command(&self, command: &str) -> Assert {
self.new_command_with(&venv_to_interpreter(&self.venv))
// Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files
// https://github.com/python/cpython/issues/75953
.arg("-B")
self.python_command()
.arg("-c")
.arg(command)
.current_dir(&self.temp_dir)
@ -1102,10 +1117,7 @@ impl TestContext {
/// Run the given python file and check whether it succeeds.
pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
self.new_command_with(&venv_to_interpreter(&self.venv))
// Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files
// https://github.com/python/cpython/issues/75953
.arg("-B")
self.python_command()
.arg(file.as_ref())
.current_dir(&self.temp_dir)
.assert()
@ -1120,6 +1132,12 @@ impl TestContext {
.stdout(version);
}
/// Assert a package is not installed.
pub fn assert_not_installed(&self, package: &'static str) {
self.assert_command(format!("import {package}").as_str())
.failure();
}
/// Generate various escaped regex patterns for the given path.
pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
let mut patterns = Vec::new();
@ -1347,16 +1365,6 @@ pub fn venv_bin_path(venv: impl AsRef<Path>) -> PathBuf {
}
}
pub fn venv_to_interpreter(venv: &Path) -> PathBuf {
if cfg!(unix) {
venv.join("bin").join("python")
} else if cfg!(windows) {
venv.join("Scripts").join("python.exe")
} else {
unimplemented!("Only Windows and Unix are supported")
}
}
/// Get the path to the python interpreter for a specific python version.
pub fn get_python(version: &PythonVersion) -> PathBuf {
ManagedPythonInstallations::from_settings(None)

View file

@ -19,7 +19,7 @@ use wiremock::{
use crate::common::{self, decode_token};
use crate::common::{
DEFAULT_PYTHON_VERSION, TestContext, build_vendor_links_url, download_to_disk, get_bin,
uv_snapshot, venv_bin_path, venv_to_interpreter,
uv_snapshot, venv_bin_path,
};
use uv_fs::Simplified;
use uv_static::EnvVars;
@ -9083,8 +9083,7 @@ fn build_tag() {
);
// Ensure that we choose the highest build tag (5).
uv_snapshot!(Command::new(venv_to_interpreter(&context.venv))
.arg("-B")
uv_snapshot!(context.python_command()
.arg("-c")
.arg("import build_tag; build_tag.main()")
.current_dir(&context.temp_dir), @r###"

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,4 @@
use std::env::consts::EXE_SUFFIX;
use std::path::Path;
use std::process::Command;
use anyhow::Result;
use assert_cmd::prelude::*;
@ -11,24 +9,10 @@ use indoc::indoc;
use predicates::Predicate;
use url::Url;
use crate::common::{
TestContext, download_to_disk, site_packages_path, uv_snapshot, venv_to_interpreter,
};
use crate::common::{TestContext, download_to_disk, site_packages_path, uv_snapshot};
use uv_fs::{Simplified, copy_dir_all};
use uv_static::EnvVars;
fn check_command(venv: &Path, command: &str, temp_dir: &Path) {
Command::new(venv_to_interpreter(venv))
// Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files
// https://github.com/python/cpython/issues/75953
.arg("-B")
.arg("-c")
.arg(command)
.current_dir(temp_dir)
.assert()
.success();
}
#[test]
fn missing_requirements_txt() {
let context = TestContext::new("3.12");
@ -463,7 +447,13 @@ fn link() -> Result<()> {
"###
);
check_command(&context2.venv, "import iniconfig", &context2.temp_dir);
context2
.python_command()
.arg("-c")
.arg("import iniconfig")
.current_dir(&context2.temp_dir)
.assert()
.success();
Ok(())
}
@ -5221,8 +5211,8 @@ fn target_built_distribution() -> Result<()> {
context.assert_command("import iniconfig").failure();
// Ensure that we can import the package by augmenting the `PYTHONPATH`.
Command::new(venv_to_interpreter(&context.venv))
.arg("-B")
context
.python_command()
.arg("-c")
.arg("import iniconfig")
.env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path())
@ -5326,8 +5316,8 @@ fn target_source_distribution() -> Result<()> {
context.assert_command("import iniconfig").failure();
// Ensure that we can import the package by augmenting the `PYTHONPATH`.
Command::new(venv_to_interpreter(&context.venv))
.arg("-B")
context
.python_command()
.arg("-c")
.arg("import iniconfig")
.env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path())
@ -5397,8 +5387,8 @@ fn target_no_build_isolation() -> Result<()> {
context.assert_command("import wheel").failure();
// Ensure that we can import the package by augmenting the `PYTHONPATH`.
Command::new(venv_to_interpreter(&context.venv))
.arg("-B")
context
.python_command()
.arg("-c")
.arg("import wheel")
.env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path())
@ -5474,8 +5464,8 @@ fn prefix() -> Result<()> {
context.assert_command("import iniconfig").failure();
// Ensure that we can import the package by augmenting the `PYTHONPATH`.
Command::new(venv_to_interpreter(&context.venv))
.arg("-B")
context
.python_command()
.arg("-c")
.arg("import iniconfig")
.env(

View file

@ -5,7 +5,7 @@ use assert_cmd::prelude::*;
use assert_fs::fixture::ChildPath;
use assert_fs::prelude::*;
use crate::common::{TestContext, get_bin, uv_snapshot, venv_to_interpreter};
use crate::common::{TestContext, get_bin, uv_snapshot};
#[test]
fn no_arguments() {
@ -113,12 +113,7 @@ fn uninstall() -> Result<()> {
.assert()
.success();
Command::new(venv_to_interpreter(&context.venv))
.arg("-c")
.arg("import markupsafe")
.current_dir(&context.temp_dir)
.assert()
.success();
context.assert_command("import markupsafe").success();
uv_snapshot!(context.pip_uninstall()
.arg("MarkupSafe"), @r###"
@ -132,12 +127,7 @@ fn uninstall() -> Result<()> {
"###
);
Command::new(venv_to_interpreter(&context.venv))
.arg("-c")
.arg("import markupsafe")
.current_dir(&context.temp_dir)
.assert()
.failure();
context.assert_command("import markupsafe").failure();
Ok(())
}
@ -156,12 +146,7 @@ fn missing_record() -> Result<()> {
.assert()
.success();
Command::new(venv_to_interpreter(&context.venv))
.arg("-c")
.arg("import markupsafe")
.current_dir(&context.temp_dir)
.assert()
.success();
context.assert_command("import markupsafe").success();
// Delete the RECORD file.
let dist_info = context.site_packages().join("MarkupSafe-2.1.3.dist-info");
@ -202,11 +187,7 @@ fn uninstall_editable_by_name() -> Result<()> {
.assert()
.success();
Command::new(venv_to_interpreter(&context.venv))
.arg("-c")
.arg("import poetry_editable")
.assert()
.success();
context.assert_command("import poetry_editable").success();
// Uninstall the editable by name.
uv_snapshot!(context.filters(), context.pip_uninstall()
@ -221,11 +202,7 @@ fn uninstall_editable_by_name() -> Result<()> {
"###
);
Command::new(venv_to_interpreter(&context.venv))
.arg("-c")
.arg("import poetry_editable")
.assert()
.failure();
context.assert_command("import poetry_editable").failure();
Ok(())
}
@ -251,11 +228,7 @@ fn uninstall_by_path() -> Result<()> {
.assert()
.success();
Command::new(venv_to_interpreter(&context.venv))
.arg("-c")
.arg("import poetry_editable")
.assert()
.success();
context.assert_command("import poetry_editable").success();
// Uninstall the editable by path.
uv_snapshot!(context.filters(), context.pip_uninstall()
@ -270,11 +243,7 @@ fn uninstall_by_path() -> Result<()> {
"###
);
Command::new(venv_to_interpreter(&context.venv))
.arg("-c")
.arg("import poetry_editable")
.assert()
.failure();
context.assert_command("import poetry_editable").failure();
Ok(())
}
@ -300,11 +269,7 @@ fn uninstall_duplicate_by_path() -> Result<()> {
.assert()
.success();
Command::new(venv_to_interpreter(&context.venv))
.arg("-c")
.arg("import poetry_editable")
.assert()
.success();
context.assert_command("import poetry_editable").success();
// Uninstall the editable by both path and name.
uv_snapshot!(context.filters(), context.pip_uninstall()
@ -320,11 +285,7 @@ fn uninstall_duplicate_by_path() -> Result<()> {
"###
);
Command::new(venv_to_interpreter(&context.venv))
.arg("-c")
.arg("import poetry_editable")
.assert()
.failure();
context.assert_command("import poetry_editable").failure();
Ok(())
}

View file

@ -16,8 +16,8 @@ use predicates::prelude::predicate;
use uv_static::EnvVars;
use crate::common::{
build_vendor_links_url, get_bin, packse_index_url, python_path_with_versions, uv_snapshot,
TestContext,
TestContext, build_vendor_links_url, get_bin, packse_index_url, python_path_with_versions,
uv_snapshot,
};
/// Provision python binaries and return a `pip compile` command with options shared across all scenarios.

View file

@ -5,52 +5,20 @@
//!
#![cfg(all(feature = "python", feature = "pypi", unix))]
use std::path::Path;
use std::process::Command;
use assert_cmd::assert::Assert;
use assert_cmd::prelude::*;
use uv_static::EnvVars;
use crate::common::{
build_vendor_links_url, get_bin, packse_index_url, uv_snapshot, venv_to_interpreter,
TestContext,
};
fn assert_command(venv: &Path, command: &str, temp_dir: &Path) -> Assert {
Command::new(venv_to_interpreter(venv))
.arg("-c")
.arg(command)
.current_dir(temp_dir)
.assert()
}
fn assert_installed(venv: &Path, package: &'static str, version: &'static str, temp_dir: &Path) {
assert_command(
venv,
format!("import {package} as package; print(package.__version__, end='')").as_str(),
temp_dir,
)
.success()
.stdout(version);
}
fn assert_not_installed(venv: &Path, package: &'static str, temp_dir: &Path) {
assert_command(venv, format!("import {package}").as_str(), temp_dir).failure();
}
use crate::common::{TestContext, build_vendor_links_url, packse_index_url, uv_snapshot};
/// Create a `pip install` command with options shared across all scenarios.
fn command(context: &TestContext) -> Command {
let mut command = Command::new(get_bin());
let mut command = context.pip_install();
command
.arg("pip")
.arg("install")
.arg("--index-url")
.arg(packse_index_url())
.arg("--find-links")
.arg(build_vendor_links_url());
context.add_shared_options(&mut command, true);
command.env_remove(EnvVars::UV_EXCLUDE_NEWER);
command
}
@ -93,25 +61,20 @@ fn {{module_name}}() {
{{/resolver_options.python_platform}}
{{#root.requires}}
.arg("{{requirement}}")
{{/root.requires}}, @r###"<snapshot>
"###);
{{/root.requires}}, @r#"<snapshot>
"#);
{{#expected.explanation}}
// {{expected.explanation}}
{{/expected.explanation}}
{{#expected.satisfiable}}
{{#expected.packages}}
assert_installed(
&context.venv,
"{{module_name}}",
"{{version}}",
&context.temp_dir
);
context.assert_installed("{{module_name}}", "{{version}}");
{{/expected.packages}}
{{/expected.satisfiable}}
{{^expected.satisfiable}}
{{#root.requires}}
assert_not_installed(&context.venv, "{{module_name}}", &context.temp_dir);
context.assert_not_installed("{{module_name}}");
{{/root.requires}}
{{/expected.satisfiable}}
}

View file

@ -15,7 +15,7 @@ use insta::assert_snapshot;
use uv_static::EnvVars;
use crate::common::{packse_index_url, TestContext, uv_snapshot};
use crate::common::{TestContext, packse_index_url, uv_snapshot};
{{#scenarios}}