Better rendering for multiline error messages (#17132)

Split out from https://github.com/astral-sh/uv/pull/17110

Indent multiline error messages properly, and add a test with a
multiline context and a context below since that combination isn't
captured atm.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
konsti 2025-12-15 17:29:11 +01:00 committed by GitHub
parent a768a9d111
commit a5d50a20d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 73 additions and 6 deletions

3
Cargo.lock generated
View File

@ -6959,6 +6959,9 @@ name = "uv-warnings"
version = "0.0.7" version = "0.0.7"
dependencies = [ dependencies = [
"anstream", "anstream",
"anyhow",
"indoc",
"insta",
"owo-colors", "owo-colors",
"rustc-hash", "rustc-hash",
] ]

View File

@ -19,3 +19,8 @@ workspace = true
anstream = { workspace = true } anstream = { workspace = true }
owo-colors = { workspace = true } owo-colors = { workspace = true }
rustc-hash = { workspace = true } rustc-hash = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
indoc = { workspace = true }
insta = { workspace = true }

View File

@ -74,6 +74,15 @@ macro_rules! warn_user_once {
/// warning: Failed to create registry entry for Python 3.12 /// warning: Failed to create registry entry for Python 3.12
/// Caused By: Security policy forbids chaining registry entries /// Caused By: Security policy forbids chaining registry entries
/// ``` /// ```
///
/// ```text
/// error: Failed to download Python 3.12
/// Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
/// Server says: This endpoint only support POST requests.
///
/// For downloads, please refer to https://example.com/download/python3.13.tar.zst
/// Caused by: Caused By: HTTP Error 400
/// ```
pub fn write_error_chain( pub fn write_error_chain(
err: &dyn Error, err: &dyn Error,
mut stream: impl std::fmt::Write, mut stream: impl std::fmt::Write,
@ -88,12 +97,62 @@ pub fn write_error_chain(
err.to_string().trim() err.to_string().trim()
)?; )?;
for source in iter::successors(err.source(), |&err| err.source()) { for source in iter::successors(err.source(), |&err| err.source()) {
writeln!( let msg = source.to_string();
&mut stream, let mut lines = msg.lines();
" {}: {}", if let Some(first) = lines.next() {
"Caused by".color(color).bold(), let padding = " ";
source.to_string().trim() let cause = "Caused by";
)?; let child_padding = " ".repeat(padding.len() + cause.len() + 2);
writeln!(
&mut stream,
"{}{}: {}",
padding,
cause.color(color).bold(),
first.trim()
)?;
for line in lines {
let line = line.trim_end();
if line.is_empty() {
// Avoid showing indents on empty lines
writeln!(&mut stream)?;
} else {
writeln!(&mut stream, "{}{}", child_padding, line.trim_end())?;
}
}
}
} }
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use crate::write_error_chain;
use anyhow::anyhow;
use indoc::indoc;
use insta::assert_snapshot;
use owo_colors::AnsiColors;
#[test]
fn format_multiline_message() {
let err_middle = indoc! {"Failed to fetch https://example.com/upload/python3.13.tar.zst
Server says: This endpoint only support POST requests.
For downloads, please refer to https://example.com/download/python3.13.tar.zst"};
let err = anyhow!("Caused By: HTTP Error 400")
.context(err_middle)
.context("Failed to download Python 3.12");
let mut rendered = String::new();
write_error_chain(err.as_ref(), &mut rendered, "error", AnsiColors::Red).unwrap();
let rendered = anstream::adapter::strip_str(&rendered);
assert_snapshot!(rendered, @r"
error: Failed to download Python 3.12
Caused by: Failed to fetch https://example.com/upload/python3.13.tar.zst
Server says: This endpoint only support POST requests.
For downloads, please refer to https://example.com/download/python3.13.tar.zst
Caused by: Caused By: HTTP Error 400
");
}
}