Add test cases for `find_uv_bin` (#15110)

Adds test cases to unblock

- https://github.com/astral-sh/uv/pull/14181
- https://github.com/astral-sh/uv/pull/14182
- https://github.com/astral-sh/uv/pull/14184
- https://github.com/astral-sh/uv/pull/14184
- https://github.com/tox-dev/pre-commit-uv/issues/70

We use a package with a symlink to the Python module to get a mock
installation of uv without building (or packaging) the uv binary. This
lets us test real patterns like `uv pip install --prefix` without
encoding logic about where things are placed during those installs.

---------

Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
Zanie Blue 2025-08-07 07:14:01 -05:00 committed by GitHub
parent aec90f0a3c
commit 278295ef02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 442 additions and 75 deletions

View File

@ -465,6 +465,12 @@ impl TestContext {
self self
} }
/// Add a custom filter to the `TestContext`.
pub fn with_filter(mut self, filter: (String, String)) -> Self {
self.filters.push(filter);
self
}
/// Clear filters on `TestContext`. /// Clear filters on `TestContext`.
pub fn clear_filters(mut self) -> Self { pub fn clear_filters(mut self) -> Self {
self.filters.clear(); self.filters.clear();
@ -608,6 +614,26 @@ impl TestContext {
.into_iter() .into_iter()
.map(|pattern| (pattern, "[VENV]/".to_string())), .map(|pattern| (pattern, "[VENV]/".to_string())),
); );
// Account for [`Simplified::user_display`] which is relative to the command working directory
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 Python library path differences between Windows and Unix
filters.push((
r"[\\/]lib[\\/]python\d+\.\d+[\\/]".to_string(),
"/[PYTHON-LIB]/".to_string(),
));
filters.push((r"[\\/]Lib[\\/]".to_string(), "/[PYTHON-LIB]/".to_string()));
for (version, executable) in &python_versions { for (version, executable) in &python_versions {
// Add filtering for the interpreter path // Add filtering for the interpreter path
filters.extend( filters.extend(
@ -680,18 +706,6 @@ impl TestContext {
"Activate with: source $1/[BIN]/activate".to_string(), "Activate with: source $1/[BIN]/activate".to_string(),
)); ));
// Account for [`Simplified::user_display`] which is relative to the command working directory
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 // 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((r"(\\|\/)\.tmp.*(\\|\/)".to_string(), "/[TMP]/".to_string()));
@ -727,6 +741,11 @@ impl TestContext {
r"environments-v(\d+)[\\/](\w+)-[a-z0-9]+".to_string(), r"environments-v(\d+)[\\/](\w+)-[a-z0-9]+".to_string(),
"environments-v$1/$2-[HASH]".to_string(), "environments-v$1/$2-[HASH]".to_string(),
)); ));
// Filter archive hashes
filters.push((
r"archive-v(\d+)[\\/][A-Za-z0-9\-\_]+".to_string(),
"archive-v$1/[HASH]".to_string(),
));
Self { Self {
root: ChildPath::new(root.path()), root: ChildPath::new(root.path()),
@ -748,7 +767,7 @@ impl TestContext {
/// Create a uv command for testing. /// Create a uv command for testing.
pub fn command(&self) -> Command { pub fn command(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
} }
@ -826,6 +845,20 @@ impl TestContext {
.env_remove(EnvVars::UV_TOOL_BIN_DIR) .env_remove(EnvVars::UV_TOOL_BIN_DIR)
.env_remove(EnvVars::XDG_CONFIG_HOME) .env_remove(EnvVars::XDG_CONFIG_HOME)
.env_remove(EnvVars::XDG_DATA_HOME) .env_remove(EnvVars::XDG_DATA_HOME)
// I believe the intent of all tests is that they are run outside the
// context of an existing git repository. And when they aren't, state
// from the parent git repository can bleed into the behavior of `uv
// init` in a way that makes it difficult to test consistently. By
// setting GIT_CEILING_DIRECTORIES, we specifically prevent git from
// climbing up past the root of our test directory to look for any
// other git repos.
//
// If one wants to write a test specifically targeting uv within a
// pre-existing git repository, then the test should make the parent
// git repo explicitly. The GIT_CEILING_DIRECTORIES here shouldn't
// impact it, since it only prevents git from discovering repositories
// at or above the root.
.env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path())
.current_dir(self.temp_dir.path()); .current_dir(self.temp_dir.path());
for (key, value) in &self.extra_env { for (key, value) in &self.extra_env {
@ -844,7 +877,7 @@ impl TestContext {
/// Create a `pip compile` command for testing. /// Create a `pip compile` command for testing.
pub fn pip_compile(&self) -> Command { pub fn pip_compile(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("pip").arg("compile"); command.arg("pip").arg("compile");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -852,14 +885,14 @@ impl TestContext {
/// Create a `pip compile` command for testing. /// Create a `pip compile` command for testing.
pub fn pip_sync(&self) -> Command { pub fn pip_sync(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("pip").arg("sync"); command.arg("pip").arg("sync");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
} }
pub fn pip_show(&self) -> Command { pub fn pip_show(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("pip").arg("show"); command.arg("pip").arg("show");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -867,7 +900,7 @@ impl TestContext {
/// Create a `pip freeze` command with options shared across scenarios. /// Create a `pip freeze` command with options shared across scenarios.
pub fn pip_freeze(&self) -> Command { pub fn pip_freeze(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("pip").arg("freeze"); command.arg("pip").arg("freeze");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -875,14 +908,14 @@ impl TestContext {
/// Create a `pip check` command with options shared across scenarios. /// Create a `pip check` command with options shared across scenarios.
pub fn pip_check(&self) -> Command { pub fn pip_check(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("pip").arg("check"); command.arg("pip").arg("check");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
} }
pub fn pip_list(&self) -> Command { pub fn pip_list(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("pip").arg("list"); command.arg("pip").arg("list");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -890,7 +923,7 @@ impl TestContext {
/// Create a `uv venv` command /// Create a `uv venv` command
pub fn venv(&self) -> Command { pub fn venv(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("venv"); command.arg("venv");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -898,7 +931,7 @@ impl TestContext {
/// Create a `pip install` command with options shared across scenarios. /// Create a `pip install` command with options shared across scenarios.
pub fn pip_install(&self) -> Command { pub fn pip_install(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("pip").arg("install"); command.arg("pip").arg("install");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -906,7 +939,7 @@ impl TestContext {
/// Create a `pip uninstall` command with options shared across scenarios. /// Create a `pip uninstall` command with options shared across scenarios.
pub fn pip_uninstall(&self) -> Command { pub fn pip_uninstall(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("pip").arg("uninstall"); command.arg("pip").arg("uninstall");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -914,7 +947,7 @@ impl TestContext {
/// Create a `pip tree` command for testing. /// Create a `pip tree` command for testing.
pub fn pip_tree(&self) -> Command { pub fn pip_tree(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("pip").arg("tree"); command.arg("pip").arg("tree");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -923,7 +956,7 @@ impl TestContext {
/// Create a `uv help` command with options shared across scenarios. /// Create a `uv help` command with options shared across scenarios.
#[allow(clippy::unused_self)] #[allow(clippy::unused_self)]
pub fn help(&self) -> Command { pub fn help(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("help"); command.arg("help");
command.env_remove(EnvVars::UV_CACHE_DIR); command.env_remove(EnvVars::UV_CACHE_DIR);
command command
@ -932,7 +965,7 @@ impl TestContext {
/// Create a `uv init` command with options shared across scenarios and /// Create a `uv init` command with options shared across scenarios and
/// isolated from any git repository that may exist in a parent directory. /// isolated from any git repository that may exist in a parent directory.
pub fn init(&self) -> Command { pub fn init(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("init"); command.arg("init");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -940,7 +973,7 @@ impl TestContext {
/// Create a `uv sync` command with options shared across scenarios. /// Create a `uv sync` command with options shared across scenarios.
pub fn sync(&self) -> Command { pub fn sync(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("sync"); command.arg("sync");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -948,7 +981,7 @@ impl TestContext {
/// Create a `uv lock` command with options shared across scenarios. /// Create a `uv lock` command with options shared across scenarios.
pub fn lock(&self) -> Command { pub fn lock(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("lock"); command.arg("lock");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -956,7 +989,7 @@ impl TestContext {
/// Create a `uv export` command with options shared across scenarios. /// Create a `uv export` command with options shared across scenarios.
pub fn export(&self) -> Command { pub fn export(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("export"); command.arg("export");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -964,43 +997,44 @@ impl TestContext {
/// Create a `uv build` command with options shared across scenarios. /// Create a `uv build` command with options shared across scenarios.
pub fn build(&self) -> Command { pub fn build(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("build"); command.arg("build");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
} }
pub fn version(&self) -> Command { pub fn version(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("version"); command.arg("version");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
} }
pub fn self_version(&self) -> Command { pub fn self_version(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("self").arg("version"); command.arg("self").arg("version");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
} }
pub fn self_update(&self) -> Command { pub fn self_update(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("self").arg("update"); command.arg("self").arg("update");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
} }
/// Create a `uv publish` command with options shared across scenarios. /// Create a `uv publish` command with options shared across scenarios.
#[allow(clippy::unused_self)]
pub fn publish(&self) -> Command { pub fn publish(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("publish"); command.arg("publish");
command command
} }
/// Create a `uv python find` command with options shared across scenarios. /// Create a `uv python find` command with options shared across scenarios.
pub fn python_find(&self) -> Command { pub fn python_find(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command command
.arg("python") .arg("python")
.arg("find") .arg("find")
@ -1012,7 +1046,7 @@ impl TestContext {
/// Create a `uv python list` command with options shared across scenarios. /// Create a `uv python list` command with options shared across scenarios.
pub fn python_list(&self) -> Command { pub fn python_list(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command command
.arg("python") .arg("python")
.arg("list") .arg("list")
@ -1023,7 +1057,7 @@ impl TestContext {
/// Create a `uv python install` command with options shared across scenarios. /// Create a `uv python install` command with options shared across scenarios.
pub fn python_install(&self) -> Command { pub fn python_install(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command.arg("python").arg("install"); command.arg("python").arg("install");
command command
@ -1031,7 +1065,7 @@ impl TestContext {
/// Create a `uv python uninstall` command with options shared across scenarios. /// Create a `uv python uninstall` command with options shared across scenarios.
pub fn python_uninstall(&self) -> Command { pub fn python_uninstall(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command.arg("python").arg("uninstall"); command.arg("python").arg("uninstall");
command command
@ -1039,7 +1073,7 @@ impl TestContext {
/// Create a `uv python upgrade` command with options shared across scenarios. /// Create a `uv python upgrade` command with options shared across scenarios.
pub fn python_upgrade(&self) -> Command { pub fn python_upgrade(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command.arg("python").arg("upgrade"); command.arg("python").arg("upgrade");
command command
@ -1047,7 +1081,7 @@ impl TestContext {
/// Create a `uv python pin` command with options shared across scenarios. /// Create a `uv python pin` command with options shared across scenarios.
pub fn python_pin(&self) -> Command { pub fn python_pin(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("python").arg("pin"); command.arg("python").arg("pin");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -1055,7 +1089,7 @@ impl TestContext {
/// Create a `uv python dir` command with options shared across scenarios. /// Create a `uv python dir` command with options shared across scenarios.
pub fn python_dir(&self) -> Command { pub fn python_dir(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("python").arg("dir"); command.arg("python").arg("dir");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -1063,7 +1097,7 @@ impl TestContext {
/// Create a `uv run` command with options shared across scenarios. /// Create a `uv run` command with options shared across scenarios.
pub fn run(&self) -> Command { pub fn run(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1"); command.arg("run").env(EnvVars::UV_SHOW_RESOLUTION, "1");
self.add_shared_options(&mut command, true); self.add_shared_options(&mut command, true);
command command
@ -1071,7 +1105,7 @@ impl TestContext {
/// Create a `uv tool run` command with options shared across scenarios. /// Create a `uv tool run` command with options shared across scenarios.
pub fn tool_run(&self) -> Command { pub fn tool_run(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command command
.arg("tool") .arg("tool")
.arg("run") .arg("run")
@ -1082,7 +1116,7 @@ impl TestContext {
/// Create a `uv upgrade run` command with options shared across scenarios. /// Create a `uv upgrade run` command with options shared across scenarios.
pub fn tool_upgrade(&self) -> Command { pub fn tool_upgrade(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("tool").arg("upgrade"); command.arg("tool").arg("upgrade");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1090,7 +1124,7 @@ impl TestContext {
/// Create a `uv tool install` command with options shared across scenarios. /// Create a `uv tool install` command with options shared across scenarios.
pub fn tool_install(&self) -> Command { pub fn tool_install(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("tool").arg("install"); command.arg("tool").arg("install");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1098,7 +1132,7 @@ impl TestContext {
/// Create a `uv tool list` command with options shared across scenarios. /// Create a `uv tool list` command with options shared across scenarios.
pub fn tool_list(&self) -> Command { pub fn tool_list(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("tool").arg("list"); command.arg("tool").arg("list");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1106,7 +1140,7 @@ impl TestContext {
/// Create a `uv tool dir` command with options shared across scenarios. /// Create a `uv tool dir` command with options shared across scenarios.
pub fn tool_dir(&self) -> Command { pub fn tool_dir(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("tool").arg("dir"); command.arg("tool").arg("dir");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1114,7 +1148,7 @@ impl TestContext {
/// Create a `uv tool uninstall` command with options shared across scenarios. /// Create a `uv tool uninstall` command with options shared across scenarios.
pub fn tool_uninstall(&self) -> Command { pub fn tool_uninstall(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("tool").arg("uninstall"); command.arg("tool").arg("uninstall");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1122,7 +1156,7 @@ impl TestContext {
/// Create a `uv add` command for the given requirements. /// Create a `uv add` command for the given requirements.
pub fn add(&self) -> Command { pub fn add(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("add"); command.arg("add");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1130,7 +1164,7 @@ impl TestContext {
/// Create a `uv remove` command for the given requirements. /// Create a `uv remove` command for the given requirements.
pub fn remove(&self) -> Command { pub fn remove(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("remove"); command.arg("remove");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1138,7 +1172,7 @@ impl TestContext {
/// Create a `uv tree` command with options shared across scenarios. /// Create a `uv tree` command with options shared across scenarios.
pub fn tree(&self) -> Command { pub fn tree(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("tree"); command.arg("tree");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1146,7 +1180,7 @@ impl TestContext {
/// Create a `uv cache clean` command. /// Create a `uv cache clean` command.
pub fn clean(&self) -> Command { pub fn clean(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("cache").arg("clean"); command.arg("cache").arg("clean");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1154,7 +1188,7 @@ impl TestContext {
/// Create a `uv cache prune` command. /// Create a `uv cache prune` command.
pub fn prune(&self) -> Command { pub fn prune(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("cache").arg("prune"); command.arg("cache").arg("prune");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1164,7 +1198,7 @@ impl TestContext {
/// ///
/// Note that this command is hidden and only invoking it through a build frontend is supported. /// Note that this command is hidden and only invoking it through a build frontend is supported.
pub fn build_backend(&self) -> Command { pub fn build_backend(&self) -> Command {
let mut command = self.new_command(); let mut command = Self::new_command();
command.arg("build-backend"); command.arg("build-backend");
self.add_shared_options(&mut command, false); self.add_shared_options(&mut command, false);
command command
@ -1182,13 +1216,29 @@ impl TestContext {
} }
pub fn python_command(&self) -> Command { pub fn python_command(&self) -> Command {
let mut command = self.new_command_with(&self.interpreter()); let mut interpreter = self.interpreter();
// If there's not a virtual environment, use the first Python interpreter in the context
if !interpreter.exists() {
interpreter.clone_from(
&self
.python_versions
.first()
.expect("At least one Python version is required")
.1,
);
}
let mut command = Self::new_command_with(&interpreter);
command command
// Our tests change files in <1s, so we must disable CPython bytecode caching or we'll get stale files // 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 // https://github.com/python/cpython/issues/75953
.arg("-B") .arg("-B")
// Python on windows // Python on windows
.env(EnvVars::PYTHONUTF8, "1"); .env(EnvVars::PYTHONUTF8, "1");
self.add_shared_env(&mut command, false);
command command
} }
@ -1385,29 +1435,14 @@ impl TestContext {
/// Creates a new `Command` that is intended to be suitable for use in /// Creates a new `Command` that is intended to be suitable for use in
/// all tests. /// all tests.
fn new_command(&self) -> Command { fn new_command() -> Command {
self.new_command_with(&get_bin()) Self::new_command_with(&get_bin())
} }
/// Creates a new `Command` that is intended to be suitable for use in /// Creates a new `Command` that is intended to be suitable for use in
/// all tests, but with the given binary. /// all tests, but with the given binary.
fn new_command_with(&self, bin: &Path) -> Command { fn new_command_with(bin: &Path) -> Command {
let mut command = Command::new(bin); Command::new(bin)
// I believe the intent of all tests is that they are run outside the
// context of an existing git repository. And when they aren't, state
// from the parent git repository can bleed into the behavior of `uv
// init` in a way that makes it difficult to test consistently. By
// setting GIT_CEILING_DIRECTORIES, we specifically prevent git from
// climbing up past the root of our test directory to look for any
// other git repos.
//
// If one wants to write a test specifically targeting uv within a
// pre-existing git repository, then the test should make the parent
// git repo explicitly. The GIT_CEILING_DIRECTORIES here shouldn't
// impact it, since it only prevents git from discovering repositories
// at or above the root.
command.env(EnvVars::GIT_CEILING_DIRECTORIES, self.root.path());
command
} }
} }

View File

@ -78,6 +78,9 @@ mod python_find;
#[cfg(feature = "python")] #[cfg(feature = "python")]
mod python_list; mod python_list;
#[cfg(all(feature = "python", feature = "pypi"))]
mod python_module;
#[cfg(feature = "python-managed")] #[cfg(feature = "python-managed")]
mod python_install; mod python_install;

View File

@ -153,14 +153,14 @@ fn missing_record() -> Result<()> {
fs_err::remove_file(dist_info.join("RECORD"))?; fs_err::remove_file(dist_info.join("RECORD"))?;
uv_snapshot!(context.filters(), context.pip_uninstall() uv_snapshot!(context.filters(), context.pip_uninstall()
.arg("MarkupSafe"), @r###" .arg("MarkupSafe"), @r"
success: false success: false
exit_code: 2 exit_code: 2
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
error: Cannot uninstall package; `RECORD` file not found at: [SITE_PACKAGES]/MarkupSafe-2.1.3.dist-info/RECORD error: Cannot uninstall package; `RECORD` file not found at: [SITE_PACKAGES]/MarkupSafe-2.1.3.dist-info/RECORD
"### "
); );
Ok(()) Ok(())

View File

@ -0,0 +1,309 @@
use assert_cmd::assert::OutputAssertExt;
use assert_fs::prelude::{FileWriteStr, PathChild};
use indoc::{formatdoc, indoc};
use uv_fs::Simplified;
use uv_static::EnvVars;
use crate::common::{TestContext, site_packages_path, uv_snapshot};
/// Filter the user scheme, which differs between Windows and Unix.
fn user_scheme_bin_filter() -> (String, String) {
if cfg!(windows) {
(
r"\[USER_CONFIG_DIR\][\\/]Python[\\/]Python\d+".to_string(),
"[USER_SCHEME]".to_string(),
)
} else {
(r"\[HOME\]/\.local".to_string(), "[USER_SCHEME]".to_string())
}
}
#[test]
fn find_uv_bin_venv() {
let context = TestContext::new("3.12")
.with_filtered_python_names()
.with_filtered_virtualenv_bin()
.with_filtered_exe_suffix()
.with_filter(user_scheme_bin_filter());
// Install in a virtual environment
uv_snapshot!(context.filters(), context.pip_install()
.arg(context.workspace_root.join("scripts/packages/fake-uv")), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ uv==0.1.0 (from file://[WORKSPACE]/scripts/packages/fake-uv)
"
);
// We should find the binary in the virtual environment
uv_snapshot!(context.filters(), context.python_command()
.arg("-c")
.arg("import uv; print(uv.find_uv_bin())"), @r"
success: true
exit_code: 0
----- stdout -----
[VENV]/[BIN]/uv
----- stderr -----
"
);
}
#[test]
fn find_uv_bin_target() {
let context = TestContext::new("3.12")
.with_filtered_python_names()
.with_filtered_virtualenv_bin()
.with_filtered_exe_suffix()
.with_filter(user_scheme_bin_filter())
// Target installs always use "bin" on all platforms. On Windows,
// with_filtered_virtualenv_bin only filters "Scripts", not "bin"
.with_filter((r"[\\/]bin[\\/]".to_string(), "/[BIN]/".to_string()));
// Install in a target directory
uv_snapshot!(context.filters(), context.pip_install()
.arg(context.workspace_root.join("scripts/packages/fake-uv"))
.arg("--target")
.arg("target"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: .venv/[BIN]/[PYTHON]
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ uv==0.1.0 (from file://[WORKSPACE]/scripts/packages/fake-uv)
"
);
// We should find the binary in the target directory
uv_snapshot!(context.filters(), context.python_command()
.arg("-c")
.arg("import uv; print(uv.find_uv_bin())")
.env(EnvVars::PYTHONPATH, context.temp_dir.child("target").path()), @r"
success: true
exit_code: 0
----- stdout -----
[TEMP_DIR]/target/[BIN]/uv
----- stderr -----
"
);
}
#[test]
fn find_uv_bin_prefix() {
let context = TestContext::new("3.12")
.with_filtered_python_names()
.with_filtered_virtualenv_bin()
.with_filtered_exe_suffix()
.with_filter(user_scheme_bin_filter());
// Install in a prefix directory
let prefix = context.temp_dir.child("prefix");
uv_snapshot!(context.filters(), context.pip_install()
.arg(context.workspace_root.join("scripts/packages/fake-uv"))
.arg("--prefix")
.arg(prefix.path()), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using CPython 3.12.[X] interpreter at: .venv/[BIN]/[PYTHON]
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ uv==0.1.0 (from file://[WORKSPACE]/scripts/packages/fake-uv)
"
);
// We should find the binary in the prefix directory
uv_snapshot!(context.filters(), context.python_command()
.arg("-c")
.arg("import uv; print(uv.find_uv_bin())")
.env(
EnvVars::PYTHONPATH,
site_packages_path(&context.temp_dir.join("prefix"), "python3.12"),
), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "[TEMP_DIR]/prefix/[PYTHON-LIB]/site-packages/uv/_find_uv.py", line 36, in find_uv_bin
raise FileNotFoundError(path)
FileNotFoundError: [USER_SCHEME]/[BIN]/uv
"#
);
}
#[test]
fn find_uv_bin_base_prefix() {
let context = TestContext::new("3.12")
.with_filtered_python_names()
.with_filtered_virtualenv_bin()
.with_filtered_exe_suffix()
.with_filter(user_scheme_bin_filter());
// Test base prefix fallback by mutating sys.base_prefix
// First, create a "base" environment with fake-uv installed
let base_venv = context.temp_dir.child("base-venv");
context.venv().arg(base_venv.path()).assert().success();
// Install fake-uv in the "base" venv
uv_snapshot!(context.filters(), context.pip_install()
.arg("--python")
.arg(base_venv.path())
.arg(context.workspace_root.join("scripts/packages/fake-uv")), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Using Python 3.12.[X] environment at: base-venv
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ uv==0.1.0 (from file://[WORKSPACE]/scripts/packages/fake-uv)
"
);
context.venv().assert().success();
// Mutate `base_prefix` to simulate lookup in a system Python installation
uv_snapshot!(context.filters(), context.python_command()
.arg("-c")
.arg(format!(r#"import sys, uv; sys.base_prefix = "{}"; print(uv.find_uv_bin())"#, base_venv.path().portable_display()))
.env(EnvVars::PYTHONPATH, site_packages_path(base_venv.path(), "python3.12")), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "[TEMP_DIR]/base-venv/[PYTHON-LIB]/site-packages/uv/_find_uv.py", line 36, in find_uv_bin
raise FileNotFoundError(path)
FileNotFoundError: [USER_SCHEME]/[BIN]/uv
"#
);
}
#[test]
fn find_uv_bin_in_ephemeral_environment() -> anyhow::Result<()> {
let context = TestContext::new("3.12")
.with_filtered_python_names()
.with_filtered_virtualenv_bin()
.with_filtered_exe_suffix()
.with_filter(user_scheme_bin_filter());
// Create a minimal pyproject.toml
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! { r#"
[project]
name = "test-project"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = []
"#
})?;
// We should find the binary in an ephemeral `--with` environment
uv_snapshot!(context.filters(), context.run()
.arg("--with")
.arg(context.workspace_root.join("scripts/packages/fake-uv"))
.arg("python")
.arg("-c")
.arg("import uv; print(uv.find_uv_bin())"), @r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Audited in [TIME]
Resolved 1 package in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ uv==0.1.0 (from file://[WORKSPACE]/scripts/packages/fake-uv)
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "[CACHE_DIR]/archive-v0/[HASH]/[PYTHON-LIB]/site-packages/uv/_find_uv.py", line 36, in find_uv_bin
raise FileNotFoundError(path)
FileNotFoundError: [USER_SCHEME]/[BIN]/uv
"#
);
Ok(())
}
#[test]
fn find_uv_bin_in_parent_of_ephemeral_environment() -> anyhow::Result<()> {
let context = TestContext::new("3.12")
.with_filtered_python_names()
.with_filtered_virtualenv_bin()
.with_filtered_exe_suffix()
.with_filter(user_scheme_bin_filter());
// Add the fake-uv package as a dependency
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(&formatdoc! { r#"
[project]
name = "test-project"
version = "1.0.0"
requires-python = ">=3.8"
dependencies = ["uv"]
[tool.uv.sources]
uv = {{ path = "{}" }}
"#,
context.workspace_root.join("scripts/packages/fake-uv").portable_display()
})?;
// When running in an ephemeral environment, we should find the binary in the project
// environment
uv_snapshot!(context.filters(), context.run()
.arg("--with")
.arg("anyio")
.arg("python")
.arg("-c")
.arg("import uv; print(uv.find_uv_bin())"),
@r#"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Resolved 2 packages in [TIME]
Prepared 1 package in [TIME]
Installed 1 package in [TIME]
+ uv==0.1.0 (from file://[WORKSPACE]/scripts/packages/fake-uv)
Resolved 3 packages in [TIME]
Prepared 3 packages in [TIME]
Installed 3 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ sniffio==1.3.1
Traceback (most recent call last):
File "<string>", line 1, in <module>
File "[SITE_PACKAGES]/uv/_find_uv.py", line 36, in find_uv_bin
raise FileNotFoundError(path)
FileNotFoundError: [USER_SCHEME]/[BIN]/uv
"#
);
Ok(())
}

View File

@ -0,0 +1,3 @@
This fake uv package symlinks the Python module of uv in-tree and has a fake `uv` binary, allowing
testing of the Python module behaviors. Consumers can replace the `uv` binary with a debug binary or
similar if they need it to actually work.

View File

@ -0,0 +1,11 @@
[project]
name = "uv"
version = "0.1.0"
requires-python = ">=3.8"
[tool.uv.build-backend.data]
scripts = "scripts"
[build-system]
requires = ["uv_build>=0.8.0,<0.9"]
build-backend = "uv_build"

View File

@ -0,0 +1,4 @@
#!/usr/bin/env sh
echo "This is a fake uv binary"

View File

@ -0,0 +1 @@
This is a fake uv binary

View File

@ -0,0 +1 @@
../../../python/