diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index d0982642d..6705716de 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -465,6 +465,12 @@ impl TestContext { 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`. pub fn clear_filters(mut self) -> Self { self.filters.clear(); @@ -608,6 +614,26 @@ impl TestContext { .into_iter() .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 { // Add filtering for the interpreter path filters.extend( @@ -680,18 +706,6 @@ impl TestContext { "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 // Note we apply this _after_ all the full paths to avoid breaking their matching 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(), "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 { root: ChildPath::new(root.path()), @@ -748,7 +767,7 @@ impl TestContext { /// Create a uv command for testing. pub fn command(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); self.add_shared_options(&mut command, true); command } @@ -826,6 +845,20 @@ impl TestContext { .env_remove(EnvVars::UV_TOOL_BIN_DIR) .env_remove(EnvVars::XDG_CONFIG_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()); for (key, value) in &self.extra_env { @@ -844,7 +877,7 @@ impl TestContext { /// Create a `pip compile` command for testing. pub fn pip_compile(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("pip").arg("compile"); self.add_shared_options(&mut command, true); command @@ -852,14 +885,14 @@ impl TestContext { /// Create a `pip compile` command for testing. pub fn pip_sync(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("pip").arg("sync"); self.add_shared_options(&mut command, true); command } pub fn pip_show(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("pip").arg("show"); self.add_shared_options(&mut command, true); command @@ -867,7 +900,7 @@ impl TestContext { /// Create a `pip freeze` command with options shared across scenarios. pub fn pip_freeze(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("pip").arg("freeze"); self.add_shared_options(&mut command, true); command @@ -875,14 +908,14 @@ impl TestContext { /// Create a `pip check` command with options shared across scenarios. pub fn pip_check(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("pip").arg("check"); self.add_shared_options(&mut command, true); command } pub fn pip_list(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("pip").arg("list"); self.add_shared_options(&mut command, true); command @@ -890,7 +923,7 @@ impl TestContext { /// Create a `uv venv` command pub fn venv(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("venv"); self.add_shared_options(&mut command, false); command @@ -898,7 +931,7 @@ impl TestContext { /// Create a `pip install` command with options shared across scenarios. pub fn pip_install(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("pip").arg("install"); self.add_shared_options(&mut command, true); command @@ -906,7 +939,7 @@ impl TestContext { /// Create a `pip uninstall` command with options shared across scenarios. pub fn pip_uninstall(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("pip").arg("uninstall"); self.add_shared_options(&mut command, true); command @@ -914,7 +947,7 @@ impl TestContext { /// Create a `pip tree` command for testing. pub fn pip_tree(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("pip").arg("tree"); self.add_shared_options(&mut command, true); command @@ -923,7 +956,7 @@ impl TestContext { /// Create a `uv help` command with options shared across scenarios. #[allow(clippy::unused_self)] pub fn help(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("help"); command.env_remove(EnvVars::UV_CACHE_DIR); command @@ -932,7 +965,7 @@ impl TestContext { /// Create a `uv init` command with options shared across scenarios and /// isolated from any git repository that may exist in a parent directory. pub fn init(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("init"); self.add_shared_options(&mut command, false); command @@ -940,7 +973,7 @@ impl TestContext { /// Create a `uv sync` command with options shared across scenarios. pub fn sync(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("sync"); self.add_shared_options(&mut command, false); command @@ -948,7 +981,7 @@ impl TestContext { /// Create a `uv lock` command with options shared across scenarios. pub fn lock(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("lock"); self.add_shared_options(&mut command, false); command @@ -956,7 +989,7 @@ impl TestContext { /// Create a `uv export` command with options shared across scenarios. pub fn export(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("export"); self.add_shared_options(&mut command, false); command @@ -964,43 +997,44 @@ impl TestContext { /// Create a `uv build` command with options shared across scenarios. pub fn build(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("build"); self.add_shared_options(&mut command, false); command } pub fn version(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("version"); self.add_shared_options(&mut command, false); command } pub fn self_version(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("self").arg("version"); self.add_shared_options(&mut command, false); command } pub fn self_update(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("self").arg("update"); self.add_shared_options(&mut command, false); command } /// Create a `uv publish` command with options shared across scenarios. + #[allow(clippy::unused_self)] pub fn publish(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("publish"); command } /// Create a `uv python find` command with options shared across scenarios. pub fn python_find(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command .arg("python") .arg("find") @@ -1012,7 +1046,7 @@ impl TestContext { /// Create a `uv python list` command with options shared across scenarios. pub fn python_list(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command .arg("python") .arg("list") @@ -1023,7 +1057,7 @@ impl TestContext { /// Create a `uv python install` command with options shared across scenarios. 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); command.arg("python").arg("install"); command @@ -1031,7 +1065,7 @@ impl TestContext { /// Create a `uv python uninstall` command with options shared across scenarios. 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); command.arg("python").arg("uninstall"); command @@ -1039,7 +1073,7 @@ impl TestContext { /// Create a `uv python upgrade` command with options shared across scenarios. 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); command.arg("python").arg("upgrade"); command @@ -1047,7 +1081,7 @@ impl TestContext { /// Create a `uv python pin` command with options shared across scenarios. pub fn python_pin(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("python").arg("pin"); self.add_shared_options(&mut command, true); command @@ -1055,7 +1089,7 @@ impl TestContext { /// Create a `uv python dir` command with options shared across scenarios. pub fn python_dir(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("python").arg("dir"); self.add_shared_options(&mut command, true); command @@ -1063,7 +1097,7 @@ impl TestContext { /// Create a `uv run` command with options shared across scenarios. 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"); self.add_shared_options(&mut command, true); command @@ -1071,7 +1105,7 @@ impl TestContext { /// Create a `uv tool run` command with options shared across scenarios. pub fn tool_run(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command .arg("tool") .arg("run") @@ -1082,7 +1116,7 @@ impl TestContext { /// Create a `uv upgrade run` command with options shared across scenarios. pub fn tool_upgrade(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("tool").arg("upgrade"); self.add_shared_options(&mut command, false); command @@ -1090,7 +1124,7 @@ impl TestContext { /// Create a `uv tool install` command with options shared across scenarios. pub fn tool_install(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("tool").arg("install"); self.add_shared_options(&mut command, false); command @@ -1098,7 +1132,7 @@ impl TestContext { /// Create a `uv tool list` command with options shared across scenarios. pub fn tool_list(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("tool").arg("list"); self.add_shared_options(&mut command, false); command @@ -1106,7 +1140,7 @@ impl TestContext { /// Create a `uv tool dir` command with options shared across scenarios. pub fn tool_dir(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("tool").arg("dir"); self.add_shared_options(&mut command, false); command @@ -1114,7 +1148,7 @@ impl TestContext { /// Create a `uv tool uninstall` command with options shared across scenarios. pub fn tool_uninstall(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("tool").arg("uninstall"); self.add_shared_options(&mut command, false); command @@ -1122,7 +1156,7 @@ impl TestContext { /// Create a `uv add` command for the given requirements. pub fn add(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("add"); self.add_shared_options(&mut command, false); command @@ -1130,7 +1164,7 @@ impl TestContext { /// Create a `uv remove` command for the given requirements. pub fn remove(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("remove"); self.add_shared_options(&mut command, false); command @@ -1138,7 +1172,7 @@ impl TestContext { /// Create a `uv tree` command with options shared across scenarios. pub fn tree(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("tree"); self.add_shared_options(&mut command, false); command @@ -1146,7 +1180,7 @@ impl TestContext { /// Create a `uv cache clean` command. pub fn clean(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("cache").arg("clean"); self.add_shared_options(&mut command, false); command @@ -1154,7 +1188,7 @@ impl TestContext { /// Create a `uv cache prune` command. pub fn prune(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("cache").arg("prune"); self.add_shared_options(&mut command, false); command @@ -1164,7 +1198,7 @@ impl TestContext { /// /// Note that this command is hidden and only invoking it through a build frontend is supported. pub fn build_backend(&self) -> Command { - let mut command = self.new_command(); + let mut command = Self::new_command(); command.arg("build-backend"); self.add_shared_options(&mut command, false); command @@ -1182,13 +1216,29 @@ impl TestContext { } 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 // 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"); + + self.add_shared_env(&mut command, false); + command } @@ -1385,29 +1435,14 @@ impl TestContext { /// Creates a new `Command` that is intended to be suitable for use in /// all tests. - fn new_command(&self) -> Command { - self.new_command_with(&get_bin()) + fn new_command() -> Command { + Self::new_command_with(&get_bin()) } /// Creates a new `Command` that is intended to be suitable for use in /// all tests, but with the given binary. - fn new_command_with(&self, bin: &Path) -> Command { - let mut command = 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 + fn new_command_with(bin: &Path) -> Command { + Command::new(bin) } } diff --git a/crates/uv/tests/it/main.rs b/crates/uv/tests/it/main.rs index 872c88d4b..ad067bc11 100644 --- a/crates/uv/tests/it/main.rs +++ b/crates/uv/tests/it/main.rs @@ -78,6 +78,9 @@ mod python_find; #[cfg(feature = "python")] mod python_list; +#[cfg(all(feature = "python", feature = "pypi"))] +mod python_module; + #[cfg(feature = "python-managed")] mod python_install; diff --git a/crates/uv/tests/it/pip_uninstall.rs b/crates/uv/tests/it/pip_uninstall.rs index c72b92876..fdcd4e249 100644 --- a/crates/uv/tests/it/pip_uninstall.rs +++ b/crates/uv/tests/it/pip_uninstall.rs @@ -153,14 +153,14 @@ fn missing_record() -> Result<()> { fs_err::remove_file(dist_info.join("RECORD"))?; uv_snapshot!(context.filters(), context.pip_uninstall() - .arg("MarkupSafe"), @r###" + .arg("MarkupSafe"), @r" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- error: Cannot uninstall package; `RECORD` file not found at: [SITE_PACKAGES]/MarkupSafe-2.1.3.dist-info/RECORD - "### + " ); Ok(()) diff --git a/crates/uv/tests/it/python_module.rs b/crates/uv/tests/it/python_module.rs new file mode 100644 index 000000000..06545ffd0 --- /dev/null +++ b/crates/uv/tests/it/python_module.rs @@ -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 "", line 1, in + 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 "", line 1, in + 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 "", line 1, in + 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 "", line 1, in + File "[SITE_PACKAGES]/uv/_find_uv.py", line 36, in find_uv_bin + raise FileNotFoundError(path) + FileNotFoundError: [USER_SCHEME]/[BIN]/uv + "# + ); + + Ok(()) +} diff --git a/scripts/packages/fake-uv/README.md b/scripts/packages/fake-uv/README.md new file mode 100644 index 000000000..a9a444156 --- /dev/null +++ b/scripts/packages/fake-uv/README.md @@ -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. diff --git a/scripts/packages/fake-uv/pyproject.toml b/scripts/packages/fake-uv/pyproject.toml new file mode 100644 index 000000000..75c4e14c1 --- /dev/null +++ b/scripts/packages/fake-uv/pyproject.toml @@ -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" diff --git a/scripts/packages/fake-uv/scripts/uv b/scripts/packages/fake-uv/scripts/uv new file mode 100755 index 000000000..4abad952e --- /dev/null +++ b/scripts/packages/fake-uv/scripts/uv @@ -0,0 +1,4 @@ +#!/usr/bin/env sh + +echo "This is a fake uv binary" + diff --git a/scripts/packages/fake-uv/scripts/uv.exe b/scripts/packages/fake-uv/scripts/uv.exe new file mode 100644 index 000000000..9c5bbf847 --- /dev/null +++ b/scripts/packages/fake-uv/scripts/uv.exe @@ -0,0 +1 @@ +This is a fake uv binary diff --git a/scripts/packages/fake-uv/src b/scripts/packages/fake-uv/src new file mode 120000 index 000000000..e22ace59e --- /dev/null +++ b/scripts/packages/fake-uv/src @@ -0,0 +1 @@ +../../../python/ \ No newline at end of file