From 681befbb9a2728351a75d6fe91e8a3153df044f8 Mon Sep 17 00:00:00 2001 From: bejugamvarun Date: Mon, 23 Mar 2026 20:24:53 -0500 Subject: [PATCH 1/5] address review comments --- crates/uv/src/commands/self_update.rs | 14 ++++++------- crates/uv/src/printer.rs | 11 ++++++++++ crates/uv/tests/it/self_update.rs | 29 +++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/crates/uv/src/commands/self_update.rs b/crates/uv/src/commands/self_update.rs index 6d4fa9a571bb2..2a1256727c674 100644 --- a/crates/uv/src/commands/self_update.rs +++ b/crates/uv/src/commands/self_update.rs @@ -24,7 +24,7 @@ pub(crate) async fn self_update( ) -> Result { if client_builder.is_offline() { writeln!( - printer.stderr(), + printer.stderr_important(), "{}", format_args!( "{}{} Self-update is not possible because network connectivity is disabled (i.e., with `--offline`)", @@ -47,7 +47,7 @@ pub(crate) async fn self_update( let Ok(updater) = updater.load_receipt() else { debug!("No receipt found; assuming uv was installed via a package manager"); writeln!( - printer.stderr(), + printer.stderr_important(), "{}", format_args!( concat!( @@ -79,7 +79,7 @@ pub(crate) async fn self_update( let receipt_prefix = updater.install_prefix_root()?; writeln!( - printer.stderr(), + printer.stderr_important(), "{}", format_args!( concat!( @@ -152,7 +152,7 @@ pub(crate) async fn self_update( if dry_run { writeln!( - printer.stderr(), + printer.stderr_important(), "Would update uv from {} to {}", format!("v{}", env!("CARGO_PKG_VERSION")).bold().white(), format!("v{}", resolved.version).bold().white(), @@ -188,7 +188,7 @@ pub(crate) async fn self_update( } }; writeln!( - printer.stderr(), + printer.stderr_important(), "Would update uv from {} to {}", format!("v{}", env!("CARGO_PKG_VERSION")).bold().white(), version.bold().white(), @@ -309,7 +309,7 @@ async fn run_updater( }; writeln!( - printer.stderr(), + printer.stderr_important(), "{}", format_args!( "{}{} {direction} uv {}! {}", @@ -340,7 +340,7 @@ async fn run_updater( return if let AxoupdateError::Reqwest(err) = err { if err.status() == Some(http::StatusCode::FORBIDDEN) && !has_token { writeln!( - printer.stderr(), + printer.stderr_important(), "{}", format_args!( "{}{} GitHub API rate limit exceeded. Please provide a GitHub token via the {} option.", diff --git a/crates/uv/src/printer.rs b/crates/uv/src/printer.rs index 234a569c7872c..4848a59375833 100644 --- a/crates/uv/src/printer.rs +++ b/crates/uv/src/printer.rs @@ -52,6 +52,17 @@ impl Printer { } } + /// Return the [`Stderr`] for this printer. + pub(crate) fn stderr_important(self) -> Stderr { + match self { + Self::Silent => Stderr::Disabled, + Self::Quiet => Stderr::Enabled, + Self::Default => Stderr::Enabled, + Self::Verbose => Stderr::Enabled, + Self::NoProgress => Stderr::Enabled, + } + } + /// Return the [`Stderr`] for this printer. pub(crate) fn stderr(self) -> Stderr { match self { diff --git a/crates/uv/tests/it/self_update.rs b/crates/uv/tests/it/self_update.rs index 02a58691afc92..fb0c2272c1e1f 100644 --- a/crates/uv/tests/it/self_update.rs +++ b/crates/uv/tests/it/self_update.rs @@ -64,6 +64,35 @@ fn test_self_update_offline_error() { "); } +#[test] +fn test_self_update_offline_quiet() { + let context = uv_test::test_context!("3.12"); + + uv_snapshot!(context.self_update().arg("--offline").arg("--quiet"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + error: Self-update is not possible because network connectivity is disabled (i.e., with `--offline`) + "); +} + +#[test] +fn test_self_update_offline_extra_quiet() { + let context = uv_test::test_context!("3.12"); + + uv_snapshot!(context.self_update().arg("--offline").arg("--quiet").arg("--quiet"), + @r" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + "); +} + #[tokio::test] async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> { let context = uv_test::test_context!("3.12").with_filter(( From aaf9615db47a81d2ce33fe096f7c387d2c253773 Mon Sep 17 00:00:00 2001 From: bejugamvarun Date: Mon, 23 Mar 2026 20:53:31 -0500 Subject: [PATCH 2/5] Add mocked integration tests for -q / -qq output behavior Tests verify that with -q, informational messages are suppressed but update notifications still appear, and with -qq all output is suppressed. Uses a fake receipt and mock server following the existing pattern in test_self_update_uses_legacy_path_with_ghe_override. --- crates/uv/tests/it/self_update.rs | 151 ++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) diff --git a/crates/uv/tests/it/self_update.rs b/crates/uv/tests/it/self_update.rs index fb0c2272c1e1f..f3d91417ad00b 100644 --- a/crates/uv/tests/it/self_update.rs +++ b/crates/uv/tests/it/self_update.rs @@ -169,3 +169,154 @@ async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn test_self_update_dry_run_quiet() -> Result<()> { + let context = uv_test::test_context!("3.12").with_filter(( + escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), + "v[CURRENT_VERSION]", + )); + + let receipt_dir = context.temp_dir.child("receipt"); + receipt_dir.create_dir_all()?; + + let install_prefix = std::path::absolute( + get_bin!() + .parent() + .expect("uv binary should have a parent directory"), + )?; + receipt_dir + .child("uv-receipt.json") + .write_str(&serde_json::to_string_pretty(&json!({ + "install_prefix": install_prefix, + "binaries": ["uv"], + "cdylibs": [], + "source": { + "release_type": "github", + "owner": "astral-sh", + "name": "uv", + "app_name": "uv", + }, + "version": env!("CARGO_PKG_VERSION"), + "provider": { + "source": "cargo-dist", + "version": "0.31.0", + }, + "modify_path": true, + }))?)?; + + let server = MockServer::start().await; + let target_version = "9.9.9"; + let installer_name = if cfg!(windows) { + "uv-installer.ps1" + } else { + "uv-installer.sh" + }; + Mock::given(method("GET")) + .and(path(format!( + "/api/v3/repos/astral-sh/uv/releases/tags/{target_version}" + ))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "tag_name": target_version, + "name": target_version, + "url": format!("{}/repos/astral-sh/uv/releases/tags/{target_version}", server.uri()), + "assets": [{ + "url": format!("{}/assets/{installer_name}", server.uri()), + "browser_download_url": format!("{}/downloads/{installer_name}", server.uri()), + "name": installer_name, + }], + "prerelease": false, + }))) + .mount(&server) + .await; + + uv_snapshot!(context.filters(), context.self_update() + .arg(target_version) + .arg("--dry-run") + .arg("--quiet") + .env("AXOUPDATER_CONFIG_PATH", receipt_dir.as_os_str()) + .env(EnvVars::UV_INSTALLER_GHE_BASE_URL, server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Would update uv from v[CURRENT_VERSION] to v9.9.9 + "); + + Ok(()) +} + +#[tokio::test] +async fn test_self_update_dry_run_extra_quiet() -> Result<()> { + let context = uv_test::test_context!("3.12"); + + let receipt_dir = context.temp_dir.child("receipt"); + receipt_dir.create_dir_all()?; + + let install_prefix = std::path::absolute( + get_bin!() + .parent() + .expect("uv binary should have a parent directory"), + )?; + receipt_dir + .child("uv-receipt.json") + .write_str(&serde_json::to_string_pretty(&json!({ + "install_prefix": install_prefix, + "binaries": ["uv"], + "cdylibs": [], + "source": { + "release_type": "github", + "owner": "astral-sh", + "name": "uv", + "app_name": "uv", + }, + "version": env!("CARGO_PKG_VERSION"), + "provider": { + "source": "cargo-dist", + "version": "0.31.0", + }, + "modify_path": true, + }))?)?; + + let server = MockServer::start().await; + let target_version = "9.9.9"; + let installer_name = if cfg!(windows) { + "uv-installer.ps1" + } else { + "uv-installer.sh" + }; + Mock::given(method("GET")) + .and(path(format!( + "/api/v3/repos/astral-sh/uv/releases/tags/{target_version}" + ))) + .respond_with(ResponseTemplate::new(200).set_body_json(json!({ + "tag_name": target_version, + "name": target_version, + "url": format!("{}/repos/astral-sh/uv/releases/tags/{target_version}", server.uri()), + "assets": [{ + "url": format!("{}/assets/{installer_name}", server.uri()), + "browser_download_url": format!("{}/downloads/{installer_name}", server.uri()), + "name": installer_name, + }], + "prerelease": false, + }))) + .mount(&server) + .await; + + uv_snapshot!(context.self_update() + .arg(target_version) + .arg("--dry-run") + .arg("--quiet") + .arg("--quiet") + .env("AXOUPDATER_CONFIG_PATH", receipt_dir.as_os_str()) + .env(EnvVars::UV_INSTALLER_GHE_BASE_URL, server.uri()), @r" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + Ok(()) +} From 396d61dbe732d4440a6f1fcca2add3360253fb68 Mon Sep 17 00:00:00 2001 From: "Tomasz (Tom) Kramkowski" Date: Wed, 25 Mar 2026 10:28:31 +0000 Subject: [PATCH 3/5] Clean up tests --- crates/uv/tests/it/self_update.rs | 138 ++++++------------------------ 1 file changed, 26 insertions(+), 112 deletions(-) diff --git a/crates/uv/tests/it/self_update.rs b/crates/uv/tests/it/self_update.rs index f3d91417ad00b..53b2b735f3d37 100644 --- a/crates/uv/tests/it/self_update.rs +++ b/crates/uv/tests/it/self_update.rs @@ -1,4 +1,4 @@ -use std::process::Command; +use std::{path::PathBuf, process::Command}; use anyhow::Result; use assert_fs::prelude::*; @@ -13,7 +13,7 @@ use wiremock::{Mock, MockServer, ResponseTemplate}; use uv_static::EnvVars; -use uv_test::{get_bin, uv_snapshot}; +use uv_test::{TestContext, get_bin, uv_snapshot}; #[test] fn check_self_update() { @@ -50,7 +50,7 @@ fn check_self_update() { } #[test] -fn test_self_update_offline_error() { +fn self_update_offline_error() { let context = uv_test::test_context!("3.12"); uv_snapshot!(context.self_update().arg("--offline"), @@ -65,7 +65,7 @@ fn test_self_update_offline_error() { } #[test] -fn test_self_update_offline_quiet() { +fn self_update_offline_quiet() { let context = uv_test::test_context!("3.12"); uv_snapshot!(context.self_update().arg("--offline").arg("--quiet"), @@ -80,7 +80,7 @@ fn test_self_update_offline_quiet() { } #[test] -fn test_self_update_offline_extra_quiet() { +fn self_update_offline_extra_quiet() { let context = uv_test::test_context!("3.12"); uv_snapshot!(context.self_update().arg("--offline").arg("--quiet").arg("--quiet"), @@ -93,13 +93,12 @@ fn test_self_update_offline_extra_quiet() { "); } -#[tokio::test] -async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> { - let context = uv_test::test_context!("3.12").with_filter(( - escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), - "v[CURRENT_VERSION]", - )); - +/// Set up a fake receipt and a mock update metadata endpoint to allow +/// simulating an update with `--dry-run`. +async fn setup_mock_update( + context: &TestContext, + target_version: &str, +) -> Result<(PathBuf, MockServer)> { let receipt_dir = context.temp_dir.child("receipt"); receipt_dir.create_dir_all()?; @@ -129,7 +128,6 @@ async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> { }))?)?; let server = MockServer::start().await; - let target_version = "9.9.9"; let installer_name = if cfg!(windows) { "uv-installer.ps1" } else { @@ -153,6 +151,19 @@ async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> { .mount(&server) .await; + Ok((receipt_dir.to_path_buf(), server)) +} + +#[tokio::test] +async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> { + let context = uv_test::test_context!("3.12").with_filter(( + escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), + "v[CURRENT_VERSION]", + )); + + let target_version = "9.9.9"; + let (receipt_dir, server) = setup_mock_update(&context, target_version).await?; + uv_snapshot!(context.filters(), context.self_update() .arg(target_version) .arg("--dry-run") @@ -177,58 +188,8 @@ async fn test_self_update_dry_run_quiet() -> Result<()> { "v[CURRENT_VERSION]", )); - let receipt_dir = context.temp_dir.child("receipt"); - receipt_dir.create_dir_all()?; - - let install_prefix = std::path::absolute( - get_bin!() - .parent() - .expect("uv binary should have a parent directory"), - )?; - receipt_dir - .child("uv-receipt.json") - .write_str(&serde_json::to_string_pretty(&json!({ - "install_prefix": install_prefix, - "binaries": ["uv"], - "cdylibs": [], - "source": { - "release_type": "github", - "owner": "astral-sh", - "name": "uv", - "app_name": "uv", - }, - "version": env!("CARGO_PKG_VERSION"), - "provider": { - "source": "cargo-dist", - "version": "0.31.0", - }, - "modify_path": true, - }))?)?; - - let server = MockServer::start().await; let target_version = "9.9.9"; - let installer_name = if cfg!(windows) { - "uv-installer.ps1" - } else { - "uv-installer.sh" - }; - Mock::given(method("GET")) - .and(path(format!( - "/api/v3/repos/astral-sh/uv/releases/tags/{target_version}" - ))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "tag_name": target_version, - "name": target_version, - "url": format!("{}/repos/astral-sh/uv/releases/tags/{target_version}", server.uri()), - "assets": [{ - "url": format!("{}/assets/{installer_name}", server.uri()), - "browser_download_url": format!("{}/downloads/{installer_name}", server.uri()), - "name": installer_name, - }], - "prerelease": false, - }))) - .mount(&server) - .await; + let (receipt_dir, server) = setup_mock_update(&context, target_version).await?; uv_snapshot!(context.filters(), context.self_update() .arg(target_version) @@ -254,55 +215,8 @@ async fn test_self_update_dry_run_extra_quiet() -> Result<()> { let receipt_dir = context.temp_dir.child("receipt"); receipt_dir.create_dir_all()?; - let install_prefix = std::path::absolute( - get_bin!() - .parent() - .expect("uv binary should have a parent directory"), - )?; - receipt_dir - .child("uv-receipt.json") - .write_str(&serde_json::to_string_pretty(&json!({ - "install_prefix": install_prefix, - "binaries": ["uv"], - "cdylibs": [], - "source": { - "release_type": "github", - "owner": "astral-sh", - "name": "uv", - "app_name": "uv", - }, - "version": env!("CARGO_PKG_VERSION"), - "provider": { - "source": "cargo-dist", - "version": "0.31.0", - }, - "modify_path": true, - }))?)?; - - let server = MockServer::start().await; let target_version = "9.9.9"; - let installer_name = if cfg!(windows) { - "uv-installer.ps1" - } else { - "uv-installer.sh" - }; - Mock::given(method("GET")) - .and(path(format!( - "/api/v3/repos/astral-sh/uv/releases/tags/{target_version}" - ))) - .respond_with(ResponseTemplate::new(200).set_body_json(json!({ - "tag_name": target_version, - "name": target_version, - "url": format!("{}/repos/astral-sh/uv/releases/tags/{target_version}", server.uri()), - "assets": [{ - "url": format!("{}/assets/{installer_name}", server.uri()), - "browser_download_url": format!("{}/downloads/{installer_name}", server.uri()), - "name": installer_name, - }], - "prerelease": false, - }))) - .mount(&server) - .await; + let (receipt_dir, server) = setup_mock_update(&context, target_version).await?; uv_snapshot!(context.self_update() .arg(target_version) From 05744ba48930ebc4012b0cb8ed0925c48e38d0ea Mon Sep 17 00:00:00 2001 From: "Tomasz (Tom) Kramkowski" Date: Wed, 25 Mar 2026 10:55:24 +0000 Subject: [PATCH 4/5] Add no-update tests --- crates/uv/tests/it/self_update.rs | 57 +++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/crates/uv/tests/it/self_update.rs b/crates/uv/tests/it/self_update.rs index 53b2b735f3d37..6af4ca57aaf6c 100644 --- a/crates/uv/tests/it/self_update.rs +++ b/crates/uv/tests/it/self_update.rs @@ -182,7 +182,7 @@ async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> { } #[tokio::test] -async fn test_self_update_dry_run_quiet() -> Result<()> { +async fn self_update_dry_run_quiet() -> Result<()> { let context = uv_test::test_context!("3.12").with_filter(( escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), "v[CURRENT_VERSION]", @@ -209,7 +209,7 @@ async fn test_self_update_dry_run_quiet() -> Result<()> { } #[tokio::test] -async fn test_self_update_dry_run_extra_quiet() -> Result<()> { +async fn self_update_dry_run_extra_quiet() -> Result<()> { let context = uv_test::test_context!("3.12"); let receipt_dir = context.temp_dir.child("receipt"); @@ -234,3 +234,56 @@ async fn test_self_update_dry_run_extra_quiet() -> Result<()> { Ok(()) } + +#[tokio::test] +async fn self_update_noop_dry_run() -> Result<()> { + let context = uv_test::test_context!("3.12").with_filter(( + escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), + "v[CURRENT_VERSION]", + )); + + let target_version = env!("CARGO_PKG_VERSION"); + let (receipt_dir, server) = setup_mock_update(&context, target_version).await?; + + uv_snapshot!(context.filters(), context.self_update() + .arg(target_version) + .arg("--dry-run") + .env("AXOUPDATER_CONFIG_PATH", receipt_dir.as_os_str()) + .env(EnvVars::UV_INSTALLER_GHE_BASE_URL, server.uri()), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + info: Checking for updates... + You're on the latest version of uv (v[CURRENT_VERSION]) + "); + + Ok(()) +} + +#[tokio::test] +async fn self_update_noop_dry_run_quiet() -> Result<()> { + let context = uv_test::test_context!("3.12").with_filter(( + escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), + "v[CURRENT_VERSION]", + )); + + let target_version = env!("CARGO_PKG_VERSION"); + let (receipt_dir, server) = setup_mock_update(&context, target_version).await?; + + uv_snapshot!(context.filters(), context.self_update() + .arg(target_version) + .arg("--dry-run") + .arg("--quiet") + .env("AXOUPDATER_CONFIG_PATH", receipt_dir.as_os_str()) + .env(EnvVars::UV_INSTALLER_GHE_BASE_URL, server.uri()), @" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + "); + + Ok(()) +} From 8dc5660d6117187bf8fb98e06ad5f6fbae39c754 Mon Sep 17 00:00:00 2001 From: "Tomasz (Tom) Kramkowski" Date: Wed, 25 Mar 2026 11:01:47 +0000 Subject: [PATCH 5/5] Add a reusable current version filter function --- crates/uv-test/src/lib.rs | 10 ++++++++++ crates/uv/tests/it/self_update.rs | 21 ++++----------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/crates/uv-test/src/lib.rs b/crates/uv-test/src/lib.rs index c5732ec4ff9b8..acbf6b1bd01f1 100755 --- a/crates/uv-test/src/lib.rs +++ b/crates/uv-test/src/lib.rs @@ -582,6 +582,16 @@ impl TestContext { self } + /// Add a (not context aware) filter for the current uv version `v..` + #[must_use] + pub fn with_filtered_current_version(mut self) -> Self { + self.filters.push(( + regex::escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), + "v[CURRENT_VERSION]".to_string(), + )); + self + } + /// Adds filters for non-deterministic `CycloneDX` data #[must_use] pub fn with_cyclonedx_filters(mut self) -> Self { diff --git a/crates/uv/tests/it/self_update.rs b/crates/uv/tests/it/self_update.rs index 6af4ca57aaf6c..4316704e5a3e0 100644 --- a/crates/uv/tests/it/self_update.rs +++ b/crates/uv/tests/it/self_update.rs @@ -6,7 +6,6 @@ use axoupdater::{ ReleaseSourceType, test::helpers::{RuntestArgs, perform_runtest}, }; -use regex::escape; use serde_json::json; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; @@ -156,10 +155,7 @@ async fn setup_mock_update( #[tokio::test] async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> { - let context = uv_test::test_context!("3.12").with_filter(( - escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), - "v[CURRENT_VERSION]", - )); + let context = uv_test::test_context!("3.12").with_filtered_current_version(); let target_version = "9.9.9"; let (receipt_dir, server) = setup_mock_update(&context, target_version).await?; @@ -183,10 +179,7 @@ async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> { #[tokio::test] async fn self_update_dry_run_quiet() -> Result<()> { - let context = uv_test::test_context!("3.12").with_filter(( - escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), - "v[CURRENT_VERSION]", - )); + let context = uv_test::test_context!("3.12").with_filtered_current_version(); let target_version = "9.9.9"; let (receipt_dir, server) = setup_mock_update(&context, target_version).await?; @@ -237,10 +230,7 @@ async fn self_update_dry_run_extra_quiet() -> Result<()> { #[tokio::test] async fn self_update_noop_dry_run() -> Result<()> { - let context = uv_test::test_context!("3.12").with_filter(( - escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), - "v[CURRENT_VERSION]", - )); + let context = uv_test::test_context!("3.12").with_filtered_current_version(); let target_version = env!("CARGO_PKG_VERSION"); let (receipt_dir, server) = setup_mock_update(&context, target_version).await?; @@ -264,10 +254,7 @@ async fn self_update_noop_dry_run() -> Result<()> { #[tokio::test] async fn self_update_noop_dry_run_quiet() -> Result<()> { - let context = uv_test::test_context!("3.12").with_filter(( - escape(&format!("v{}", env!("CARGO_PKG_VERSION"))), - "v[CURRENT_VERSION]", - )); + let context = uv_test::test_context!("3.12").with_filtered_current_version(); let target_version = env!("CARGO_PKG_VERSION"); let (receipt_dir, server) = setup_mock_update(&context, target_version).await?;