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, pub exit_code_mismatch: bool, } #[derive(Debug)] pub struct TestResult { pub path: String, pub passed: bool, pub command_count: usize, pub failures: Vec, pub error: Option, } 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(); }