Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/uv-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,16 @@ impl TestContext {
self
}

/// Add a (not context aware) filter for the current uv version `v<major>.<minor>.<patch>`
#[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 {
Expand Down
14 changes: 7 additions & 7 deletions crates/uv/src/commands/self_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub(crate) async fn self_update(
) -> Result<ExitStatus> {
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`)",
Expand All @@ -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!(
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -309,7 +309,7 @@ async fn run_updater(
};

writeln!(
printer.stderr(),
printer.stderr_important(),
"{}",
format_args!(
"{}{} {direction} uv {}! {}",
Expand Down Expand Up @@ -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.",
Expand Down
11 changes: 11 additions & 0 deletions crates/uv/src/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
156 changes: 145 additions & 11 deletions crates/uv/tests/it/self_update.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
use std::process::Command;
use std::{path::PathBuf, process::Command};

use anyhow::Result;
use assert_fs::prelude::*;
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};

use uv_static::EnvVars;

use uv_test::{get_bin, uv_snapshot};
use uv_test::{TestContext, get_bin, uv_snapshot};

#[test]
fn check_self_update() {
Expand Down Expand Up @@ -50,7 +49,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"),
Expand All @@ -64,13 +63,41 @@ fn test_self_update_offline_error() {
");
}

#[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]",
));
#[test]
fn 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 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 -----
");
}
Copy link

Copilot AI Mar 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tests added for --quiet/--quiet --quiet cover the offline error path, but they don’t exercise the new behavior introduced in this PR (messages written via stderr_important() should appear with -q and be suppressed with -qq). Consider adding an integration test that triggers an update-needed or update-success message and asserts that it is present with a single --quiet but absent when --quiet is repeated.

Suggested change
}
}
/// With `-q`, important messages (emitted via `stderr_important()`) should still be shown.
#[test]
fn test_self_update_quiet_shows_important() {
let context = uv_test::test_context!("3.12");
let output = context
.self_update()
.arg("--quiet")
.output()
.expect("failed to run 'uv self update --quiet'");
assert!(
output.status.success(),
"'uv self update --quiet' returned non-zero"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.trim().is_empty(),
"expected important message on stderr with --quiet"
);
}
/// With `-qq`, important messages should be suppressed (no stderr output) while still succeeding.
#[test]
fn test_self_update_extra_quiet_suppresses_important() {
let context = uv_test::test_context!("3.12");
let output = context
.self_update()
.arg("--quiet")
.arg("--quiet")
.output()
.expect("failed to run 'uv self update --quiet --quiet'");
assert!(
output.status.success(),
"'uv self update --quiet --quiet' returned non-zero"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.trim().is_empty(),
"expected no important messages on stderr with --quiet --quiet"
);
}

Copilot uses AI. Check for mistakes.

/// 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()?;

Expand Down Expand Up @@ -100,7 +127,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 {
Expand All @@ -124,6 +150,16 @@ 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_filtered_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")
Expand All @@ -140,3 +176,101 @@ async fn test_self_update_uses_legacy_path_with_ghe_override() -> Result<()> {

Ok(())
}

#[tokio::test]
async fn self_update_dry_run_quiet() -> Result<()> {
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?;

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 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 target_version = "9.9.9";
let (receipt_dir, server) = setup_mock_update(&context, target_version).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(())
}

#[tokio::test]
async fn self_update_noop_dry_run() -> Result<()> {
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?;

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_filtered_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(())
}
Loading