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 insta::assert_snapshot;
use predicates::prelude::predicate; use predicates::prelude::predicate;
use std::env::current_dir; use std::env::current_dir;
use std::process::Command;
use zip::ZipArchive; use zip::ZipArchive;
#[test] #[test]
@ -1857,7 +1856,7 @@ fn build_unconfigured_setuptools() -> Result<()> {
+ greet==0.1.0 (from file://[TEMP_DIR]/) + 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 success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----

View file

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

View file

@ -1085,15 +1085,30 @@ impl TestContext {
} }
pub fn interpreter(&self) -> PathBuf { 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. /// Run the given python code and check whether it succeeds.
pub fn assert_command(&self, command: &str) -> Assert { pub fn assert_command(&self, command: &str) -> Assert {
self.new_command_with(&venv_to_interpreter(&self.venv)) self.python_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")
.arg("-c") .arg("-c")
.arg(command) .arg(command)
.current_dir(&self.temp_dir) .current_dir(&self.temp_dir)
@ -1102,10 +1117,7 @@ impl TestContext {
/// Run the given python file and check whether it succeeds. /// Run the given python file and check whether it succeeds.
pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert { pub fn assert_file(&self, file: impl AsRef<Path>) -> Assert {
self.new_command_with(&venv_to_interpreter(&self.venv)) self.python_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")
.arg(file.as_ref()) .arg(file.as_ref())
.current_dir(&self.temp_dir) .current_dir(&self.temp_dir)
.assert() .assert()
@ -1120,6 +1132,12 @@ impl TestContext {
.stdout(version); .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. /// Generate various escaped regex patterns for the given path.
pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> { pub fn path_patterns(path: impl AsRef<Path>) -> Vec<String> {
let mut patterns = Vec::new(); 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. /// Get the path to the python interpreter for a specific python version.
pub fn get_python(version: &PythonVersion) -> PathBuf { pub fn get_python(version: &PythonVersion) -> PathBuf {
ManagedPythonInstallations::from_settings(None) ManagedPythonInstallations::from_settings(None)

View file

@ -19,7 +19,7 @@ use wiremock::{
use crate::common::{self, decode_token}; use crate::common::{self, decode_token};
use crate::common::{ use crate::common::{
DEFAULT_PYTHON_VERSION, TestContext, build_vendor_links_url, download_to_disk, get_bin, 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_fs::Simplified;
use uv_static::EnvVars; use uv_static::EnvVars;
@ -9083,8 +9083,7 @@ fn build_tag() {
); );
// Ensure that we choose the highest build tag (5). // Ensure that we choose the highest build tag (5).
uv_snapshot!(Command::new(venv_to_interpreter(&context.venv)) uv_snapshot!(context.python_command()
.arg("-B")
.arg("-c") .arg("-c")
.arg("import build_tag; build_tag.main()") .arg("import build_tag; build_tag.main()")
.current_dir(&context.temp_dir), @r###" .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::env::consts::EXE_SUFFIX;
use std::path::Path;
use std::process::Command;
use anyhow::Result; use anyhow::Result;
use assert_cmd::prelude::*; use assert_cmd::prelude::*;
@ -11,24 +9,10 @@ use indoc::indoc;
use predicates::Predicate; use predicates::Predicate;
use url::Url; use url::Url;
use crate::common::{ use crate::common::{TestContext, download_to_disk, site_packages_path, uv_snapshot};
TestContext, download_to_disk, site_packages_path, uv_snapshot, venv_to_interpreter,
};
use uv_fs::{Simplified, copy_dir_all}; use uv_fs::{Simplified, copy_dir_all};
use uv_static::EnvVars; 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] #[test]
fn missing_requirements_txt() { fn missing_requirements_txt() {
let context = TestContext::new("3.12"); 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(()) Ok(())
} }
@ -5221,8 +5211,8 @@ fn target_built_distribution() -> Result<()> {
context.assert_command("import iniconfig").failure(); context.assert_command("import iniconfig").failure();
// Ensure that we can import the package by augmenting the `PYTHONPATH`. // Ensure that we can import the package by augmenting the `PYTHONPATH`.
Command::new(venv_to_interpreter(&context.venv)) context
.arg("-B") .python_command()
.arg("-c") .arg("-c")
.arg("import iniconfig") .arg("import iniconfig")
.env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path()) .env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path())
@ -5326,8 +5316,8 @@ fn target_source_distribution() -> Result<()> {
context.assert_command("import iniconfig").failure(); context.assert_command("import iniconfig").failure();
// Ensure that we can import the package by augmenting the `PYTHONPATH`. // Ensure that we can import the package by augmenting the `PYTHONPATH`.
Command::new(venv_to_interpreter(&context.venv)) context
.arg("-B") .python_command()
.arg("-c") .arg("-c")
.arg("import iniconfig") .arg("import iniconfig")
.env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path()) .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(); context.assert_command("import wheel").failure();
// Ensure that we can import the package by augmenting the `PYTHONPATH`. // Ensure that we can import the package by augmenting the `PYTHONPATH`.
Command::new(venv_to_interpreter(&context.venv)) context
.arg("-B") .python_command()
.arg("-c") .arg("-c")
.arg("import wheel") .arg("import wheel")
.env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path()) .env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path())
@ -5474,8 +5464,8 @@ fn prefix() -> Result<()> {
context.assert_command("import iniconfig").failure(); context.assert_command("import iniconfig").failure();
// Ensure that we can import the package by augmenting the `PYTHONPATH`. // Ensure that we can import the package by augmenting the `PYTHONPATH`.
Command::new(venv_to_interpreter(&context.venv)) context
.arg("-B") .python_command()
.arg("-c") .arg("-c")
.arg("import iniconfig") .arg("import iniconfig")
.env( .env(

View file

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

View file

@ -16,8 +16,8 @@ use predicates::prelude::predicate;
use uv_static::EnvVars; use uv_static::EnvVars;
use crate::common::{ use crate::common::{
build_vendor_links_url, get_bin, packse_index_url, python_path_with_versions, uv_snapshot, TestContext, build_vendor_links_url, get_bin, packse_index_url, python_path_with_versions,
TestContext, uv_snapshot,
}; };
/// Provision python binaries and return a `pip compile` command with options shared across all scenarios. /// 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))] #![cfg(all(feature = "python", feature = "pypi", unix))]
use std::path::Path;
use std::process::Command; use std::process::Command;
use assert_cmd::assert::Assert;
use assert_cmd::prelude::*;
use uv_static::EnvVars; use uv_static::EnvVars;
use crate::common::{ use crate::common::{TestContext, build_vendor_links_url, packse_index_url, uv_snapshot};
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();
}
/// Create a `pip install` command with options shared across all scenarios. /// Create a `pip install` command with options shared across all scenarios.
fn command(context: &TestContext) -> Command { fn command(context: &TestContext) -> Command {
let mut command = Command::new(get_bin()); let mut command = context.pip_install();
command command
.arg("pip")
.arg("install")
.arg("--index-url") .arg("--index-url")
.arg(packse_index_url()) .arg(packse_index_url())
.arg("--find-links") .arg("--find-links")
.arg(build_vendor_links_url()); .arg(build_vendor_links_url());
context.add_shared_options(&mut command, true);
command.env_remove(EnvVars::UV_EXCLUDE_NEWER); command.env_remove(EnvVars::UV_EXCLUDE_NEWER);
command command
} }
@ -93,25 +61,20 @@ fn {{module_name}}() {
{{/resolver_options.python_platform}} {{/resolver_options.python_platform}}
{{#root.requires}} {{#root.requires}}
.arg("{{requirement}}") .arg("{{requirement}}")
{{/root.requires}}, @r###"<snapshot> {{/root.requires}}, @r#"<snapshot>
"###); "#);
{{#expected.explanation}} {{#expected.explanation}}
// {{expected.explanation}} // {{expected.explanation}}
{{/expected.explanation}} {{/expected.explanation}}
{{#expected.satisfiable}} {{#expected.satisfiable}}
{{#expected.packages}} {{#expected.packages}}
assert_installed( context.assert_installed("{{module_name}}", "{{version}}");
&context.venv,
"{{module_name}}",
"{{version}}",
&context.temp_dir
);
{{/expected.packages}} {{/expected.packages}}
{{/expected.satisfiable}} {{/expected.satisfiable}}
{{^expected.satisfiable}} {{^expected.satisfiable}}
{{#root.requires}} {{#root.requires}}
assert_not_installed(&context.venv, "{{module_name}}", &context.temp_dir); context.assert_not_installed("{{module_name}}");
{{/root.requires}} {{/root.requires}}
{{/expected.satisfiable}} {{/expected.satisfiable}}
} }

View file

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