use std::io;
use camino::Utf8Path;
use rstest::rstest;
use super::{DiffOptions, render_summary, write_unified_diff};
const WRITER_FAILURE_MESSAGE: &str = "closed";
struct FailingWriter {
budget: usize,
}
impl io::Write for FailingWriter {
fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
if buffer.len() > self.budget {
return Err(io::Error::new(
io::ErrorKind::BrokenPipe,
WRITER_FAILURE_MESSAGE,
));
}
self.budget -= buffer.len();
Ok(buffer.len())
}
fn flush(&mut self) -> io::Result<()> { Ok(()) }
}
#[rstest]
#[case(0, 0, 0, "No files were analysed.")]
#[case(0, 0, 1, "1 file could not be read.")]
#[case(0, 0, 2, "2 files could not be read.")]
#[case(0, 1, 0, "1 file left unchanged.")]
#[case(0, 1, 1, "1 file left unchanged, 1 file could not be read.")]
#[case(0, 1, 2, "1 file left unchanged, 2 files could not be read.")]
#[case(0, 2, 0, "2 files left unchanged.")]
#[case(0, 2, 1, "2 files left unchanged, 1 file could not be read.")]
#[case(0, 2, 2, "2 files left unchanged, 2 files could not be read.")]
#[case(1, 0, 0, "1 file would be reformatted.")]
#[case(1, 0, 1, "1 file would be reformatted, 1 file could not be read.")]
#[case(1, 0, 2, "1 file would be reformatted, 2 files could not be read.")]
#[case(1, 1, 0, "1 file would be reformatted, 1 file left unchanged.")]
#[case(
1,
1,
1,
"1 file would be reformatted, 1 file left unchanged, 1 file could not be read."
)]
#[case(
1,
1,
2,
"1 file would be reformatted, 1 file left unchanged, 2 files could not be read."
)]
#[case(1, 2, 0, "1 file would be reformatted, 2 files left unchanged.")]
#[case(
1,
2,
1,
"1 file would be reformatted, 2 files left unchanged, 1 file could not be read."
)]
#[case(
1,
2,
2,
"1 file would be reformatted, 2 files left unchanged, 2 files could not be read."
)]
#[case(2, 0, 0, "2 files would be reformatted.")]
#[case(2, 0, 1, "2 files would be reformatted, 1 file could not be read.")]
#[case(2, 0, 2, "2 files would be reformatted, 2 files could not be read.")]
#[case(2, 1, 0, "2 files would be reformatted, 1 file left unchanged.")]
#[case(
2,
1,
1,
"2 files would be reformatted, 1 file left unchanged, 1 file could not be read."
)]
#[case(
2,
1,
2,
"2 files would be reformatted, 1 file left unchanged, 2 files could not be read."
)]
#[case(2, 2, 0, "2 files would be reformatted, 2 files left unchanged.")]
#[case(
2,
2,
1,
"2 files would be reformatted, 2 files left unchanged, 1 file could not be read."
)]
#[case(
2,
2,
2,
"2 files would be reformatted, 2 files left unchanged, 2 files could not be read."
)]
fn summary_renders_every_combination(
#[case] changed: usize,
#[case] unchanged: usize,
#[case] errored: usize,
#[case] expected: &str,
) {
assert_eq!(render_summary(changed, unchanged, errored), expected);
}
#[test]
fn an_unterminated_original_marks_the_deleted_line() {
let mut out = Vec::new();
write_unified_diff(
&mut out,
Utf8Path::new("ragged.md"),
"|A|B|\n|---|---|\n|1|2|",
"| A | B |\n| --- | --- |\n| 1 | 2 |\n",
DiffOptions {
context_radius: 3,
patience_threshold: 1000,
},
)
.expect("writing to a Vec cannot fail");
assert_eq!(
String::from_utf8(out).expect("diff is UTF-8"),
concat!(
"--- ragged.md\n",
"+++ ragged.md\n",
"@@ -1,3 +1,3 @@\n",
"-|A|B|\n",
"-|---|---|\n",
"-|1|2|\n",
"\\ No newline at end of file\n",
"+| A | B |\n",
"+| --- | --- |\n",
"+| 1 | 2 |\n",
)
);
}
#[test]
fn equal_texts_render_nothing() {
let mut out = Vec::new();
write_unified_diff(
&mut out,
Utf8Path::new("clean.md"),
"| A | B |\n",
"| A | B |\n",
DiffOptions {
context_radius: 3,
patience_threshold: 1000,
},
)
.expect("writing to a Vec cannot fail");
assert!(out.is_empty(), "a clean file has no diff: {out:?}");
}
#[rstest]
#[case(0)]
#[case(16)]
fn a_failing_writer_error_reaches_the_caller(#[case] budget: usize) {
let mut out = FailingWriter { budget };
let error = write_unified_diff(
&mut out,
Utf8Path::new("ragged.md"),
"|A|B|\n",
"| A | B |\n",
DiffOptions {
context_radius: 3,
patience_threshold: 1000,
},
)
.expect_err("a writer that fails must fail the render");
assert_eq!(
error.kind(),
io::ErrorKind::BrokenPipe,
"the writer's own error must reach the caller: {error}"
);
assert_eq!(
error.to_string(),
WRITER_FAILURE_MESSAGE,
"the writer's own message must survive, not just its kind: {error}"
);
}
#[test]
fn summary_grammar_is_snapshot_stable() {
let mut rendered = String::new();
for changed in 0..3 {
for unchanged in 0..3 {
for errored in 0..3 {
rendered.push_str(&render_summary(changed, unchanged, errored));
rendered.push('\n');
}
}
}
insta::assert_snapshot!("summary_grammar", rendered);
}