137 lines
3.9 KiB
Rust
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
|
|
}
|