From 05ed7ac64bb5cf253bd58d66e5dbe12abec047b6 Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 1 Apr 2024 16:07:25 +0200 Subject: [PATCH] Don't use exclude newer manually in test functions (#2697) With this change, all usages of `EXCLUDE_NEWER` are now in command wrappers, not in the test functions themselves. For the venv test, i refactored them into the same kind of test context abstraction that the other test modules have in the second commit. The third commit makes`"INSTA_FILTERS` "private", removing the last remaining individual usage. Pending windows CI :crossed_fingers: --- crates/uv/tests/common/mod.rs | 41 ++- crates/uv/tests/pip_compile.rs | 33 +-- crates/uv/tests/pip_install.rs | 308 +++++++++----------- crates/uv/tests/venv.rs | 518 ++++++++++----------------------- 4 files changed, 338 insertions(+), 562 deletions(-) diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index b746eee1c..0e95d984a 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -23,6 +23,7 @@ use uv_interpreter::find_requested_python; // Exclude any packages uploaded after this date. pub static EXCLUDE_NEWER: &str = "2024-03-25T00:00:00Z"; +#[doc(hidden)] // Macro and test context only, don't use directly. pub const INSTA_FILTERS: &[(&str, &str)] = &[ (r"--cache-dir [^\s]+", "--cache-dir [CACHE_DIR]"), // Operation times @@ -157,6 +158,38 @@ impl TestContext { cmd } + /// Create a `pip install` command with options shared across scenarios. + pub fn install(&self) -> std::process::Command { + let mut command = self.install_without_exclude_newer(); + command.arg("--exclude-newer").arg(EXCLUDE_NEWER); + command + } + + /// Create a `pip install` command with no `--exclude-newer` option. + /// + /// One should avoid using this in tests to the extent possible because + /// it can result in tests failing when the index state changes. Therefore, + /// if you use this, there should be some other kind of mitigation in place. + /// For example, pinning package versions. + pub fn install_without_exclude_newer(&self) -> std::process::Command { + let mut command = std::process::Command::new(get_bin()); + command + .arg("pip") + .arg("install") + .arg("--cache-dir") + .arg(self.cache_dir.path()) + .env("VIRTUAL_ENV", self.venv.as_os_str()) + .current_dir(&self.temp_dir); + + if cfg!(all(windows, debug_assertions)) { + // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the + // default windows stack of 1MB + command.env("UV_STACK_SIZE", (4 * 1024 * 1024).to_string()); + } + + command + } + /// Run the given python code and check whether it succeeds. pub fn assert_command(&self, command: &str) -> Assert { std::process::Command::new(venv_to_interpreter(&self.venv)) @@ -386,9 +419,9 @@ pub fn create_bin_with_executables( /// Execute the command and format its output status, stdout and stderr into a snapshot string. /// /// This function is derived from `insta_cmd`s `spawn_with_info`. -pub fn run_and_format<'a>( +pub fn run_and_format>( mut command: impl BorrowMut, - filters: impl AsRef<[(&'a str, &'a str)]>, + filters: impl AsRef<[(T, T)]>, windows_filters: bool, ) -> (String, Output) { let program = command @@ -411,9 +444,9 @@ pub fn run_and_format<'a>( for (matcher, replacement) in filters.as_ref() { // TODO(konstin): Cache regex compilation - let re = Regex::new(matcher).expect("Do you need to regex::escape your filter?"); + let re = Regex::new(matcher.as_ref()).expect("Do you need to regex::escape your filter?"); if re.is_match(&snapshot) { - snapshot = re.replace_all(&snapshot, *replacement).to_string(); + snapshot = re.replace_all(&snapshot, replacement.as_ref()).to_string(); } } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index c1b02e1da..9c6a44154 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -15,7 +15,7 @@ use url::Url; use common::{uv_snapshot, TestContext}; use uv_fs::Simplified; -use crate::common::{get_bin, EXCLUDE_NEWER}; +use crate::common::get_bin; mod common; @@ -3034,7 +3034,7 @@ fn recursive_extras_direct_url() -> Result<()> { let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str("black[dev] @ ../../scripts/packages/black_editable")?; - let mut command = Command::new(get_bin()); + let mut command = context.compile(); if cfg!(all(windows, debug_assertions)) { // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the // default windows stack of 1MB @@ -3042,19 +3042,13 @@ fn recursive_extras_direct_url() -> Result<()> { } uv_snapshot!(context.filters(), command - .arg("pip") - .arg("compile") - .arg(requirements_in.path()) - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("VIRTUAL_ENV", context.venv.as_os_str()), @r###" + .arg(requirements_in.path()) + .current_dir(current_dir().unwrap()), @r###" success: true exit_code: 0 ----- stdout ----- # This file was autogenerated by uv via the following command: - # uv pip compile [TEMP_DIR]/requirements.in --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z [TEMP_DIR]/requirements.in aiohttp==3.9.3 # via black aiosignal==1.3.1 @@ -6450,21 +6444,8 @@ fn local_version_of_remote_package() -> Result<()> { "###); // Actually install the local dependency - // TODO(zanieb): We should have an `install` utility on the context instead of doing this - let mut command = Command::new(get_bin()); - command - .arg("pip") - .arg("install") - .arg(root_path.join("anyio_local")) - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("VIRTUAL_ENV", context.venv.as_os_str()) - .current_dir(context.temp_dir.path()); - if cfg!(all(windows, debug_assertions)) { - command.env("UV_STACK_SIZE", (8 * 1024 * 1024).to_string()); - } + let mut command = context.install(); + command.arg(root_path.join("anyio_local")); uv_snapshot!( context.filters(), command, @r###" diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index e743b9d25..3aa704244 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -9,7 +9,7 @@ use itertools::Itertools; use std::process::Command; -use common::{uv_snapshot, TestContext, EXCLUDE_NEWER}; +use common::{uv_snapshot, TestContext}; use uv_fs::Simplified; use crate::common::get_bin; @@ -34,38 +34,6 @@ fn decode_token(content: &[&str]) -> String { token } -/// Create a `pip install` command with options shared across scenarios. -fn command(context: &TestContext) -> Command { - let mut command = command_without_exclude_newer(context); - command.arg("--exclude-newer").arg(EXCLUDE_NEWER); - command -} - -/// Create a `pip install` command with no `--exclude-newer` option. -/// -/// One should avoid using this in tests to the extent possible because -/// it can result in tests failing when the index state changes. Therefore, -/// if you use this, there should be some other kind of mitigation in place. -/// For example, pinning package versions. -fn command_without_exclude_newer(context: &TestContext) -> Command { - let mut command = Command::new(get_bin()); - command - .arg("pip") - .arg("install") - .arg("--cache-dir") - .arg(context.cache_dir.path()) - .env("VIRTUAL_ENV", context.venv.as_os_str()) - .current_dir(&context.temp_dir); - - if cfg!(all(windows, debug_assertions)) { - // TODO(konstin): Reduce stack usage in debug mode enough that the tests pass with the - // default windows stack of 1MB - command.env("UV_STACK_SIZE", (4 * 1024 * 1024).to_string()); - } - - command -} - /// Create a `pip uninstall` command with options shared across scenarios. fn uninstall_command(context: &TestContext) -> Command { let mut command = Command::new(get_bin()); @@ -91,7 +59,7 @@ fn missing_requirements_txt() { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -114,7 +82,7 @@ fn empty_requirements_txt() -> Result<()> { let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.touch()?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -135,7 +103,7 @@ fn empty_requirements_txt() -> Result<()> { fn missing_pyproject_toml() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("pyproject.toml"), @r###" success: false @@ -155,7 +123,7 @@ fn invalid_pyproject_toml_syntax() -> Result<()> { let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str("123 - 456")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("pyproject.toml"), @r###" success: false @@ -182,7 +150,7 @@ fn invalid_pyproject_toml_schema() -> Result<()> { let pyproject_toml = context.temp_dir.child("pyproject.toml"); pyproject_toml.write_str("[project]")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("pyproject.toml"), @r###" success: false @@ -219,7 +187,7 @@ dependencies = ["flask==1.0.x"] .chain(context.filters()) .collect::>(); - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() .arg("-r") .arg("pyproject.toml"), @r###" success: false @@ -289,7 +257,7 @@ dependencies = ["flask==1.0.x"] fn no_solution() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("flask>=3.0.2") .arg("WerkZeug<1.0.0") .arg("--strict"), @r###" @@ -313,7 +281,7 @@ fn install_package() { let context = TestContext::new("3.12"); // Install Flask. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("Flask") .arg("--strict"), @r###" success: true @@ -346,7 +314,7 @@ fn install_requirements_txt() -> Result<()> { let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str("Flask")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -374,7 +342,7 @@ fn install_requirements_txt() -> Result<()> { let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str("Jinja2")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -402,7 +370,7 @@ fn respect_installed_and_reinstall() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_str("Flask==2.3.2")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -431,7 +399,7 @@ fn respect_installed_and_reinstall() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_str("Flask")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -461,7 +429,7 @@ fn respect_installed_and_reinstall() -> Result<()> { } else { context.filters() }; - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -483,7 +451,7 @@ fn respect_installed_and_reinstall() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_str("Flask")?; - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() .arg("-r") .arg("requirements.txt") .arg("--reinstall-package") @@ -507,7 +475,7 @@ fn respect_installed_and_reinstall() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_str("Flask")?; - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() .arg("-r") .arg("requirements.txt") .arg("--reinstall-package") @@ -537,7 +505,7 @@ fn reinstall_extras() -> Result<()> { let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str("httpx")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -566,7 +534,7 @@ fn reinstall_extras() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_str("httpx[http2]")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -599,7 +567,7 @@ fn reinstall_incomplete() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_str("anyio==3.7.0")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt"), @r###" success: true @@ -624,7 +592,7 @@ fn reinstall_incomplete() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_str("anyio==4.0.0")?; - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("-r") .arg("requirements.txt"), @r###" success: true @@ -654,7 +622,7 @@ fn allow_incompatibilities() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_str("Flask")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -683,7 +651,7 @@ fn allow_incompatibilities() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_str("jinja2==2.11.3")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -712,7 +680,7 @@ fn install_editable() { let context = TestContext::new("3.12"); // Install the editable package. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("-e") .arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###" success: true @@ -732,7 +700,7 @@ fn install_editable() { ); // Install it again (no-op). - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("-e") .arg(context.workspace_root.join("scripts/packages/poetry_editable")), @r###" success: true @@ -745,7 +713,7 @@ fn install_editable() { ); // Add another, non-editable dependency. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("-e") .arg(context.workspace_root.join("scripts/packages/poetry_editable")) .arg("black"), @r###" @@ -775,7 +743,7 @@ fn install_editable_and_registry() { let context = TestContext::new("3.12"); // Install the registry-based version of Black. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("black"), @r###" success: true exit_code: 0 @@ -795,7 +763,7 @@ fn install_editable_and_registry() { ); // Install the editable version of Black. This should remove the registry-based version. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("-e") .arg(context.workspace_root.join("scripts/packages/black_editable")), @r###" success: true @@ -813,7 +781,7 @@ fn install_editable_and_registry() { // Re-install the registry-based version of Black. This should be a no-op, since we have a // version of Black installed (the editable version) that satisfies the requirements. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("black") .arg("--strict"), @r###" success: true @@ -835,7 +803,7 @@ fn install_editable_and_registry() { .collect(); // Re-install Black at a specific version. This should replace the editable version. - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() .arg("black==23.10.0"), @r###" success: true exit_code: 0 @@ -856,7 +824,7 @@ fn install_editable_no_binary() { let context = TestContext::new("3.12"); // Install the editable package with no-binary enabled - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("-e") .arg(context.workspace_root.join("scripts/packages/black_editable")) .arg("--no-binary") @@ -888,7 +856,7 @@ fn reinstall_build_system() -> Result<()> { " })?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("--reinstall") .arg("-r") .arg("requirements.txt") @@ -920,7 +888,7 @@ fn reinstall_build_system() -> Result<()> { fn install_no_index() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("Flask") .arg("--no-index"), @r###" success: false @@ -947,7 +915,7 @@ fn install_no_index() { fn install_no_index_version() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("Flask==3.0.0") .arg("--no-index"), @r###" success: false @@ -986,7 +954,7 @@ fn install_no_index_version() { fn install_extra_index_url_has_priority() { let context = TestContext::new("3.12"); - uv_snapshot!(command_without_exclude_newer(&context) + uv_snapshot!(context.install_without_exclude_newer() .arg("--index-url") .arg("https://test.pypi.org/simple") .arg("--extra-index-url") @@ -1023,11 +991,11 @@ fn install_extra_index_url_has_priority() { fn install_git_public_https() { let context = TestContext::new("3.8"); - let mut command = command(&context); - command.arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"); - - uv_snapshot!(command - , @r###" + uv_snapshot!( + context + .install() + .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), + @r###" success: true exit_code: 0 ----- stdout ----- @@ -1053,7 +1021,7 @@ fn install_git_public_https_missing_branch_or_tag() { filters.push(("`git fetch .*`", "`git fetch [...]`")); filters.push(("exit status", "exit code")); - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() // 2.0.0 does not exist .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@2.0.0"), @r###" success: false @@ -1089,7 +1057,7 @@ fn install_git_public_https_missing_commit() { "", )); - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() // 2.0.0 does not exist .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@79a935a7a1a0ad6d0bdf72dce0e16cb0a24a1b3b") , @r###" @@ -1125,7 +1093,7 @@ fn install_git_private_https_pat() { "uv-private-pypackage @ git+https://{token}@github.com/astral-test/uv-private-pypackage" ); - uv_snapshot!(filters, command(&context).arg(package) + uv_snapshot!(filters, context.install().arg(package) , @r###" success: true exit_code: 0 @@ -1164,7 +1132,7 @@ fn install_git_private_https_pat_at_ref() { }; let package = format!("uv-private-pypackage @ git+https://{user}{token}@github.com/astral-test/uv-private-pypackage@6c09ce9ae81f50670a60abd7d95f30dd416d00ac"); - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() .arg(package), @r###" success: true exit_code: 0 @@ -1198,7 +1166,7 @@ fn install_git_private_https_pat_and_username() { .chain(context.filters()) .collect(); - uv_snapshot!(filters, command(&context).arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage")) + uv_snapshot!(filters, context.install().arg(format!("uv-private-pypackage @ git+https://{user}:{token}@github.com/astral-test/uv-private-pypackage")) , @r###" success: true exit_code: 0 @@ -1228,7 +1196,7 @@ fn install_git_private_https_pat_not_authorized() { // We provide a username otherwise (since the token is invalid), the git cli will prompt for a password // and hang the test - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() .arg(format!("uv-private-pypackage @ git+https://git:{token}@github.com/astral-test/uv-private-pypackage")) , @r###" success: false @@ -1254,9 +1222,11 @@ fn reinstall_no_binary() { let context = TestContext::new("3.12"); // The first installation should use a pre-built wheel - let mut command = command(&context); + let mut command = context.install(); command.arg("anyio").arg("--strict"); - uv_snapshot!(command, @r###" + uv_snapshot!( + command, + @r###" success: true exit_code: 0 ----- stdout ----- @@ -1275,7 +1245,7 @@ fn reinstall_no_binary() { // Running installation again with `--no-binary` should be a no-op // The first installation should use a pre-built wheel - let mut command = crate::command(&context); + let mut command = context.install(); command .arg("anyio") .arg("--no-binary") @@ -1305,7 +1275,7 @@ fn reinstall_no_binary() { context.filters() }; - let mut command = crate::command(&context); + let mut command = context.install(); command .arg("anyio") .arg("--no-binary") @@ -1343,7 +1313,7 @@ fn only_binary_requirements_txt() { }) .unwrap(); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -1369,7 +1339,7 @@ fn only_binary_requirements_txt() { fn install_executable() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("pylint==3.0.0"), @r###" success: true exit_code: 0 @@ -1403,7 +1373,7 @@ fn install_executable() { fn install_executable_copy() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("pylint==3.0.0") .arg("--link-mode") .arg("copy"), @r###" @@ -1439,7 +1409,7 @@ fn install_executable_copy() { fn install_executable_hardlink() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("pylint==3.0.0") .arg("--link-mode") .arg("hardlink"), @r###" @@ -1475,7 +1445,7 @@ fn no_deps() { let context = TestContext::new("3.12"); // Install Flask. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("Flask") .arg("--no-deps") .arg("--strict"), @r###" @@ -1505,7 +1475,7 @@ fn install_upgrade() { let context = TestContext::new("3.12"); // Install an old version of anyio and httpcore. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("anyio==3.6.2") .arg("httpcore==0.16.3") .arg("--strict"), @r###" @@ -1529,7 +1499,7 @@ fn install_upgrade() { context.assert_command("import anyio").success(); // Upgrade anyio. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("anyio") .arg("--upgrade-package") .arg("anyio"), @r###" @@ -1547,7 +1517,7 @@ fn install_upgrade() { ); // Upgrade anyio again, should not reinstall. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("anyio") .arg("--upgrade-package") .arg("anyio"), @r###" @@ -1562,7 +1532,7 @@ fn install_upgrade() { ); // Install httpcore, request anyio upgrade should not reinstall - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("httpcore") .arg("--upgrade-package") .arg("anyio"), @r###" @@ -1577,7 +1547,7 @@ fn install_upgrade() { ); // Upgrade httpcore with global flag - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("httpcore") .arg("--upgrade"), @r###" success: true @@ -1604,7 +1574,7 @@ fn install_constraints_txt() -> Result<()> { let constraints_txt = context.temp_dir.child("constraints.txt"); constraints_txt.write_str("idna<3.4")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--constraint") @@ -1636,7 +1606,7 @@ fn install_constraints_inline() -> Result<()> { let constraints_txt = context.temp_dir.child("constraints.txt"); constraints_txt.write_str("idna<3.4")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt"), @r###" success: true @@ -1661,7 +1631,7 @@ fn install_constraints_inline() -> Result<()> { fn install_constraints_remote() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-c") .arg("https://raw.githubusercontent.com/apache/airflow/constraints-2-6/constraints-3.11.txt") .arg("typing_extensions>=4.0"), @r###" @@ -1686,7 +1656,7 @@ fn install_constraints_inline_remote() -> Result<()> { let requirementstxt = context.temp_dir.child("requirements.txt"); requirementstxt.write_str("typing-extensions>=4.0\n-c https://raw.githubusercontent.com/apache/airflow/constraints-2-6/constraints-3.11.txt")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt"), @r###" success: true @@ -1708,7 +1678,7 @@ fn install_constraints_inline_remote() -> Result<()> { fn install_constraints_respects_offline_mode() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("--offline") .arg("-r") .arg("http://example.com/requirements.txt"), @r###" @@ -1736,7 +1706,7 @@ fn install_pinned_polars_invalid_metadata() { let context = TestContext::new("3.12"); // Install Flask. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("polars==0.14.0"), @r###" success: true @@ -1762,7 +1732,7 @@ fn install_sdist_resolution_lowest() -> Result<()> { let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.in") .arg("--resolution=lowest-direct"), @r###" @@ -1795,7 +1765,7 @@ fn direct_url_zip_file_bunk_permissions() -> Result<()> { "opensafely-pipeline @ https://github.com/opensafely-core/pipeline/archive/refs/tags/v2023.11.06.145820.zip", )?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--strict"), @r###" @@ -1834,7 +1804,7 @@ fn launcher() -> Result<()> { uv_snapshot!( filters, - command(&context) + context.install() .arg(format!("simple_launcher@{}", project_root.join("scripts/wheels/simple_launcher-0.1.0-py3-none-any.whl").display())) .arg("--strict"), @r###" success: true @@ -1879,7 +1849,7 @@ fn launcher_with_symlink() -> Result<()> { ]; uv_snapshot!(filters, - command(&context) + context.install() .arg(format!("simple_launcher@{}", project_root.join("scripts/wheels/simple_launcher-0.1.0-py3-none-any.whl").display())) .arg("--strict"), @r###" @@ -1937,7 +1907,7 @@ fn config_settings() { let context = TestContext::new("3.12"); // Install the editable package. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("-e") .arg(context.workspace_root.join("scripts/packages/setuptools_editable")), @r###" success: true @@ -1963,7 +1933,7 @@ fn config_settings() { // Install the editable package with `--editable_mode=compat`. let context = TestContext::new("3.12"); - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("-e") .arg(context.workspace_root.join("scripts/packages/setuptools_editable")) .arg("-C") @@ -2034,7 +2004,7 @@ fn reinstall_duplicate() -> Result<()> { )?; // Run `pip install`. - uv_snapshot!(command(&context1) + uv_snapshot!(context1.install() .arg("pip") .arg("--reinstall"), @r###" @@ -2060,7 +2030,7 @@ fn reinstall_duplicate() -> Result<()> { fn install_symlink() { let context = TestContext::new("3.12"); - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("pgpdump==1.5") .arg("--strict"), @r###" success: true @@ -2109,7 +2079,7 @@ requires-python = ">=3.8" "#, )?; - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("--editable") .arg(editable_dir.path()), @r###" success: true @@ -2129,7 +2099,7 @@ requires-python = ">=3.8" ); // Re-installing should be a no-op. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("--editable") .arg(editable_dir.path()), @r###" success: true @@ -2154,7 +2124,7 @@ requires-python = ">=3.8" )?; // Re-installing should update the package. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("--editable") .arg(editable_dir.path()), @r###" success: true @@ -2200,7 +2170,7 @@ dependencies = {file = ["requirements.txt"]} let requirements_txt = editable_dir.child("requirements.txt"); requirements_txt.write_str("anyio==4.0.0")?; - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("--editable") .arg(editable_dir.path()), @r###" success: true @@ -2220,7 +2190,7 @@ dependencies = {file = ["requirements.txt"]} ); // Re-installing should re-install. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("--editable") .arg(editable_dir.path()), @r###" success: true @@ -2240,7 +2210,7 @@ dependencies = {file = ["requirements.txt"]} requirements_txt.write_str("anyio==3.7.1")?; // Re-installing should update the package. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("--editable") .arg(editable_dir.path()), @r###" success: true @@ -2281,7 +2251,7 @@ requires-python = ">=3.8" "#, )?; - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("example @ .") .current_dir(editable_dir.path()), @r###" success: true @@ -2300,7 +2270,7 @@ requires-python = ">=3.8" ); // Re-installing should be a no-op. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("example @ .") .current_dir(editable_dir.path()), @r###" success: true @@ -2325,7 +2295,7 @@ requires-python = ">=3.8" )?; // Re-installing should update the package. - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("example @ .") .current_dir(editable_dir.path()), @r###" success: true @@ -2367,7 +2337,7 @@ requires-python = ">=3.11,<3.13" "#, )?; - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("--editable") .arg(editable_dir.path()), @r###" success: true @@ -2409,7 +2379,7 @@ requires-python = "<=3.8" "#, )?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("--editable") .arg(editable_dir.path()), @r###" success: false @@ -2435,7 +2405,7 @@ fn no_build_isolation() -> Result<()> { let filters = std::iter::once((r"exit code: 1", "exit status: 1")) .chain(context.filters()) .collect::>(); - uv_snapshot!(filters, command(&context) + uv_snapshot!(filters, context.install() .arg("-r") .arg("requirements.in") .arg("--no-build-isolation"), @r###" @@ -2458,7 +2428,7 @@ fn no_build_isolation() -> Result<()> { ); // Install `setuptools` and `wheel`. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("setuptools") .arg("wheel"), @r###" success: true @@ -2474,7 +2444,7 @@ fn no_build_isolation() -> Result<()> { "###); // We expect the build to succeed, since `setuptools` is now installed. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.in") .arg("--no-build-isolation"), @r###" @@ -2505,7 +2475,7 @@ fn install_utf16le_requirements() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_binary(&utf8_to_utf16_with_bom_le("tomli"))?; - uv_snapshot!(command_without_exclude_newer(&context) + uv_snapshot!(context.install_without_exclude_newer() .arg("-r") .arg("requirements.txt"), @r###" success: true @@ -2532,7 +2502,7 @@ fn install_utf16be_requirements() -> Result<()> { requirements_txt.touch()?; requirements_txt.write_binary(&utf8_to_utf16_with_bom_be("tomli"))?; - uv_snapshot!(command_without_exclude_newer(&context) + uv_snapshot!(context.install_without_exclude_newer() .arg("-r") .arg("requirements.txt"), @r###" success: true @@ -2576,7 +2546,7 @@ fn dry_run_install() -> std::result::Result<(), Box> { requirements_txt.touch()?; requirements_txt.write_str("httpx==0.25.1")?; - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("-r") .arg("requirements.txt") .arg("--dry-run") @@ -2609,7 +2579,7 @@ fn dry_run_install_url_dependency() -> std::result::Result<(), Box std::result::Result<(), Box std::result::Result<(), Box std::result::Result<(), Box std::result::Result<(), Box std::result::Result<(), Box std::result::Result<(), Box Result<()> { let pre_mtime_ns = metadata.mtime_nsec(); // Install a package. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("anyio") .arg("--strict"), @r###" success: true @@ -3014,7 +2984,7 @@ fn deptry_gitignore() { .workspace_root .join("scripts/packages/deptry_reproducer"); - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(format!("deptry_reproducer @ {}", source_dist_dir.join("deptry_reproducer-0.1.0.tar.gz").simplified_display())) .arg("--strict") .current_dir(source_dist_dir), @r###" @@ -3044,7 +3014,7 @@ fn reinstall_no_index() { let context = TestContext::new("3.12"); // Install anyio - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("anyio") .arg("--strict"), @r###" success: true @@ -3062,7 +3032,7 @@ fn reinstall_no_index() { ); // Install anyio again - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("anyio") .arg("--no-index") .arg("--strict"), @r###" @@ -3078,7 +3048,7 @@ fn reinstall_no_index() { // Reinstall // We should not consider the already installed package as a source and // should attempt to pull from the index - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("anyio") .arg("--no-index") .arg("--reinstall") @@ -3104,7 +3074,7 @@ fn already_installed_remote_dependencies() { let context = TestContext::new("3.12"); // Install anyio's dependencies. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("idna") .arg("sniffio") .arg("--strict"), @r###" @@ -3122,7 +3092,7 @@ fn already_installed_remote_dependencies() { ); // Install anyio. - uv_snapshot!(command(&context) + uv_snapshot!(context.install() .arg("anyio") .arg("--strict"), @r###" success: true @@ -3147,7 +3117,7 @@ fn already_installed_dependent_editable() { .join("scripts/packages/dependent_editables"); // Install the first editable - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("first_editable")), @r###" success: true exit_code: 0 @@ -3163,7 +3133,7 @@ fn already_installed_dependent_editable() { // Install the second editable which depends on the first editable // The already installed first editable package should satisfy the requirement - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("second_editable")) // Disable the index to guard this test against dependency confusion attacks .arg("--no-index") @@ -3183,7 +3153,7 @@ fn already_installed_dependent_editable() { // Request install of the first editable by full path again // We should audit the installed package - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("first_editable")), @r###" success: true exit_code: 0 @@ -3196,7 +3166,7 @@ fn already_installed_dependent_editable() { // Request reinstallation of the first package during install of the second // It's not available on an index and the user has not specified the path so we fail - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("second_editable")) .arg("--reinstall-package") .arg("first-editable") @@ -3221,7 +3191,7 @@ fn already_installed_dependent_editable() { // Request reinstallation of the first package // We include it in the install command with a full path so we should succeed - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("first_editable")) .arg("--reinstall-package") .arg("first-editable"), @r###" @@ -3247,7 +3217,7 @@ fn already_installed_local_path_dependent() { .join("scripts/packages/dependent_locals"); // Install the first local - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("first_local")), @r###" success: true exit_code: 0 @@ -3263,7 +3233,7 @@ fn already_installed_local_path_dependent() { // Install the second local which depends on the first local // The already installed first local package should satisfy the requirement - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("second_local")) // Disable the index to guard this test against dependency confusion attacks .arg("--no-index") @@ -3283,7 +3253,7 @@ fn already_installed_local_path_dependent() { // Request install of the first local by full path again // We should audit the installed package - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("first_local")), @r###" success: true exit_code: 0 @@ -3296,7 +3266,7 @@ fn already_installed_local_path_dependent() { // Request reinstallation of the first package during install of the second // It's not available on an index and the user has not specified the path so we fail - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("second_local")) .arg("--reinstall-package") .arg("first-local") @@ -3320,7 +3290,7 @@ fn already_installed_local_path_dependent() { // Request reinstallation of the first package // We include it in the install command with a full path so we succeed - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("second_local")) .arg(root_path.join("first_local")) .arg("--reinstall-package") @@ -3339,7 +3309,7 @@ fn already_installed_local_path_dependent() { // Request upgrade of the first package // It's not available on an index and the user has not specified the path so we fail - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("second_local")) .arg("--upgrade-package") .arg("first-local") @@ -3363,7 +3333,7 @@ fn already_installed_local_path_dependent() { // Request upgrade of the first package // A full path is specified and there's nothing to upgrade to so we should just audit - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("first_local")) .arg(root_path.join("second_local")) .arg("--upgrade-package") @@ -3390,7 +3360,7 @@ fn already_installed_local_version_of_remote_package() { let root_path = context.workspace_root.join("scripts/packages"); // Install the local anyio first - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("anyio_local")), @r###" success: true exit_code: 0 @@ -3405,7 +3375,7 @@ fn already_installed_local_version_of_remote_package() { ); // Install again without specifying a local path — this should not pull from the index - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("anyio"), @r###" success: true exit_code: 0 @@ -3419,7 +3389,7 @@ fn already_installed_local_version_of_remote_package() { // Request install with a different version // We should attempt to pull from the index since the installed version does not match // but we disable it here to preserve this dependency for future tests - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("anyio==4.2.0") .arg("--no-index"), @r###" success: false @@ -3440,7 +3410,7 @@ fn already_installed_local_version_of_remote_package() { // Request reinstallation with the local version segment — this should fail since it is not available // in the index and the path was not provided - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("anyio==4.3.0+foo") .arg("--reinstall"), @r###" success: false @@ -3457,7 +3427,7 @@ fn already_installed_local_version_of_remote_package() { // Request reinstall with the full path, this should reinstall from the path // and not pull from the index - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("anyio_local")) .arg("--reinstall") .arg("anyio"), @r###" @@ -3475,7 +3445,7 @@ fn already_installed_local_version_of_remote_package() { // Request reinstallation with just the name, this should pull from the index // and replace the path dependency - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("anyio") .arg("--reinstall"), @r###" success: true @@ -3494,7 +3464,7 @@ fn already_installed_local_version_of_remote_package() { ); // Install the local anyio again so we can test upgrades - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg(root_path.join("anyio_local")), @r###" success: true exit_code: 0 @@ -3510,7 +3480,7 @@ fn already_installed_local_version_of_remote_package() { // Request upgrade with just the name // We shouldn't pull from the index because the local version is "newer" - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("anyio") .arg("--upgrade"), @r###" success: true @@ -3525,7 +3495,7 @@ fn already_installed_local_version_of_remote_package() { // Install something that depends on anyio // We shouldn't overwrite our local version with the remote anyio here - uv_snapshot!(context.filters(), command(&context) + uv_snapshot!(context.filters(), context.install() .arg("httpx"), @r###" success: true exit_code: 0 @@ -3550,7 +3520,7 @@ fn already_installed_remote_url() { let context = TestContext::new("3.8"); // First, install from the remote URL - uv_snapshot!(context.filters(), command(&context).arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), @r###" + uv_snapshot!(context.filters(), context.install().arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -3567,7 +3537,7 @@ fn already_installed_remote_url() { // Request installation again with just the name // We should just audit the URL package since it fulfills this requirement uv_snapshot!( - command(&context).arg("uv-public-pypackage"), @r###" + context.install().arg("uv-public-pypackage"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -3579,7 +3549,7 @@ fn already_installed_remote_url() { // Request reinstallation // We should fail since the URL was not provided uv_snapshot!( - command(&context) + context.install() .arg("uv-public-pypackage") .arg("--no-index") .arg("--reinstall"), @r###" @@ -3601,7 +3571,7 @@ fn already_installed_remote_url() { // Request installation again with just the full URL // We should just audit the existing package uv_snapshot!( - command(&context).arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), @r###" + context.install().arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -3614,7 +3584,7 @@ fn already_installed_remote_url() { // Request reinstallation with the full URL // We should reinstall successfully uv_snapshot!( - command(&context) + context.install() .arg("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage") .arg("--reinstall"), @r###" success: true @@ -3631,7 +3601,7 @@ fn already_installed_remote_url() { // Request installation again with a different version // We should attempt to pull from the index since the local version does not match uv_snapshot!( - command(&context).arg("uv-public-pypackage==0.2.0").arg("--no-index"), @r###" + context.install().arg("uv-public-pypackage==0.2.0").arg("--no-index"), @r###" success: false exit_code: 1 ----- stdout ----- diff --git a/crates/uv/tests/venv.rs b/crates/uv/tests/venv.rs index 805d1bb60..94e79ae79 100644 --- a/crates/uv/tests/venv.rs +++ b/crates/uv/tests/venv.rs @@ -1,11 +1,13 @@ #![cfg(feature = "python")] +use std::ffi::OsString; use std::process::Command; use anyhow::Result; use assert_cmd::prelude::*; +use assert_fs::fixture::ChildPath; use assert_fs::prelude::*; - +use fs_err::PathExt; use uv_fs::Simplified; use crate::common::{ @@ -14,214 +16,166 @@ use crate::common::{ mod common; +struct VenvTestContext { + cache_dir: assert_fs::TempDir, + temp_dir: assert_fs::TempDir, + venv: ChildPath, + bin: OsString, +} + +impl VenvTestContext { + fn new(python_versions: &[&str]) -> Self { + let temp_dir = assert_fs::TempDir::new().unwrap(); + let bin = create_bin_with_executables(&temp_dir, python_versions) + .expect("Failed to create bin dir"); + let venv = temp_dir.child(".venv"); + Self { + cache_dir: assert_fs::TempDir::new().unwrap(), + temp_dir, + venv, + bin, + } + } + + fn venv_command(&self) -> Command { + let mut command = Command::new(get_bin()); + command + .arg("venv") + .arg("--cache-dir") + .arg(self.cache_dir.path()) + .arg("--exclude-newer") + .arg(EXCLUDE_NEWER) + .env("UV_TEST_PYTHON_PATH", self.bin.clone()) + .current_dir(self.temp_dir.path()); + command + } + + fn filters(&self) -> Vec<(String, String)> { + // On windows, a directory can have multiple names (https://superuser.com/a/1666770), e.g. + // `C:\Users\KONSTA~1` and `C:\Users\Konstantin` are the same. + let venv_full = regex::escape(&self.venv.display().to_string()); + let mut filters = vec![(venv_full, ".venv".to_string())]; + + // For mac, otherwise it shows some /var/folders/ path. + if let Ok(canonicalized) = self.venv.path().fs_err_canonicalize() { + let venv_full = regex::escape(&canonicalized.simplified_display().to_string()); + filters.push((venv_full, ".venv".to_string())); + } + + filters.push(( + r"interpreter at: .+".to_string(), + "interpreter at: [PATH]".to_string(), + )); + filters.push(( + r"Activate with: (?:.*)\\Scripts\\activate".to_string(), + "Activate with: source .venv/bin/activate".to_string(), + )); + filters + } +} + #[test] -fn create_venv() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); +fn create_venv() { + let context = VenvTestContext::new(&["3.12"]); // Create a virtual environment at `.venv`. - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; - let filters = &[ - ( - r"Using Python 3\.\d+\.\d+ interpreter at: .+", - "Using Python [VERSION] interpreter at: [PATH]", - ), - (filter_prompt, "Activate with: source .venv/bin/activate"), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--python") - .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_TEST_PYTHON_PATH", bin.clone()) - .current_dir(&temp_dir), @r###" + .arg("3.12"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Using Python [VERSION] interpreter at: [PATH] + Using Python 3.12.1 interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### ); - venv.assert(predicates::path::is_dir()); + context.venv.assert(predicates::path::is_dir()); // Create a virtual environment at the same location, which should replace it. - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; - let filters = &[ - ( - r"Using Python 3\.\d+\.\d+ interpreter at: .+", - "Using Python [VERSION] interpreter at: [PATH]", - ), - (filter_prompt, "Activate with: source .venv/bin/activate"), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .env("UV_NO_WRAP", "1"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Using Python [VERSION] interpreter at: [PATH] + Using Python 3.12.1 interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### ); - venv.assert(predicates::path::is_dir()); - - Ok(()) + context.venv.assert(predicates::path::is_dir()); } #[test] -fn create_venv_defaults_to_cwd() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); - - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; - let filters = &[ - ( - r"Using Python 3\.\d+\.\d+ interpreter at: .+", - "Using Python [VERSION] interpreter at: [PATH]", - ), - (&filter_venv, ".venv"), - (filter_prompt, "Activate with: source .venv/bin/activate"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") +fn create_venv_defaults_to_cwd() { + let context = VenvTestContext::new(&["3.12"]); + uv_snapshot!(context.filters(), context.venv_command() .arg("--python") .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .env("UV_NO_WRAP", "1"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Using Python [VERSION] interpreter at: [PATH] + Using Python 3.12.1 interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### ); - venv.assert(predicates::path::is_dir()); - - Ok(()) + context.venv.assert(predicates::path::is_dir()); } #[test] -fn seed() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); - - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; - let filters = &[ - ( - r"Using Python 3\.\d+\.\d+ interpreter at: .+", - "Using Python [VERSION] interpreter at: [PATH]", - ), - (filter_prompt, "Activate with: source .venv/bin/activate"), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) +fn seed() { + let context = VenvTestContext::new(&["3.12"]); + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--seed") .arg("--python") .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .env("UV_NO_WRAP", "1"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Using Python [VERSION] interpreter at: [PATH] + Using Python 3.12.1 interpreter at: [PATH] Creating virtualenv at: .venv + pip==24.0 Activate with: source .venv/bin/activate "### ); - venv.assert(predicates::path::is_dir()); - - Ok(()) + context.venv.assert(predicates::path::is_dir()); } #[test] -fn seed_older_python_version() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.10"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); - - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; - let filters = &[ - ( - r"Using Python 3\.\d+\.\d+ interpreter at: .+", - "Using Python [VERSION] interpreter at: [PATH]", - ), - (filter_prompt, "Activate with: source .venv/bin/activate"), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) +fn seed_older_python_version() { + let context = VenvTestContext::new(&["3.10"]); + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--seed") .arg("--python") .arg("3.10") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .env("UV_NO_WRAP", "1"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Using Python [VERSION] interpreter at: [PATH] + Using Python 3.10.13 interpreter at: [PATH] Creating virtualenv at: .venv + pip==24.0 + setuptools==69.2.0 @@ -230,31 +184,19 @@ fn seed_older_python_version() -> Result<()> { "### ); - venv.assert(predicates::path::is_dir()); - - Ok(()) + context.venv.assert(predicates::path::is_dir()); } #[test] -fn create_venv_unknown_python_minor() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); +fn create_venv_unknown_python_minor() { + let context = VenvTestContext::new(&["3.12"]); - let mut command = Command::new(get_bin()); + let mut command = context.venv_command(); command - .arg("venv") - .arg(venv.as_os_str()) + .arg(context.venv.as_os_str()) .arg("--python") .arg("3.15") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir); + .env("UV_NO_WRAP", "1"); if cfg!(windows) { uv_snapshot!(&mut command, @r###" success: false @@ -277,19 +219,13 @@ fn create_venv_unknown_python_minor() -> Result<()> { ); } - venv.assert(predicates::path::missing()); - - Ok(()) + context.venv.assert(predicates::path::missing()); } #[test] -fn create_venv_unknown_python_patch() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); +fn create_venv_unknown_python_patch() { + let context = VenvTestContext::new(&["3.12"]); - let filter_venv = regex::escape(&venv.simplified_display().to_string()); let filters = &[ ( r"Using Python 3\.\d+\.\d+ interpreter at: .+", @@ -299,20 +235,12 @@ fn create_venv_unknown_python_patch() -> Result<()> { r"No Python 3\.8\.0 found through `py --list-paths` or in `PATH`\. Is Python 3\.8\.0 installed\?", "No Python 3.8.0 in `PATH`. Is Python 3.8.0 installed?", ), - (&filter_venv, ".venv"), ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + uv_snapshot!(filters, context.venv_command() + .arg(context.venv.as_os_str()) .arg("--python") .arg("3.8.0") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .env("UV_NO_WRAP", "1"), @r###" success: false exit_code: 1 ----- stdout ----- @@ -322,38 +250,18 @@ fn create_venv_unknown_python_patch() -> Result<()> { "### ); - venv.assert(predicates::path::missing()); - - Ok(()) + context.venv.assert(predicates::path::missing()); } #[test] -fn create_venv_python_patch() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = - create_bin_with_executables(&temp_dir, &["3.12.1"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); +fn create_venv_python_patch() { + let context = VenvTestContext::new(&["3.12.1"]); - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; - let filters = &[ - (r"interpreter at: .+", "interpreter at: [PATH]"), - (filter_prompt, "Activate with: source .venv/bin/activate"), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12.1") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .env("UV_NO_WRAP", "1"), @r###" success: true exit_code: 0 ----- stdout ----- @@ -365,47 +273,27 @@ fn create_venv_python_patch() -> Result<()> { "### ); - venv.assert(predicates::path::is_dir()); - - Ok(()) + context.venv.assert(predicates::path::is_dir()); } #[test] fn file_exists() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); + let context = VenvTestContext::new(&["3.12"]); // Create a file at `.venv`. Creating a virtualenv at the same path should fail. - venv.touch()?; + context.venv.touch()?; - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filters = &[ - ( - r"Using Python 3\.\d+\.\d+ interpreter at: .+", - "Using Python [VERSION] interpreter at: [PATH]", - ), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .env("UV_NO_WRAP", "1"), @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- - Using Python [VERSION] interpreter at: [PATH] + Using Python 3.12.1 interpreter at: [PATH] Creating virtualenv at: .venv uv::venv::creation @@ -419,89 +307,50 @@ fn file_exists() -> Result<()> { #[test] fn empty_dir_exists() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); + let context = VenvTestContext::new(&["3.12"]); // Create an empty directory at `.venv`. Creating a virtualenv at the same path should succeed. - venv.create_dir_all()?; - - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; - let filters = &[ - ( - r"Using Python 3\.\d+\.\d+ interpreter at: .+", - "Using Python [VERSION] interpreter at: [PATH]", - ), - (filter_prompt, "Activate with: source .venv/bin/activate"), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + context.venv.create_dir_all()?; + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .env("UV_NO_WRAP", "1"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- - Using Python [VERSION] interpreter at: [PATH] + Using Python 3.12.1 interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### ); - venv.assert(predicates::path::is_dir()); + context.venv.assert(predicates::path::is_dir()); Ok(()) } #[test] fn non_empty_dir_exists() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); + let context = VenvTestContext::new(&["3.12"]); // Create a non-empty directory at `.venv`. Creating a virtualenv at the same path should fail. - venv.create_dir_all()?; - venv.child("file").touch()?; + context.venv.create_dir_all()?; + context.venv.child("file").touch()?; - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filters = &[ - ( - r"Using Python 3\.\d+\.\d+ interpreter at: .+", - "Using Python [VERSION] interpreter at: [PATH]", - ), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .env("UV_NO_WRAP", "1"), @r###" success: false exit_code: 1 ----- stdout ----- ----- stderr ----- - Using Python [VERSION] interpreter at: [PATH] + Using Python 3.12.1 interpreter at: [PATH] Creating virtualenv at: .venv uv::venv::creation @@ -516,14 +365,10 @@ fn non_empty_dir_exists() -> Result<()> { #[test] #[cfg(windows)] fn windows_shims() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = - create_bin_with_executables(&temp_dir, &["3.9", "3.8"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); - let shim_path = temp_dir.child("shim"); + let context = VenvTestContext::new(&["3.9", "3.8"]); + let shim_path = context.temp_dir.child("shim"); - let py38 = std::env::split_paths(&bin) + let py38 = std::env::split_paths(&context.bin) .last() .expect("create_bin_with_executables to set up the python versions"); // We want 3.8 and the first version should be 3.9. @@ -540,88 +385,50 @@ fn windows_shims() -> Result<()> { )?; // Create a virtual environment at `.venv`, passing the redundant `--clear` flag. - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; - let filters = &[ - ( - r"Using Python 3\.8.\d+ interpreter at: .+", - "Using Python 3.8.x interpreter at: [PATH]", - ), - (&filter_prompt, "Activate with: source .venv/bin/activate"), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--clear") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_TEST_PYTHON_PATH", format!("{};{}", shim_path.display(), bin.simplified_display())) - .current_dir(&temp_dir), @r###" + .env("UV_TEST_PYTHON_PATH", format!("{};{}", shim_path.display(), context.bin.simplified_display())), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- warning: virtualenv's `--clear` has no effect (uv always clears the virtual environment). - Using Python 3.8.x interpreter at: [PATH] + Using Python 3.8.12 interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### ); - venv.assert(predicates::path::is_dir()); + context.venv.assert(predicates::path::is_dir()); Ok(()) } #[test] -fn virtualenv_compatibility() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); +fn virtualenv_compatibility() { + let context = VenvTestContext::new(&["3.12"]); // Create a virtual environment at `.venv`, passing the redundant `--clear` flag. - let filter_venv = regex::escape(&venv.simplified_display().to_string()); - let filter_prompt = r"Activate with: (?:.*)\\Scripts\\activate"; - let filters = &[ - ( - r"Using Python 3\.\d+\.\d+ interpreter at: .+", - "Using Python [VERSION] interpreter at: [PATH]", - ), - (filter_prompt, "Activate with: source .venv/bin/activate"), - (&filter_venv, ".venv"), - ]; - uv_snapshot!(filters, Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + uv_snapshot!(context.filters(), context.venv_command() + .arg(context.venv.as_os_str()) .arg("--clear") .arg("--python") - .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_TEST_PYTHON_PATH", bin) - .current_dir(&temp_dir), @r###" + .arg("3.12"), @r###" success: true exit_code: 0 ----- stdout ----- ----- stderr ----- warning: virtualenv's `--clear` has no effect (uv always clears the virtual environment). - Using Python [VERSION] interpreter at: [PATH] + Using Python 3.12.1 interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### ); - venv.assert(predicates::path::is_dir()); - - Ok(()) + context.venv.assert(predicates::path::is_dir()); } #[test] @@ -644,27 +451,18 @@ fn verify_pyvenv_cfg() { /// Ensure that a nested virtual environment uses the same `home` directory as the parent. #[test] fn verify_nested_pyvenv_cfg() -> Result<()> { - let temp_dir = assert_fs::TempDir::new()?; - let cache_dir = assert_fs::TempDir::new()?; - let bin = create_bin_with_executables(&temp_dir, &["3.12"]).expect("Failed to create bin dir"); - let venv = temp_dir.child(".venv"); + let context = VenvTestContext::new(&["3.12"]); // Create a virtual environment at `.venv`. - Command::new(get_bin()) - .arg("venv") - .arg(venv.as_os_str()) + context + .venv_command() + .arg(context.venv.as_os_str()) .arg("--python") .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("UV_TEST_PYTHON_PATH", bin.clone()) - .current_dir(&temp_dir) .assert() .success(); - let pyvenv_cfg = venv.child("pyvenv.cfg"); + let pyvenv_cfg = context.venv.child("pyvenv.cfg"); // Check pyvenv.cfg exists pyvenv_cfg.assert(predicates::path::is_file()); @@ -677,19 +475,13 @@ fn verify_nested_pyvenv_cfg() -> Result<()> { .expect("home line not found"); // Now, create a virtual environment from within the virtual environment. - let subvenv = temp_dir.child(".subvenv"); - Command::new(get_bin()) - .arg("venv") + let subvenv = context.temp_dir.child(".subvenv"); + context + .venv_command() .arg(subvenv.as_os_str()) .arg("--python") .arg("3.12") - .arg("--cache-dir") - .arg(cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) - .env("VIRTUAL_ENV", venv.as_os_str()) - .env("UV_TEST_PYTHON_PATH", bin.clone()) - .current_dir(&temp_dir) + .env("VIRTUAL_ENV", context.venv.as_os_str()) .assert() .success();