189 lines
5.6 KiB
Rust
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();
|
|
}
|