shout/src/format.rs
2026-04-02 15:18:22 -07:00

189 lines
5.6 KiB
Rust

use crate::matching::{DiffKind, DiffLine, diff, match_output};
use crate::parse::ExitCode;
use crate::run::CommandResult;
// ANSI color codes
const RED: &str = "\x1b[31m";
const GREEN: &str = "\x1b[32m";
#[allow(dead_code)]
const YELLOW: &str = "\x1b[33m";
const DIM: &str = "\x1b[2m";
const RESET: &str = "\x1b[0m";
#[derive(Debug)]
pub struct FailedCommand {
pub result: CommandResult,
pub diff_lines: Vec<DiffLine>,
pub exit_code_mismatch: bool,
}
#[derive(Debug)]
pub struct TestResult {
pub path: String,
pub passed: bool,
pub command_count: usize,
pub failures: Vec<FailedCommand>,
pub error: Option<String>,
}
pub fn evaluate_file(
path: &str,
results: &[CommandResult],
error: Option<&str>,
) -> TestResult {
if let Some(err) = error {
return TestResult {
path: path.to_string(),
passed: false,
command_count: results.len(),
failures: vec![],
error: Some(err.to_string()),
};
}
let mut failures = Vec::new();
for result in results {
let output_matches = match_output(&result.command.expected, &result.actual);
let exit_code_mismatch = match &result.command.exit_code {
ExitCode::Default => result.exit_code != 0,
ExitCode::Any => result.exit_code == 0,
ExitCode::Code(expected) => result.exit_code != *expected,
};
if !output_matches || exit_code_mismatch {
failures.push(FailedCommand {
result: result.clone(),
diff_lines: if output_matches {
vec![]
} else {
diff(&result.command.expected, &result.actual)
},
exit_code_mismatch,
});
}
}
TestResult {
path: path.to_string(),
passed: failures.is_empty(),
command_count: results.len(),
failures,
error: None,
}
}
pub fn format_failure(test: &TestResult) -> String {
let mut lines = Vec::new();
lines.push(format!("{RED}FAIL {}{RESET}", test.path));
if let Some(ref err) = test.error {
lines.push(format!(" {RED}{err}{RESET}"));
return lines.join("\n");
}
for failure in &test.failures {
lines.push(String::new());
lines.push(format!(" {DIM}${RESET} {}", failure.result.command.command));
if !failure.diff_lines.is_empty() {
let mut expected_lines = Vec::new();
let mut actual_lines = Vec::new();
for dl in &failure.diff_lines {
let text = if dl.kind == DiffKind::Context {
format!("{DIM}{}{RESET}", dl.text)
} else {
dl.text.clone()
};
match dl.kind {
DiffKind::Expected | DiffKind::Equal | DiffKind::Context => {
let prefix = if dl.kind == DiffKind::Expected {
format!("{GREEN} > {RESET}")
} else {
" ".to_string()
};
expected_lines.push(format!("{prefix}{text}"));
}
_ => {}
}
match dl.kind {
DiffKind::Actual | DiffKind::Equal | DiffKind::Context => {
let prefix = if dl.kind == DiffKind::Actual {
format!("{RED} > {RESET}")
} else {
" ".to_string()
};
actual_lines.push(format!("{prefix}{text}"));
}
_ => {}
}
}
lines.push(format!("{GREEN} expected:{RESET}"));
lines.extend(expected_lines);
lines.push(format!("{RED} actual:{RESET}"));
lines.extend(actual_lines);
}
if failure.exit_code_mismatch {
let expected = match &failure.result.command.exit_code {
ExitCode::Default => "0".to_string(),
ExitCode::Any => "non-zero".to_string(),
ExitCode::Code(c) => c.to_string(),
};
lines.push(format!("{GREEN} expected exit code: {expected}{RESET}"));
lines.push(format!("{RED} actual exit code: {}{RESET}", failure.result.exit_code));
}
}
lines.join("\n")
}
pub fn format_summary(
results: &[TestResult],
elapsed_ms: f64,
single_file: Option<&str>,
) -> String {
let total_commands: usize = results.iter().map(|r| r.command_count).sum();
let failed_commands: usize = results.iter().map(|r| r.failures.len()).sum();
let passed_commands = total_commands - failed_commands;
let mut parts = Vec::new();
if passed_commands > 0 {
parts.push(format!("{GREEN}{passed_commands} passed{RESET}"));
}
if failed_commands > 0 {
parts.push(format!("{RED}{failed_commands} failed{RESET}"));
}
let time = if elapsed_ms < 1000.0 {
format!("{}ms", elapsed_ms.round() as u64)
} else {
format!("{:.1}s", elapsed_ms / 1000.0)
};
let label = match single_file {
Some(f) => format!(" in {f}"),
None => String::new(),
};
format!("{}{label} {DIM}[{time}]{RESET}", parts.join(", "))
}
/// Print a green dot for pass, red F for fail.
pub fn print_dot(passed: bool) {
use std::io::{Write, stdout};
let s = if passed {
format!("{GREEN}.{RESET}")
} else {
format!("{RED}F{RESET}")
};
let _ = stdout().write_all(s.as_bytes());
let _ = stdout().flush();
}