use std::process::Command;
use anyhow::Result;
use assert_cmd::assert::OutputAssertExt;
use assert_fs::prelude::*;
use indoc::indoc;
use insta::assert_snapshot;
use uv_static::EnvVars;
use uv_test::uv_snapshot;
#[test]
fn tool_upgrade_empty() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("--all")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Nothing to upgrade
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("--all")
.arg("-p")
.arg("3.13")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Nothing to upgrade
");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.14.0
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("--all")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Nothing to upgrade
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("--all")
.arg("-p")
.arg("3.12")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Nothing to upgrade
");
}
#[test]
fn tool_upgrade_preserves_workspace_member_editability() -> Result<()> {
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
let root_pyproject = context.temp_dir.child("pyproject.toml");
root_pyproject.write_str(indoc! {
r#"
[project]
name = "root"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["child"]
[project.scripts]
root_cli = "root:main"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.sources]
child = { workspace = true }
[tool.uv.workspace]
members = ["child"]
"#
})?;
let root_src = context.temp_dir.child("src").child("root");
root_src.create_dir_all()?;
root_src.child("__init__.py").write_str(indoc! {
r"
def main():
import child
print(child.MESSAGE)
"
})?;
let child = context.temp_dir.child("child");
child.create_dir_all()?;
child.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "child"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
let child_src = child.child("src").child("child");
child_src.create_dir_all()?;
child_src
.child("__init__.py")
.write_str("MESSAGE = 'OK'\n")?;
let status = context
.tool_install()
.arg(context.temp_dir.path())
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str())
.status()
.expect("failed to run uv tool install");
assert!(status.success());
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
OK
----- stderr -----
");
child_src
.child("__init__.py")
.write_str("MESSAGE = 'PRE-UPGRADE'\n")?;
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
OK
----- stderr -----
");
let status = context
.tool_upgrade()
.arg("root")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str())
.status()
.expect("failed to run uv tool upgrade");
assert!(status.success());
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
OK
----- stderr -----
");
child_src
.child("__init__.py")
.write_str("MESSAGE = 'POST-UPGRADE'\n")?;
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
OK
----- stderr -----
");
Ok(())
}
#[test]
fn tool_upgrade_preserves_mixed_workspace_member_editability() -> Result<()> {
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
let tool_root = context.temp_dir.child("tool-root");
tool_root.create_dir_all()?;
tool_root.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "tool-root"
version = "0.1.0"
requires-python = ">=3.12"
[project.scripts]
root_cli = "tool_root:main"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
let tool_root_src = tool_root.child("src").child("tool_root");
tool_root_src.create_dir_all()?;
tool_root_src.child("__init__.py").write_str(indoc! {
r#"
def main():
import importlib.metadata
import other_child
print(f"{importlib.metadata.version('tool-root')} {other_child.MESSAGE}")
"#
})?;
let other_workspace = context.temp_dir.child("other-workspace");
other_workspace.create_dir_all()?;
other_workspace
.child("pyproject.toml")
.write_str(indoc! {r#"
[project]
name = "other-workspace"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["other-child"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.sources]
other-child = { workspace = true }
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
let other_workspace_src = other_workspace.child("src").child("other_workspace");
other_workspace_src.create_dir_all()?;
other_workspace_src.child("__init__.py").touch()?;
let other_child = other_workspace.child("packages").child("other-child");
other_child.create_dir_all()?;
other_child.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "other-child"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
let other_child_src = other_child.child("src").child("other_child");
other_child_src.create_dir_all()?;
other_child_src
.child("__init__.py")
.write_str("MESSAGE = 'OK'\n")?;
let status = context
.tool_install()
.arg("--with-editable")
.arg(other_workspace.path())
.arg(tool_root.path())
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str())
.status()
.expect("failed to run uv tool install");
assert!(status.success());
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
0.1.0 OK
----- stderr -----
");
tool_root.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "tool-root"
version = "0.1.1"
requires-python = ">=3.12"
[project.scripts]
root_cli = "tool_root:main"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
let status = context
.tool_upgrade()
.arg("tool-root")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str())
.status()
.expect("failed to run uv tool upgrade");
assert!(status.success());
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
0.1.1 OK
----- stderr -----
");
other_child_src
.child("__init__.py")
.write_str("MESSAGE = 'POST-UPGRADE'\n")?;
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
0.1.1 POST-UPGRADE
----- stderr -----
");
Ok(())
}
#[test]
fn tool_upgrade_preserves_mixed_workspace_member_non_editability() -> Result<()> {
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
let tool_root = context.temp_dir.child("tool-root");
tool_root.create_dir_all()?;
tool_root.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "tool-root"
version = "0.1.0"
requires-python = ">=3.12"
[project.scripts]
root_cli = "tool_root:main"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
let tool_root_src = tool_root.child("src").child("tool_root");
tool_root_src.create_dir_all()?;
tool_root_src.child("__init__.py").write_str(indoc! {
r#"
def main():
import importlib.metadata
import other_child
print(f"{importlib.metadata.version('tool-root')} {other_child.MESSAGE}")
"#
})?;
let other_workspace = context.temp_dir.child("other-workspace");
other_workspace.create_dir_all()?;
other_workspace
.child("pyproject.toml")
.write_str(indoc! {r#"
[project]
name = "other-workspace"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["other-child"]
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
[tool.uv.sources]
other-child = { workspace = true }
[tool.uv.workspace]
members = ["packages/*"]
"#})?;
let other_workspace_src = other_workspace.child("src").child("other_workspace");
other_workspace_src.create_dir_all()?;
other_workspace_src.child("__init__.py").touch()?;
let other_child = other_workspace.child("packages").child("other-child");
other_child.create_dir_all()?;
other_child.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "other-child"
version = "0.1.0"
requires-python = ">=3.12"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
let other_child_src = other_child.child("src").child("other_child");
other_child_src.create_dir_all()?;
other_child_src
.child("__init__.py")
.write_str("MESSAGE = 'OK'\n")?;
let status = context
.tool_install()
.arg("--editable")
.arg("--with")
.arg(other_workspace.path())
.arg(tool_root.path())
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str())
.status()
.expect("failed to run uv tool install");
assert!(status.success());
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
0.1.0 OK
----- stderr -----
");
tool_root.child("pyproject.toml").write_str(indoc! {r#"
[project]
name = "tool-root"
version = "0.1.1"
requires-python = ">=3.12"
[project.scripts]
root_cli = "tool_root:main"
[build-system]
requires = ["uv_build>=0.7,<10000"]
build-backend = "uv_build"
"#})?;
let status = context
.tool_upgrade()
.arg("tool-root")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str())
.status()
.expect("failed to run uv tool upgrade");
assert!(status.success());
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
0.1.1 OK
----- stderr -----
");
other_child_src
.child("__init__.py")
.write_str("MESSAGE = 'POST-UPGRADE'\n")?;
uv_snapshot!(context.filters(), Command::new("root_cli").env(EnvVars::PATH, bin_dir.as_os_str()), @r"
success: true
exit_code: 0
----- stdout -----
0.1.1 OK
----- stderr -----
");
Ok(())
}
#[test]
fn tool_upgrade_name() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated babel v2.6.0 -> v2.14.0
- babel==2.6.0
+ babel==2.14.0
- pytz==2018.5
Installed 1 executable: pybabel
");
}
#[test]
fn tool_upgrade_recomputes_relative_exclude_newer() {
let context = uv_test::test_context!("3.12").with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
context
.tool_install()
.arg("black")
.arg("--exclude-newer")
.arg("3 weeks")
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
.env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2024-03-22T00:00:00Z")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str())
.assert()
.success();
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("black")
.env_remove(EnvVars::UV_EXCLUDE_NEWER)
.env(EnvVars::UV_TEST_CURRENT_TIMESTAMP, "2024-04-15T00:00:00Z")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated black v24.2.0 -> v24.3.0
- black==24.2.0
+ black==24.3.0
- packaging==23.2
+ packaging==24.0
Installed 2 executables: black, blackd
");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r#"
[tool]
requirements = [{ name = "black" }]
entrypoints = [
{ name = "black", install-path = "[TEMP_DIR]/bin/black", from = "black" },
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd", from = "black" },
]
[tool.options]
exclude-newer = "2024-03-25T00:00:00Z"
exclude-newer-span = "P3W"
"#);
});
}
#[test]
fn tool_upgrade_multiple_names() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("python-dotenv")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ python-dotenv==0.10.2.post2
Installed 1 executable: dotenv
");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("python-dotenv")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated babel v2.6.0 -> v2.14.0
- babel==2.6.0
+ babel==2.14.0
- pytz==2018.5
Installed 1 executable: pybabel
Updated python-dotenv v0.10.2.post2 -> v1.0.1
- python-dotenv==0.10.2.post2
+ python-dotenv==1.0.1
Installed 1 executable: dotenv
");
}
#[test]
fn tool_upgrade_pinned_hint() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel==2.6.0")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Modified babel environment
- pytz==2018.5
+ pytz==2024.1
hint: `babel` is pinned to `2.6.0` (installed with an exact version pin); reinstall with `uv tool install babel@latest` to upgrade to a new version.
");
}
#[test]
fn tool_upgrade_pinned_hint_with_mixed_constraint() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel>=2.0,==2.6.0")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Modified babel environment
- pytz==2018.5
+ pytz==2024.1
hint: `babel` is pinned to `2.6.0` (installed with an exact version pin); reinstall with `uv tool install babel@latest` to upgrade to a new version.
");
}
#[test]
fn tool_upgrade_all() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("python-dotenv")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ python-dotenv==0.10.2.post2
Installed 1 executable: dotenv
");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("--all")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated babel v2.6.0 -> v2.14.0
- babel==2.6.0
+ babel==2.14.0
- pytz==2018.5
Installed 1 executable: pybabel
Updated python-dotenv v0.10.2.post2 -> v1.0.1
- python-dotenv==0.10.2.post2
+ python-dotenv==1.0.1
Installed 1 executable: dotenv
");
}
#[test]
fn tool_upgrade_non_existing_package() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("black")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
error: Failed to upgrade black
Caused by: `black` is not installed; run `uv tool install black` to install
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("--all")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Nothing to upgrade
");
}
#[test]
fn tool_upgrade_not_stop_if_upgrade_fails() -> anyhow::Result<()> {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("python-dotenv")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ python-dotenv==0.10.2.post2
Installed 1 executable: dotenv
");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
tool_dir
.child("python-dotenv")
.child("uv-receipt.toml")
.write_str("Invalid receipt")?;
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("--all")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
Updated babel v2.6.0 -> v2.14.0
- babel==2.6.0
+ babel==2.14.0
- pytz==2018.5
Installed 1 executable: pybabel
error: Failed to upgrade python-dotenv
Caused by: `python-dotenv` is missing a valid receipt; run `uv tool install --force python-dotenv` to reinstall
");
Ok(())
}
#[test]
fn tool_upgrade_settings() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("black>=23")
.arg("--resolution=lowest-direct")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ black==23.1.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed 2 executables: black, blackd
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("black")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Nothing to upgrade
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("black")
.arg("--resolution=highest")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated black v23.1.0 -> v24.3.0
- black==23.1.0
+ black==24.3.0
Installed 2 executables: black, blackd
");
}
#[test]
fn tool_upgrade_respect_constraints() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel<2.10")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated babel v2.6.0 -> v2.9.1
- babel==2.6.0
+ babel==2.9.1
- pytz==2018.5
+ pytz==2024.1
Installed 1 executable: pybabel
");
}
#[test]
fn tool_upgrade_constraint() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel<2.12.0")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated babel v2.6.0 -> v2.11.0
- babel==2.6.0
+ babel==2.11.0
- pytz==2018.5
+ pytz==2024.1
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.arg("--upgrade-package")
.arg("babel<2.14.0")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `--upgrade-package` is enabled by default on `uv tool upgrade`
Updated babel v2.11.0 -> v2.13.1
- babel==2.11.0
+ babel==2.13.1
- pytz==2024.1
+ setuptools==69.2.0
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated babel v2.13.1 -> v2.14.0
- babel==2.13.1
+ babel==2.14.0
- setuptools==69.2.0
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.arg("--upgrade")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
warning: `--upgrade` is enabled by default on `uv tool upgrade`
Nothing to upgrade
");
}
#[test]
fn tool_upgrade_with() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel==2.6.0")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Modified babel environment
- pytz==2018.5
+ pytz==2024.1
hint: `babel` is pinned to `2.6.0` (installed with an exact version pin); reinstall with `uv tool install babel@latest` to upgrade to a new version.
");
}
#[test]
fn tool_upgrade_python() {
let context = uv_test::test_context_with_versions!(&["3.11", "3.12"])
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel==2.6.0")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.arg("--python").arg("3.11")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(
context.filters(),
context.tool_upgrade().arg("babel")
.arg("--python").arg("3.12")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
Upgraded tool environment for `babel` to Python 3.12
"
);
insta::with_settings!({
filters => context.filters(),
}, {
let content = fs_err::read_to_string(tool_dir.join("babel").join("pyvenv.cfg")).unwrap();
let lines: Vec<&str> = content.split('\n').collect();
assert_snapshot!(lines[lines.len() - 3], @"version_info = 3.12.[X]");
});
}
#[test]
fn tool_upgrade_python_with_all() {
let context = uv_test::test_context_with_versions!(&["3.11", "3.12"])
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel==2.6.0")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.arg("--python").arg("3.11")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_install()
.arg("python-dotenv")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.arg("--python").arg("3.11")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ python-dotenv==0.10.2.post2
Installed 1 executable: dotenv
");
uv_snapshot!(
context.filters(),
context.tool_upgrade().arg("--all")
.arg("--python").arg("3.12")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
+ pytz==2018.5
Installed 1 executable: pybabel
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ python-dotenv==0.10.2.post2
Installed 1 executable: dotenv
Upgraded tool environments for `babel` and `python-dotenv` to Python 3.12
"
);
insta::with_settings!({
filters => context.filters(),
}, {
let content = fs_err::read_to_string(tool_dir.join("babel").join("pyvenv.cfg")).unwrap();
let lines: Vec<&str> = content.split('\n').collect();
assert_snapshot!(lines[lines.len() - 3], @"version_info = 3.12.[X]");
});
insta::with_settings!({
filters => context.filters(),
}, {
let content = fs_err::read_to_string(tool_dir.join("python-dotenv").join("pyvenv.cfg")).unwrap();
let lines: Vec<&str> = content.split('\n').collect();
assert_snapshot!(lines[lines.len() - 3], @"version_info = 3.12.[X]");
});
}
#[test]
fn test_tool_upgrade_additional_entrypoints() {
let context = uv_test::test_context_with_versions!(&["3.11", "3.12"])
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("--python")
.arg("3.11")
.arg("--with-executables-from")
.arg("black")
.arg("babel==2.14.0")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.14.0
+ black==24.3.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed 2 executables from `black`: black, blackd
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("--python")
.arg("3.12")
.arg("babel")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.14.0
+ black==24.3.0
+ click==8.1.7
+ mypy-extensions==1.0.0
+ packaging==24.0
+ pathspec==0.12.1
+ platformdirs==4.2.0
Installed 2 executables from `black`: black, blackd
Installed 1 executable: pybabel
Upgraded tool environment for `babel` to Python 3.12
");
}
#[test]
fn tool_upgrade_excludes() {
let context = uv_test::test_context!("3.12")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
let excludes_txt = context.temp_dir.child("excludes.txt");
excludes_txt.write_str("pytz").unwrap();
uv_snapshot!(context.filters(), context.tool_install()
.arg("babel<2.10")
.arg("--excludes")
.arg("excludes.txt")
.arg("--index-url")
.arg("https://test.pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ babel==2.6.0
Installed 1 executable: pybabel
");
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("babel")
.arg("--index-url")
.arg("https://pypi.org/simple/")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Updated babel v2.6.0 -> v2.9.1
- babel==2.6.0
+ babel==2.9.1
Installed 1 executable: pybabel
");
}
#[tokio::test]
async fn tool_upgrade_invalid_auth() -> Result<()> {
let proxy = crate::pypi_proxy::start().await;
let context = uv_test::test_context!("3.12")
.with_exclude_newer("2025-01-18T00:00:00Z")
.with_filtered_counts()
.with_filtered_exe_suffix();
let tool_dir = context.temp_dir.child("tools");
let bin_dir = context.temp_dir.child("bin");
uv_snapshot!(context.filters(), context.tool_install()
.arg("executable-application")
.arg("--index")
.arg(proxy.authenticated_url("public", "heron", "/basic-auth/simple"))
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved [N] packages in [TIME]
Prepared [N] packages in [TIME]
Installed [N] packages in [TIME]
+ executable-application==0.3.0
Installed 1 executable: app
");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(fs_err::read_to_string(tool_dir.join("executable-application").join("uv-receipt.toml")).unwrap(), @r#"
[tool]
requirements = [{ name = "executable-application" }]
entrypoints = [
{ name = "app", install-path = "[TEMP_DIR]/bin/app", from = "executable-application" },
]
[tool.options]
index = [{ url = "http://[LOCALHOST]/basic-auth/simple", explicit = false, default = false, format = "simple", authenticate = "always" }]
exclude-newer = "2025-01-18T00:00:00Z"
"#);
});
uv_snapshot!(context.filters(), context.tool_upgrade()
.arg("executable-application")
.env(EnvVars::UV_TOOL_DIR, tool_dir.as_os_str())
.env(EnvVars::XDG_BIN_HOME, bin_dir.as_os_str())
.env(EnvVars::PATH, bin_dir.as_os_str()), @"
success: false
exit_code: 1
----- stdout -----
----- stderr -----
error: Failed to upgrade executable-application
Caused by: Failed to fetch: `http://[LOCALHOST]/basic-auth/simple/executable-application/`
Caused by: Missing credentials for http://[LOCALHOST]/basic-auth/simple/executable-application/
");
Ok(())
}