use std::fmt::Debug;
use anyhow::Result;
use serde::Serialize;
use serde::ser::SerializeMap;
use crate::expectation::Expectation;
use crate::lossy_string;
use crate::newline::BytesNewline;
use crate::newline::SplitLinesByNewline;
pub struct DiffTool {
expectations: Vec<Expectation>,
}
impl DiffTool {
pub fn new(expectations: Vec<Expectation>) -> Self {
Self { expectations }
}
pub fn diff(&self, output: &[u8]) -> Result<Diff> {
let lines = output.split_at_newline();
let to_output_list = |i| -> (usize, Vec<u8>) { (i, lines[i].to_owned()) };
let mut expectation_index = 0;
let mut line_index = 0;
let mut diffs = vec![];
let mut match_start = None;
while expectation_index < self.expectations.len() && line_index < lines.len() {
let expectation = &self.expectations[expectation_index];
let next_expectation = self.expectations.get(expectation_index + 1);
let line = lines[line_index];
if expectation.matches(line) {
if expectation.multiline {
if let Some(next_expectation) = next_expectation {
if (expectation.optional || match_start.is_some())
&& next_expectation.matches(line)
{
if let Some(match_start_index) = match_start {
diffs.push(DiffLine::MatchedExpectation {
index: expectation_index,
expectation: expectation.to_owned(),
lines: (match_start_index..line_index)
.map(to_output_list)
.collect(),
});
}
expectation_index += 1;
match_start = None;
continue;
}
}
if match_start.is_none() {
match_start = Some(line_index);
}
line_index += 1;
continue;
}
diffs.push(DiffLine::MatchedExpectation {
index: expectation_index,
expectation: expectation.to_owned(),
lines: vec![(line_index, line.to_owned())],
});
line_index += 1;
expectation_index += 1;
continue;
}
if let Some(match_start_index) = match_start {
diffs.push(DiffLine::MatchedExpectation {
index: expectation_index,
expectation: expectation.to_owned(),
lines: (match_start_index..line_index)
.map(to_output_list)
.collect(),
});
match_start = None;
expectation_index += 1;
continue;
}
match_start = None;
match self.peek_match(line_index, &lines, expectation_index) {
PeekMatch::NextExpectation(next_expectation_index) => {
(expectation_index..next_expectation_index)
.filter(|index| !self.expectations[*index].optional)
.for_each(|index| {
diffs.push(DiffLine::UnmatchedExpectation {
index,
expectation: self.expectations[index].clone(),
});
});
expectation_index = next_expectation_index;
}
PeekMatch::NextLine(next_line_index) => {
diffs.push(DiffLine::UnexpectedLines {
lines: (line_index..next_line_index).map(to_output_list).collect(),
});
line_index = next_line_index;
}
PeekMatch::None => {
if !expectation.optional {
diffs.push(DiffLine::UnmatchedExpectation {
index: expectation_index,
expectation: expectation.to_owned(),
});
}
expectation_index += 1;
}
}
}
if let Some(match_start) = match_start {
diffs.push(DiffLine::MatchedExpectation {
index: expectation_index,
expectation: self.expectations[expectation_index].to_owned(),
lines: (match_start..line_index).map(to_output_list).collect(),
});
expectation_index += 1;
}
if expectation_index < self.expectations.len() {
(expectation_index..self.expectations.len())
.filter(|index| !self.expectations[*index].optional)
.for_each(|index| {
diffs.push(DiffLine::UnmatchedExpectation {
index,
expectation: self.expectations[index].to_owned(),
})
});
}
if line_index < lines.len() {
diffs.push(DiffLine::UnexpectedLines {
lines: (line_index..lines.len()).map(to_output_list).collect(),
});
}
Ok(Diff::new(diffs))
}
fn peek_match(
&self,
current_line_index: usize,
lines: &[&[u8]],
current_expectation_index: usize,
) -> PeekMatch {
let expectation_index = self
.peek_matching_expectation(lines[current_line_index], current_expectation_index + 1);
if let Some(expectation_index) = expectation_index {
return PeekMatch::NextExpectation(expectation_index);
}
let line_index = self.peek_matching_line(
&self.expectations[current_expectation_index],
current_line_index + 1,
lines,
);
if let Some(line_index) = line_index {
return PeekMatch::NextLine(line_index);
}
PeekMatch::None
}
fn peek_matching_line(
&self,
expectation: &Expectation,
start_line_index: usize,
lines: &[&[u8]],
) -> Option<usize> {
lines
.iter()
.skip(start_line_index)
.position(|line| expectation.matches(line))
.map(|position| position + start_line_index)
}
fn peek_matching_expectation(
&self,
line: &[u8],
start_expectation_index: usize,
) -> Option<usize> {
self.expectations
.iter()
.skip(start_expectation_index)
.position(|expectation| expectation.matches(line))
.map(|position| position + start_expectation_index)
}
}
enum PeekMatch {
NextExpectation(usize),
NextLine(usize),
None,
}
#[derive(Clone, PartialEq, Serialize)]
pub struct Diff {
pub lines: Vec<DiffLine>,
#[serde(skip)]
pub count_matched: usize,
#[serde(skip)]
pub count_unmatched: usize,
#[serde(skip)]
pub count_output_lines: usize,
}
impl Diff {
pub fn new(lines: Vec<DiffLine>) -> Self {
let (mut count_matched, mut count_unmatched, mut count_output_lines) = (0, 0, 0);
lines.iter().for_each(|line| match line {
DiffLine::MatchedExpectation {
index: _,
expectation: _,
lines,
} => {
count_matched += 1;
count_output_lines += lines.len();
}
DiffLine::UnmatchedExpectation {
index: _,
expectation: _,
} => {
count_unmatched += 1;
}
DiffLine::UnexpectedLines { lines } => count_output_lines += lines.len(),
});
Self {
lines,
count_matched,
count_unmatched,
count_output_lines,
}
}
pub fn has_differences(&self) -> bool {
self.lines.iter().any(|line| {
!matches!(
line,
DiffLine::MatchedExpectation {
index: _,
expectation: _,
lines: _,
}
)
})
}
}
impl Debug for Diff {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut rendered = String::new();
let (mut count_matched, mut count_unmatched, mut count_unexpected) = (0, 0, 0);
for line in &self.lines {
match &line {
DiffLine::MatchedExpectation {
index: _,
expectation: _,
lines,
} => count_matched += lines.len(),
DiffLine::UnmatchedExpectation {
index: _,
expectation: _,
} => count_unmatched += 1,
DiffLine::UnexpectedLines { lines } => count_unexpected += lines.len(),
}
rendered.push_str(&format!("{line:?}"));
}
writeln!(
f,
"[matched: {count_matched}, unmatched: {count_unmatched}, unexpected: {count_unexpected}]",
)?;
write!(f, "{rendered}")?;
Ok(())
}
}
#[derive(Clone, PartialEq)]
pub enum DiffLine {
MatchedExpectation {
index: usize,
expectation: Expectation,
lines: Vec<(usize, Vec<u8>)>,
},
UnmatchedExpectation {
index: usize,
expectation: Expectation,
},
UnexpectedLines {
lines: Vec<(usize, Vec<u8>)>,
},
}
impl Debug for DiffLine {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::MatchedExpectation {
index,
expectation,
lines,
} => {
writeln!(f, "{:04} | = {}", index + 1, expectation)?;
for (index, line) in lines {
write!(f, " {:04} | = {}", index + 1, lossy_string!(line))?;
}
Ok(())
}
Self::UnmatchedExpectation { index, expectation } => {
writeln!(f, "{:04} | - {}", index + 1, expectation)
}
Self::UnexpectedLines { lines } => {
for (index, line) in lines {
let eol = (line.as_ref() as &[u8]).ends_in_newline();
write!(
f,
" {:04} | + {}{}",
index + 1,
lossy_string!(line),
if eol { "" } else { " (no-eol)" }
)?;
}
Ok(())
}
}
}
}
impl Serialize for DiffLine {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
match self {
DiffLine::MatchedExpectation {
index,
expectation,
lines,
} => {
let mut variant = serializer.serialize_map(Some(4))?;
variant.serialize_entry("kind", "matched_expectation")?;
variant.serialize_entry("index", index)?;
variant.serialize_entry("expectation", &expectation)?;
variant.serialize_entry("lines", &lines_to_strings(lines))?;
variant.end()
}
DiffLine::UnmatchedExpectation { index, expectation } => {
let mut variant = serializer.serialize_map(Some(3))?;
variant.serialize_entry("kind", "unmatched_expectation")?;
variant.serialize_entry("index", index)?;
variant.serialize_entry("expectation", &expectation)?;
variant.end()
}
DiffLine::UnexpectedLines { lines } => {
let mut variant = serializer.serialize_map(Some(2))?;
variant.serialize_entry("kind", "unexpected_lines")?;
variant.serialize_entry("lines", &lines_to_strings(lines))?;
variant.end()
}
}
}
}
fn lines_to_strings(lines: &[(usize, Vec<u8>)]) -> Vec<(usize, String)> {
lines
.iter()
.map(|(index, line)| (*index, lossy_string!(line)))
.collect::<Vec<_>>()
}
#[cfg(test)]
mod tests {
use super::DiffLine;
use super::DiffTool;
use crate::bformatln;
use crate::blines;
use crate::diff::Diff;
use crate::test_expectation;
#[test]
fn test_exact_match() {
let differ = DiffTool {
expectations: vec![test_expectation!("equal", "foo")],
};
let diffs = differ.diff(&bformatln!("foo")).expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_exact_no_match() {
let differ = DiffTool {
expectations: vec![test_expectation!("equal", "bar")],
};
let diffs = differ.diff(&bformatln!("foo")).expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_quantifiers_optional() {
let tests = &[
(
DiffTool {
expectations: vec![test_expectation!("equal", "foo", false, false)],
},
vec![],
false,
),
(
DiffTool {
expectations: vec![test_expectation!("equal", "foo", false, false)],
},
blines!("foo"),
true,
),
(
DiffTool {
expectations: vec![test_expectation!("equal", "foo", true, false)],
},
vec![],
true,
),
(
DiffTool {
expectations: vec![test_expectation!("equal", "foo", true, false)],
},
blines!("foo"),
true,
),
(
DiffTool {
expectations: vec![
test_expectation!("equal", "foo", false, false),
test_expectation!("equal", "bar", false, false),
test_expectation!("equal", "baz", false, false),
],
},
blines!("foo", "bar", "baz"),
true,
),
(
DiffTool {
expectations: vec![
test_expectation!("equal", "foo", true, false),
test_expectation!("equal", "bar", true, false),
test_expectation!("equal", "baz", true, false),
],
},
blines!("foo", "bar", "baz"),
true,
),
(
DiffTool {
expectations: vec![
test_expectation!("equal", "foo", true, false),
test_expectation!("equal", "bar", true, false),
test_expectation!("equal", "baz", true, false),
],
},
blines!("bar", "baz"),
true,
),
(
DiffTool {
expectations: vec![
test_expectation!("equal", "foo", true, false),
test_expectation!("equal", "bar", true, false),
test_expectation!("equal", "baz", true, false),
],
},
blines!("foo", "baz"),
true,
),
(
DiffTool {
expectations: vec![
test_expectation!("equal", "foo", true, false),
test_expectation!("equal", "bar", true, false),
test_expectation!("equal", "baz", true, false),
],
},
blines!("foo", "bar"),
true,
),
(
DiffTool {
expectations: vec![
test_expectation!("equal", "foo", true, false),
test_expectation!("equal", "bar", true, false),
test_expectation!("equal", "baz", true, false),
],
},
vec![],
true,
),
];
for (idx, (differ, lines, expect)) in tests.iter().enumerate() {
let diffs = differ.diff(lines).expect("diff created");
assert_eq!(
!diffs.has_differences(),
*expect,
"test {}: for input empty = {} with optional = {} -> {:?}",
idx + 1,
lines.is_empty(),
differ.expectations[0].optional,
diffs
)
}
}
#[test]
fn test_unmatched_middle_expectation() {
let differ = make();
let diffs = differ.diff(&blines!("foo", "baz")).expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_unmatched_tailing_expectation() {
let differ = make();
let diffs = differ.diff(&blines!("foo", "bar")).expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_unmatched_and_unexpected() {
let differ = make();
let diffs = differ
.diff(&blines!("foo", "bar", "zoing"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_unmatched_heading_expectation() {
let differ = make();
let diffs = differ.diff(&blines!("bar", "baz")).expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_unexpected_heading_lines() {
let differ = make();
let diffs = differ
.diff(&blines!("something", "bla", "foo", "bar", "baz"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_unexpected_intermediate_lines() {
let differ = make();
let diffs = differ
.diff(&blines!("foo", "bar", "something", "bla", "baz"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_unexpected_tailing_lines() {
let differ = make();
let diffs = differ
.diff(&blines!("foo", "bar", "baz", "something", "more"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_unused_expectations() {
let differ = make();
let diffs = differ.diff(&blines!("foo")).expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_multiline_expectation() {
let differ = DiffTool {
expectations: vec![test_expectation!("glob", "f*", false, true)],
};
let diffs = differ
.diff(&blines!("foo", "fun", "fact"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_starting_multiline_expectation() {
let differ = DiffTool {
expectations: vec![
test_expectation!("glob", "f*", false, true),
test_expectation!("equal", "bar", false, true),
],
};
let diffs = differ
.diff(&blines!("foo", "fun", "fact", "bar"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_middle_multiline_expectation() {
let differ = DiffTool {
expectations: vec![
test_expectation!("equal", "baz", false, true),
test_expectation!("glob", "f*", false, true),
test_expectation!("equal", "bar", false, true),
],
};
let diffs = differ
.diff(&blines!("baz", "foo", "fun", "fact", "bar"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_tailing_multiline_expectation() {
let differ = DiffTool {
expectations: vec![
test_expectation!("equal", "baz", false, true),
test_expectation!("glob", "f*", false, true),
],
};
let diffs = differ
.diff(&blines!("baz", "foo", "fun", "fact"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_regression_excess_expectations_after_multiline_fail() {
let differ = DiffTool {
expectations: vec![
test_expectation!("glob", "*", false, true),
test_expectation!("equal", "bar", false, true),
],
};
let diffs = differ
.diff(&blines!("foo", "foo", "baz"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_matching_non_multiline_precedent_over_matching_multiline() {
let differ = DiffTool {
expectations: vec![
test_expectation!("glob", "*", false, true),
test_expectation!("equal", "bar", false, true),
],
};
let diffs = differ
.diff(&blines!("foo", "foo", "bar"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_next_expectation_is_used_first() {
let differ = DiffTool {
expectations: vec![
test_expectation!("equal", "foo"),
test_expectation!("equal", ""),
test_expectation!("equal", "bar"),
test_expectation!("equal", ""),
test_expectation!("equal", "baz"),
test_expectation!("equal", ""),
test_expectation!("equal", "zoing"),
],
};
let diffs = differ
.diff(&blines!("foo", "", "baz", "", "zoing"))
.expect("no error");
insta::assert_debug_snapshot!(diffs);
}
#[test]
fn test_serialize() {
let diff = Diff::new(vec![
DiffLine::MatchedExpectation {
index: 0,
expectation: test_expectation!("equal", "matched", false, false),
lines: vec![(0, bformatln!("line content"))],
},
DiffLine::UnmatchedExpectation {
index: 0,
expectation: test_expectation!("equal", "unmatched", false, false),
},
DiffLine::UnexpectedLines {
lines: vec![(0, bformatln!("line content"))],
},
]);
let rendered = serde_yaml::to_string(&diff).expect("render to yaml");
insta::assert_snapshot!(&rendered);
}
fn make() -> DiffTool {
DiffTool {
expectations: vec![
test_expectation!("equal", "foo"),
test_expectation!("equal", "bar"),
test_expectation!("equal", "baz"),
],
}
}
}