Rewrites the shout CLI in Rust for better performance, with parallel test execution via rayon and the same .shout file format semantics.
124 lines
3.7 KiB
Rust
124 lines
3.7 KiB
Rust
use regex::Regex;
|
|
|
|
/// Check if a single line matches a pattern that may contain inline `...` wildcards.
|
|
pub fn match_line(pattern: &str, actual: &str) -> bool {
|
|
if !pattern.contains("...") {
|
|
return pattern == actual;
|
|
}
|
|
|
|
let parts: Vec<&str> = pattern.split("...").collect();
|
|
let escaped: Vec<String> = parts.iter().map(|p| regex::escape(p)).collect();
|
|
let re_str = format!("^{}$", escaped.join(".*"));
|
|
match Regex::new(&re_str) {
|
|
Ok(re) => re.is_match(actual),
|
|
Err(_) => false,
|
|
}
|
|
}
|
|
|
|
/// Match expected output against actual output, supporting multi-line `...` wildcards.
|
|
pub fn match_output(expected: &[String], actual: &[String]) -> bool {
|
|
do_match(expected, 0, actual, 0)
|
|
}
|
|
|
|
fn do_match(expected: &[String], ei: usize, actual: &[String], ai: usize) -> bool {
|
|
// Both exhausted — match
|
|
if ei == expected.len() && ai == actual.len() {
|
|
return true;
|
|
}
|
|
|
|
// Expected exhausted but actual remains — no match
|
|
if ei == expected.len() {
|
|
return false;
|
|
}
|
|
|
|
let exp = &expected[ei];
|
|
|
|
// Multi-line wildcard
|
|
if exp == "..." {
|
|
// Try matching zero or more actual lines
|
|
for skip in ai..=actual.len() {
|
|
if do_match(expected, ei + 1, actual, skip) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Actual exhausted but expected remains — no match
|
|
if ai == actual.len() {
|
|
return false;
|
|
}
|
|
|
|
// Line-level match (with possible inline wildcards)
|
|
if match_line(exp, &actual[ai]) {
|
|
return do_match(expected, ei + 1, actual, ai + 1);
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum DiffKind {
|
|
Equal,
|
|
Expected,
|
|
Actual,
|
|
Context,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DiffLine {
|
|
pub kind: DiffKind,
|
|
pub text: String,
|
|
}
|
|
|
|
pub fn diff(expected: &[String], actual: &[String]) -> Vec<DiffLine> {
|
|
let mut result = Vec::new();
|
|
let mut ei = 0;
|
|
let mut ai = 0;
|
|
|
|
while ei < expected.len() || ai < actual.len() {
|
|
if ei < expected.len() && expected[ei] == "..." {
|
|
// Find where the wildcard ends by looking at next expected line
|
|
let next_exp = if ei + 1 < expected.len() {
|
|
Some(&expected[ei + 1])
|
|
} else {
|
|
None
|
|
};
|
|
if next_exp.is_none() {
|
|
// ... at end matches everything remaining
|
|
result.push(DiffLine { kind: DiffKind::Context, text: "...".to_string() });
|
|
break;
|
|
}
|
|
// Skip actual lines until we find the next expected match
|
|
result.push(DiffLine { kind: DiffKind::Context, text: "...".to_string() });
|
|
ei += 1;
|
|
let next = next_exp.unwrap();
|
|
while ai < actual.len() && !match_line(next, &actual[ai]) {
|
|
ai += 1;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ei < expected.len() && ai < actual.len() {
|
|
if match_line(&expected[ei], &actual[ai]) {
|
|
result.push(DiffLine { kind: DiffKind::Equal, text: actual[ai].clone() });
|
|
ei += 1;
|
|
ai += 1;
|
|
} else {
|
|
result.push(DiffLine { kind: DiffKind::Expected, text: expected[ei].clone() });
|
|
result.push(DiffLine { kind: DiffKind::Actual, text: actual[ai].clone() });
|
|
ei += 1;
|
|
ai += 1;
|
|
}
|
|
} else if ei < expected.len() {
|
|
result.push(DiffLine { kind: DiffKind::Expected, text: expected[ei].clone() });
|
|
ei += 1;
|
|
} else {
|
|
result.push(DiffLine { kind: DiffKind::Actual, text: actual[ai].clone() });
|
|
ai += 1;
|
|
}
|
|
}
|
|
|
|
result
|
|
}
|