Add filtering of interpreter names for tests with multiple Python versions (#4368)

Extends new filters for interpreter paths to apply to tests with
multiple Python versions. Adds patch version filtering for them as well,
which is needed for #4360 tests.
This commit is contained in:
Zanie Blue 2024-06-17 16:22:46 -04:00 committed by GitHub
parent 3f164b5a3a
commit 05870609ee
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 114 additions and 105 deletions

View file

@ -7,6 +7,7 @@ use assert_fs::assert::PathAssert;
use assert_fs::fixture::{ChildPath, PathChild};
use regex::Regex;
use std::borrow::BorrowMut;
use std::collections::VecDeque;
use std::env;
use std::ffi::OsString;
use std::iter::Iterator;
@ -62,8 +63,11 @@ pub struct TestContext {
pub python_version: String,
pub workspace_root: PathBuf,
// Additional Python versions
python_versions: Vec<(PythonVersion, PathBuf)>,
// Standard filters for this test context
filters: Vec<(String, String)>,
filters: VecDeque<(String, String)>,
}
impl TestContext {
@ -87,7 +91,7 @@ impl TestContext {
let python_version =
PythonVersion::from_str(python_version).expect("Tests must use valid Python versions");
let mut filters = Vec::new();
let mut filters = VecDeque::new();
filters.extend(
Self::path_patterns(&cache_dir)
@ -116,7 +120,7 @@ impl TestContext {
);
// Account for [`Simplified::user_display`] which is relative to the command working directory
filters.push((
filters.push_back((
Self::path_pattern(
site_packages
.strip_prefix(&temp_dir)
@ -124,7 +128,7 @@ impl TestContext {
),
"[SITE_PACKAGES]/".to_string(),
));
filters.push((
filters.push_back((
Self::path_pattern(
venv.strip_prefix(&temp_dir)
.expect("The test virtual environment directory is always in the tempdir"),
@ -134,13 +138,13 @@ impl TestContext {
// Filter non-deterministic temporary directory names
// Note we apply this _after_ all the full paths to avoid breaking their matching
filters.push((r"(\\|\/)\.tmp.*(\\|\/)".to_string(), "/[TMP]/".to_string()));
filters.push_back((r"(\\|\/)\.tmp.*(\\|\/)".to_string(), "/[TMP]/".to_string()));
// Account for platform prefix differences `file://` (Unix) vs `file:///` (Windows)
filters.push((r"file:///".to_string(), "file://".to_string()));
filters.push_back((r"file:///".to_string(), "file://".to_string()));
// Destroy any remaining UNC prefixes (Windows only)
filters.push((r"\\\\\?\\".to_string(), String::new()));
filters.push_back((r"\\\\\?\\".to_string(), String::new()));
let mut result = Self {
temp_dir,
@ -148,6 +152,7 @@ impl TestContext {
venv,
python_version: python_version.to_string(),
filters,
python_versions: Vec::new(),
workspace_root,
};
@ -156,22 +161,47 @@ impl TestContext {
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
self.filters.extend(
Self::path_patterns(executable)
.into_iter()
.map(|pattern| (format!("{pattern}python.*"), format!("[PYTHON-{version}]"))),
);
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((
self.filters.push_back((
format!(r"({})\.\d+", regex::escape(version.to_string().as_str())),
"$1.[X]".to_string(),
));
}
}
/// Create a `pip compile` command for testing.
pub fn compile(&self) -> std::process::Command {
let mut command = self.compile_without_exclude_newer();
@ -193,7 +223,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TEST_PYTHON_PATH", "/dev/null")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(self.temp_dir.path());
if cfg!(all(windows, debug_assertions)) {
@ -227,7 +257,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TEST_PYTHON_PATH", "/dev/null")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(&self.temp_dir);
if cfg!(all(windows, debug_assertions)) {
@ -247,9 +277,9 @@ impl TestContext {
.arg("--cache-dir")
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_TEST_PYTHON_PATH", "/dev/null")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.env("UV_NO_WRAP", "1")
.env("UV_TEST_PYTHON_PATH", "/dev/null")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(&self.temp_dir);
if cfg!(all(windows, debug_assertions)) {
@ -267,7 +297,7 @@ impl TestContext {
command
.arg("--exclude-newer")
.arg(EXCLUDE_NEWER)
.env("UV_TEST_PYTHON_PATH", "/dev/null");
.env("UV_TEST_PYTHON_PATH", &self.python_path());
command
}
@ -285,7 +315,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TEST_PYTHON_PATH", "/dev/null")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(&self.temp_dir);
if cfg!(all(windows, debug_assertions)) {
@ -311,7 +341,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TEST_PYTHON_PATH", "/dev/null")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.env("UV_PREVIEW", "1")
.env("UV_TOOLCHAIN_DIR", self.toolchains_dir().as_os_str())
.current_dir(&self.temp_dir);
@ -346,7 +376,7 @@ impl TestContext {
.arg(self.cache_dir.path())
.env("VIRTUAL_ENV", self.venv.as_os_str())
.env("UV_NO_WRAP", "1")
.env("UV_TEST_PYTHON_PATH", "/dev/null")
.env("UV_TEST_PYTHON_PATH", &self.python_path())
.current_dir(&self.temp_dir);
if cfg!(all(windows, debug_assertions)) {
@ -467,6 +497,10 @@ impl TestContext {
)
}
fn python_path(&self) -> OsString {
std::env::join_paths(self.python_versions.iter().map(|(_, path)| path)).unwrap()
}
/// Standard snapshot filters _plus_ those for this test context.
pub fn filters(&self) -> Vec<(&str, &str)> {
// Put test context snapshots before the default filters
@ -606,6 +640,19 @@ pub fn python_path_with_versions(
temp_dir: &assert_fs::TempDir,
python_versions: &[&str],
) -> anyhow::Result<OsString> {
Ok(std::env::join_paths(python_toolchains_for_versions(
temp_dir,
python_versions,
)?)?)
}
/// Create a `PATH` with the requested Python versions available in order.
///
/// Generally this should be used with `UV_TEST_PYTHON_PATH`.
pub fn python_toolchains_for_versions(
temp_dir: &assert_fs::TempDir,
python_versions: &[&str],
) -> anyhow::Result<Vec<PathBuf>> {
let cache = Cache::from_path(temp_dir.child("cache").to_path_buf()).init()?;
let selected_pythons = python_versions
.iter()
@ -658,7 +705,7 @@ pub fn python_path_with_versions(
"Failed to fulfill requested test Python versions: {selected_pythons:?}"
);
Ok(env::join_paths(selected_pythons)?)
Ok(selected_pythons)
}
#[derive(Debug, Copy, Clone)]

View file

@ -4,16 +4,14 @@ use anyhow::Result;
use assert_fs::prelude::*;
use indoc::indoc;
use common::{python_path_with_versions, uv_snapshot, TestContext};
use common::{uv_snapshot, TestContext};
mod common;
/// Run with different python versions, which also depend on different dependency versions.
#[test]
fn run_with_python_version() -> Result<()> {
let context = TestContext::new("3.12");
let python_path = python_path_with_versions(&context.temp_dir, &["3.11", "3.12"])
.expect("Failed to create Python test path");
let context = TestContext::new_with_versions(&["3.12", "3.11"]);
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
@ -44,8 +42,7 @@ fn run_with_python_version() -> Result<()> {
.arg("--preview")
.arg("python")
.arg("-B")
.arg("main.py")
.env("UV_TEST_PYTHON_PATH", &python_path);
.arg("main.py");
uv_snapshot!(context.filters(), command_with_args, @r###"
success: true
exit_code: 0
@ -71,8 +68,7 @@ fn run_with_python_version() -> Result<()> {
.arg("3.12")
.arg("python")
.arg("-B")
.arg("main.py")
.env("UV_TEST_PYTHON_PATH", &python_path);
.arg("main.py");
uv_snapshot!(context.filters(), command_with_args, @r###"
success: true
exit_code: 0
@ -94,17 +90,9 @@ fn run_with_python_version() -> Result<()> {
.arg("python")
.arg("-B")
.arg("main.py")
.env("UV_TEST_PYTHON_PATH", &python_path)
.env_remove("VIRTUAL_ENV");
let mut filters = context.filters();
filters.push((
r"Using Python 3.11.\d+ interpreter at: .*",
"Using Python 3.11.[X] interpreter at: [PYTHON]",
));
filters.push((r"3.11.\d+", "3.11.[X]"));
uv_snapshot!(filters, command_with_args, @r###"
uv_snapshot!(context.filters(), command_with_args, @r###"
success: true
exit_code: 0
----- stdout -----
@ -113,7 +101,7 @@ fn run_with_python_version() -> Result<()> {
----- stderr -----
Removing virtual environment at: [VENV]/
Using Python 3.11.[X] interpreter at: [PYTHON]
Using Python 3.11.[X] interpreter at: [PYTHON-3.11]
Creating virtualenv at: [VENV]/
Resolved 5 packages in [TIME]
Downloaded 4 packages in [TIME]

View file

@ -7,110 +7,85 @@ mod common;
#[test]
fn toolchain_find() {
let context: TestContext = TestContext::new("3.12");
let context: TestContext = TestContext::new_with_versions(&["3.11", "3.12"]);
// No interpreters on the path
uv_snapshot!(context.filters(), context.toolchain_find(), @r###"
success: false
exit_code: 2
----- stdout -----
----- stderr -----
error: No Python interpreters found in provided path, active virtual environment, or search path
"###);
let python_path = python_path_with_versions(&context.temp_dir, &["3.11", "3.12"])
.expect("Failed to create Python test path");
// Create some filters for the test interpreters, otherwise they'll be a path on the dev's machine
// TODO(zanieb): Standardize this when writing more tests
let python_path_filters = std::env::split_paths(&python_path)
.zip(["3.11", "3.12"])
.flat_map(|(path, version)| {
TestContext::path_patterns(path)
.into_iter()
.map(move |pattern| {
(
format!("{pattern}python.*"),
format!("[PYTHON-PATH-{version}]"),
)
})
})
.collect::<Vec<_>>();
let filters = python_path_filters
.iter()
.map(|(pattern, replacement)| (pattern.as_str(), replacement.as_str()))
.chain(context.filters())
.collect::<Vec<_>>();
// We find the first interpreter on the path
uv_snapshot!(filters, context.toolchain_find()
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-PATH-3.11]
[PYTHON-3.11]
----- stderr -----
"###);
// We find the first interpreter on the path
uv_snapshot!(context.filters(), context.toolchain_find()
, @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-3.11]
----- stderr -----
"###);
// Request Python 3.12
uv_snapshot!(filters, context.toolchain_find()
uv_snapshot!(context.filters(), context.toolchain_find()
.arg("3.12")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
, @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-PATH-3.12]
[PYTHON-3.12]
----- stderr -----
"###);
// Request Python 3.11
uv_snapshot!(filters, context.toolchain_find()
uv_snapshot!(context.filters(), context.toolchain_find()
.arg("3.11")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
, @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-PATH-3.11]
[PYTHON-3.11]
----- stderr -----
"###);
// Request CPython
uv_snapshot!(filters, context.toolchain_find()
uv_snapshot!(context.filters(), context.toolchain_find()
.arg("cpython")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
, @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-PATH-3.11]
[PYTHON-3.11]
----- stderr -----
"###);
// Request CPython 3.12
uv_snapshot!(filters, context.toolchain_find()
uv_snapshot!(context.filters(), context.toolchain_find()
.arg("cpython@3.12")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
, @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-PATH-3.12]
[PYTHON-3.12]
----- stderr -----
"###);
// Request CPython 3.12 via partial key syntax
uv_snapshot!(filters, context.toolchain_find()
uv_snapshot!(context.filters(), context.toolchain_find()
.arg("cpython-3.12")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
, @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-PATH-3.12]
[PYTHON-3.12]
----- stderr -----
"###);
@ -119,21 +94,21 @@ fn toolchain_find() {
let os = Os::from_env();
let arch = Arch::from_env();
uv_snapshot!(filters, context.toolchain_find()
uv_snapshot!(context.filters(), context.toolchain_find()
.arg(format!("cpython-3.12-{os}-{arch}"))
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
, @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-PATH-3.12]
[PYTHON-3.12]
----- stderr -----
"###);
// Request PyPy
uv_snapshot!(filters, context.toolchain_find()
uv_snapshot!(context.filters(), context.toolchain_find()
.arg("pypy")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
, @r###"
success: false
exit_code: 2
----- stdout -----
@ -142,28 +117,27 @@ fn toolchain_find() {
error: No interpreter found for PyPy in provided path, active virtual environment, or search path
"###);
// Swap the order (but don't change the filters to preserve our indices)
// Swap the order of the Python versions
let python_path = python_path_with_versions(&context.temp_dir, &["3.12", "3.11"])
.expect("Failed to create Python test path");
uv_snapshot!(filters, context.toolchain_find()
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
uv_snapshot!(context.filters(), context.toolchain_find().env("UV_TEST_PYTHON_PATH", python_path), @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-PATH-3.12]
[PYTHON-3.12]
----- stderr -----
"###);
// Request Python 3.11
uv_snapshot!(filters, context.toolchain_find()
uv_snapshot!(context.filters(), context.toolchain_find()
.arg("3.11")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
, @r###"
success: true
exit_code: 0
----- stdout -----
[PYTHON-PATH-3.11]
[PYTHON-3.11]
----- stderr -----
"###);