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

View file

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

View file

@ -7,110 +7,85 @@ mod common;
#[test] #[test]
fn toolchain_find() { 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 // No interpreters on the path
uv_snapshot!(context.filters(), context.toolchain_find(), @r###" 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 success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- 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 ----- ----- stderr -----
"###); "###);
// Request Python 3.12 // Request Python 3.12
uv_snapshot!(filters, context.toolchain_find() uv_snapshot!(context.filters(), context.toolchain_find()
.arg("3.12") .arg("3.12")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
[PYTHON-PATH-3.12] [PYTHON-3.12]
----- stderr ----- ----- stderr -----
"###); "###);
// Request Python 3.11 // Request Python 3.11
uv_snapshot!(filters, context.toolchain_find() uv_snapshot!(context.filters(), context.toolchain_find()
.arg("3.11") .arg("3.11")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
[PYTHON-PATH-3.11] [PYTHON-3.11]
----- stderr ----- ----- stderr -----
"###); "###);
// Request CPython // Request CPython
uv_snapshot!(filters, context.toolchain_find() uv_snapshot!(context.filters(), context.toolchain_find()
.arg("cpython") .arg("cpython")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
[PYTHON-PATH-3.11] [PYTHON-3.11]
----- stderr ----- ----- stderr -----
"###); "###);
// Request CPython 3.12 // Request CPython 3.12
uv_snapshot!(filters, context.toolchain_find() uv_snapshot!(context.filters(), context.toolchain_find()
.arg("cpython@3.12") .arg("cpython@3.12")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
[PYTHON-PATH-3.12] [PYTHON-3.12]
----- stderr ----- ----- stderr -----
"###); "###);
// Request CPython 3.12 via partial key syntax // 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") .arg("cpython-3.12")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
[PYTHON-PATH-3.12] [PYTHON-3.12]
----- stderr ----- ----- stderr -----
"###); "###);
@ -119,21 +94,21 @@ fn toolchain_find() {
let os = Os::from_env(); let os = Os::from_env();
let arch = Arch::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}")) .arg(format!("cpython-3.12-{os}-{arch}"))
.env("UV_TEST_PYTHON_PATH", &python_path), @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
[PYTHON-PATH-3.12] [PYTHON-3.12]
----- stderr ----- ----- stderr -----
"###); "###);
// Request PyPy // Request PyPy
uv_snapshot!(filters, context.toolchain_find() uv_snapshot!(context.filters(), context.toolchain_find()
.arg("pypy") .arg("pypy")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###" , @r###"
success: false success: false
exit_code: 2 exit_code: 2
----- stdout ----- ----- stdout -----
@ -142,28 +117,27 @@ fn toolchain_find() {
error: No interpreter found for PyPy in provided path, active virtual environment, or search path 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"]) let python_path = python_path_with_versions(&context.temp_dir, &["3.12", "3.11"])
.expect("Failed to create Python test path"); .expect("Failed to create Python test path");
uv_snapshot!(filters, context.toolchain_find() uv_snapshot!(context.filters(), context.toolchain_find().env("UV_TEST_PYTHON_PATH", python_path), @r###"
.env("UV_TEST_PYTHON_PATH", &python_path), @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
[PYTHON-PATH-3.12] [PYTHON-3.12]
----- stderr ----- ----- stderr -----
"###); "###);
// Request Python 3.11 // Request Python 3.11
uv_snapshot!(filters, context.toolchain_find() uv_snapshot!(context.filters(), context.toolchain_find()
.arg("3.11") .arg("3.11")
.env("UV_TEST_PYTHON_PATH", &python_path), @r###" , @r###"
success: true success: true
exit_code: 0 exit_code: 0
----- stdout ----- ----- stdout -----
[PYTHON-PATH-3.11] [PYTHON-3.11]
----- stderr ----- ----- stderr -----
"###); "###);