shout/shout-rs/src/format.rs
Chris Wanstrath 175899001a Add Rust implementation of the shout test runner
Rewrites the shout CLI in Rust for better performance, with parallel
test execution via rayon and the same .shout file format semantics.
2026-04-02 13:28:48 -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();
}