use std::fmt;
use crate::error::{Payload, TestError};
use crate::trace::TraceEntry;
const RED: &str = "\x1b[31m";
const GREEN: &str = "\x1b[32m";
const RESET: &str = "\x1b[0m";
pub(crate) fn render(error: &TestError, f: &mut fmt::Formatter<'_>, colorize: bool) -> fmt::Result {
match &error.message {
Some(message) => writeln!(f, "{}: {message}", error.kind.headline())?,
None => writeln!(f, "{}", error.kind.headline())?,
}
for frame in &error.context {
writeln!(f, " while {}", frame.message)?;
}
if !error.trace.is_empty() {
writeln!(f, " trace:")?;
for entry in &error.trace {
match entry {
TraceEntry::Step(message) => writeln!(f, " - {message}")?,
TraceEntry::Kv { key, value } => writeln!(f, " - {key} = {value}")?,
}
}
}
if let Some(payload) = error.payload.as_deref() {
render_payload(payload, f, colorize)?;
}
write!(f, " at {}", error.location)
}
fn render_payload(payload: &Payload, f: &mut fmt::Formatter<'_>, colorize: bool) -> fmt::Result {
match payload {
Payload::ExpectedActual {
expected,
actual,
diff,
} => {
if colorize {
writeln!(f, " expected: {GREEN}{expected}{RESET}")?;
writeln!(f, " actual: {RED}{actual}{RESET}")?;
} else {
writeln!(f, " expected: {expected}")?;
writeln!(f, " actual: {actual}")?;
}
if let Some(diff) = diff {
for line in diff.lines() {
match (colorize, diff_line_color(line)) {
(true, Some(color)) => writeln!(f, " {color}{line}{RESET}")?,
_ => writeln!(f, " {line}")?,
}
}
}
}
Payload::Other(inner) => {
writeln!(f, " caused by: {inner}")?;
let mut source = inner.source();
while let Some(current) = source {
writeln!(f, " caused by: {current}")?;
source = current.source();
}
}
Payload::Multiple(errors) => {
let count = errors.len();
let noun = if count == 1 { "failure" } else { "failures" };
writeln!(f, " {count} {noun}:")?;
for (index, sub) in errors.iter().enumerate() {
writeln!(f, " [{}]", index + 1)?;
let rendered = sub.to_string();
for line in rendered.lines() {
writeln!(f, " {line}")?;
}
}
}
}
Ok(())
}
fn diff_line_color(line: &str) -> Option<&'static str> {
match line.as_bytes().first() {
Some(b'-') => Some(RED),
Some(b'+') => Some(GREEN),
_ => None,
}
}
#[cfg(test)]
mod tests {
use crate::color::{ColorChoice, color_choice, set_color_choice};
use crate::error::{ContextFrame, ErrorKind, Payload, TestError};
use crate::{OrFail, TestResult, Trace};
use test_better_matchers::{check, is_false, is_true};
#[test]
fn render_has_no_trailing_newline() -> TestResult {
let rendered = TestError::new(ErrorKind::Assertion).to_string();
check!(rendered.ends_with('\n'))
.satisfies(is_false())
.or_fail()?;
Ok(())
}
#[test]
fn nested_multiple_indents_each_line() -> TestResult {
let inner = TestError::new(ErrorKind::Assertion)
.with_message("inner")
.with_context_frame(ContextFrame::new("inner context"));
let outer =
TestError::new(ErrorKind::Assertion).with_payload(Payload::Multiple(vec![inner]));
let rendered = outer.to_string();
check!(rendered.contains(" assertion failed: inner"))
.satisfies(is_true())
.or_fail()?;
check!(rendered.contains(" while inner context"))
.satisfies(is_true())
.or_fail()?;
Ok(())
}
#[test]
fn trace_breadcrumbs_render_in_chronological_order() -> TestResult {
let mut trace = Trace::new();
trace.step("connecting to db");
trace.kv("db_url", "postgres://localhost");
trace.step("running the query");
let error = TestError::new(ErrorKind::Assertion).with_message("query returned no rows");
drop(trace);
let rendered = error.to_string();
let connect = rendered
.find("- connecting to db")
.or_fail_with("step rendered")?;
let url = rendered
.find("- db_url = postgres://localhost")
.or_fail_with("kv rendered")?;
let query = rendered
.find("- running the query")
.or_fail_with("second step rendered")?;
check!(connect < url).satisfies(is_true()).or_fail()?;
check!(url < query).satisfies(is_true()).or_fail()?;
Ok(())
}
#[test]
fn debug_colorizes_only_when_color_is_on() -> TestResult {
let _guard = crate::color::TEST_LOCK
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let original = color_choice();
let error = TestError::new(ErrorKind::Assertion).with_payload(Payload::ExpectedActual {
expected: "line one\nline two".to_string(),
actual: "line one\nline 2".to_string(),
diff: Some("-line two\n+line 2".to_string()),
});
set_color_choice(ColorChoice::Always);
let colored = format!("{error:?}");
set_color_choice(ColorChoice::Never);
let plain = format!("{error:?}");
set_color_choice(ColorChoice::Always);
let display = format!("{error}");
set_color_choice(original);
check!(colored.contains("\x1b[31m"))
.satisfies(is_true())
.or_fail()?;
check!(colored.contains("\x1b[32m"))
.satisfies(is_true())
.or_fail()?;
check!(plain.contains('\x1b'))
.satisfies(is_false())
.or_fail()?;
check!(display.contains('\x1b'))
.satisfies(is_false())
.or_fail()?;
Ok(())
}
}