diff --git a/crates/uv-git/src/source.rs b/crates/uv-git/src/source.rs index 58599f6512a8..9cbefc60a08d 100644 --- a/crates/uv-git/src/source.rs +++ b/crates/uv-git/src/source.rs @@ -133,7 +133,7 @@ impl GitSource { // Report the checkout operation to the reporter. if let Some(task) = task { if let Some(reporter) = self.reporter.as_ref() { - reporter.on_checkout_complete(remote.url(), short_id.as_str(), task); + reporter.on_checkout_complete(remote.url(), actual_rev.as_str(), task); } } diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 6a5c90c3e94a..aa35eb162ce3 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -579,6 +579,10 @@ impl EnvVars { #[attr_hidden] pub const UV_TEST_INDEX_URL: &'static str = "UV_TEST_INDEX_URL"; + /// Hide progress messages with non-deterministic order in tests. + #[attr_hidden] + pub const UV_TEST_NO_CLI_PROGRESS: &'static str = "UV_TEST_NO_CLI_PROGRESS"; + /// `.env` files from which to load environment variables when executing `uv run` commands. pub const UV_ENV_FILE: &'static str = "UV_ENV_FILE"; diff --git a/crates/uv/src/commands/reporters.rs b/crates/uv/src/commands/reporters.rs index 1e3fdb1e50cd..b3a022e5bdba 100644 --- a/crates/uv/src/commands/reporters.rs +++ b/crates/uv/src/commands/reporters.rs @@ -1,4 +1,6 @@ use std::env; +use std::fmt::Write; +use std::sync::LazyLock; use std::sync::{Arc, Mutex}; use std::time::Duration; @@ -7,6 +9,8 @@ use owo_colors::OwoColorize; use rustc_hash::FxHashMap; use url::Url; +use crate::commands::human_readable_bytes; +use crate::printer::Printer; use uv_cache::Removal; use uv_distribution_types::{ BuildableSource, CachedDist, DistributionMetadata, Name, SourceDist, VersionOrUrlRef, @@ -16,7 +20,10 @@ use uv_pep440::Version; use uv_python::PythonInstallationKey; use uv_static::EnvVars; -use crate::printer::Printer; +/// Since downloads, fetches and builds run in parallel, their message output order is +/// non-deterministic, so can't capture them in test output. +static HAS_UV_TEST_NO_CLI_PROGRESS: LazyLock = + LazyLock::new(|| env::var(EnvVars::UV_TEST_NO_CLI_PROGRESS).is_ok()); #[derive(Debug)] struct ProgressReporter { @@ -44,6 +51,8 @@ struct BarState { sizes: Vec, /// A map of progress bars, by ID. bars: FxHashMap, + /// The download size, if known, by ID. + download_size: FxHashMap>, /// A monotonic counter for bar IDs. id: usize, } @@ -95,11 +104,15 @@ impl ProgressReporter { ); progress.set_style(ProgressStyle::with_template("{wide_msg}").unwrap()); - progress.set_message(format!( - "{} {}", + let message = format!( + " {} {}", "Building".bold().cyan(), source.to_color_string() - )); + ); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!(self.printer.stderr(), "{message}"); + } + progress.set_message(message); state.headers += 1; state.bars.insert(id, progress); @@ -107,7 +120,11 @@ impl ProgressReporter { } fn on_build_complete(&self, source: &BuildableSource, id: usize) { - let ProgressMode::Multi { state, .. } = &self.mode else { + let ProgressMode::Multi { + state, + multi_progress, + } = &self.mode + else { return; }; @@ -117,11 +134,15 @@ impl ProgressReporter { state.bars.remove(&id).unwrap() }; - progress.finish_with_message(format!( - " {} {}", + let message = format!( + " {} {}", "Built".bold().green(), source.to_color_string() - )); + ); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!(self.printer.stderr(), "{message}"); + } + progress.finish_with_message(message); } fn on_download_start(&self, name: String, size: Option) -> usize { @@ -145,7 +166,7 @@ impl ProgressReporter { ProgressBar::with_draw_target(size, self.printer.target()), ); - if size.is_some() { + if let Some(size) = size { // We're using binary bytes to match `human_readable_bytes`. progress.set_style( ProgressStyle::with_template( @@ -154,15 +175,36 @@ impl ProgressReporter { .unwrap() .progress_chars("--"), ); + // If the download is larger than 1MB, show a message to indicate that this may take + // a while keeping the log concise. + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS && size > 1024 * 1024 { + let (bytes, unit) = human_readable_bytes(size); + let _ = writeln!( + self.printer.stderr(), + "{} {} {}", + "Downloading".bold().cyan(), + name, + format!("({bytes:.1}{unit})").dimmed() + ); + } progress.set_message(name); } else { progress.set_style(ProgressStyle::with_template("{wide_msg:.dim} ....").unwrap()); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!( + self.printer.stderr(), + "{} {}", + "Downloading".bold().cyan(), + name + ); + } progress.set_message(name); progress.finish(); } let id = state.id(); state.bars.insert(id, progress); + state.download_size.insert(id, size); id } @@ -175,11 +217,29 @@ impl ProgressReporter { } fn on_download_complete(&self, id: usize) { - let ProgressMode::Multi { state, .. } = &self.mode else { + let ProgressMode::Multi { + state, + multi_progress, + } = &self.mode + else { return; }; let progress = state.lock().unwrap().bars.remove(&id).unwrap(); + + let size = state.lock().unwrap().download_size[&id]; + if multi_progress.is_hidden() + && !*HAS_UV_TEST_NO_CLI_PROGRESS + && size.is_none_or(|size| size > 1024 * 1024) + { + let _ = writeln!( + self.printer.stderr(), + " {} {}", + "Downloaded".bold().green(), + progress.message() + ); + } + progress.finish_and_clear(); } @@ -201,12 +261,11 @@ impl ProgressReporter { ); progress.set_style(ProgressStyle::with_template("{wide_msg}").unwrap()); - progress.set_message(format!( - "{} {} ({})", - "Updating".bold().cyan(), - url, - rev.dimmed() - )); + let message = format!(" {} {} ({})", "Updating".bold().cyan(), url, rev.dimmed()); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!(self.printer.stderr(), "{message}"); + } + progress.set_message(message); progress.finish(); state.headers += 1; @@ -215,7 +274,11 @@ impl ProgressReporter { } fn on_checkout_complete(&self, url: &Url, rev: &str, id: usize) { - let ProgressMode::Multi { state, .. } = &self.mode else { + let ProgressMode::Multi { + state, + multi_progress, + } = &self.mode + else { return; }; @@ -225,12 +288,16 @@ impl ProgressReporter { state.bars.remove(&id).unwrap() }; - progress.finish_with_message(format!( - " {} {} ({})", + let message = format!( + " {} {} ({})", "Updated".bold().green(), url, rev.dimmed() - )); + ); + if multi_progress.is_hidden() && !*HAS_UV_TEST_NO_CLI_PROGRESS { + let _ = writeln!(self.printer.stderr(), "{message}"); + } + progress.finish_with_message(message); } } diff --git a/crates/uv/tests/it/common/mod.rs b/crates/uv/tests/it/common/mod.rs index da96add6af92..a4dd1f187804 100644 --- a/crates/uv/tests/it/common/mod.rs +++ b/crates/uv/tests/it/common/mod.rs @@ -555,6 +555,9 @@ impl TestContext { .env(EnvVars::UV_PYTHON_DOWNLOADS, "never") .env(EnvVars::UV_TEST_PYTHON_PATH, self.python_path()) .env(EnvVars::UV_EXCLUDE_NEWER, EXCLUDE_NEWER) + // Since downloads, fetches and builds run in parallel, their message output order is + // non-deterministic, so can't capture them in test output. + .env(EnvVars::UV_TEST_NO_CLI_PROGRESS, "1") .env_remove(EnvVars::UV_CACHE_DIR) .env_remove(EnvVars::UV_TOOL_BIN_DIR) .current_dir(self.temp_dir.path());