shout/src/matching.rs

137 lines
3.9 KiB
Rust

/// 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 mut pos = 0;
for (i, part) in parts.iter().enumerate() {
if i == 0 {
if !actual.starts_with(part) {
return false;
}
pos = part.len();
} else if i == parts.len() - 1 {
if !actual[pos..].ends_with(part) {
return false;
}
} else {
match actual[pos..].find(part) {
Some(idx) => pos += idx + part.len(),
None => return false,
}
}
}
true
}
/// 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
}