use std::{fs, path::Path, time::SystemTime};
use assert_cmd::Command;
use tempfile::tempdir;
const RAGGED: &str = "|A|B|\n|---|---|\n|1|2|\n";
const ALT_RAGGED: &str = "|A|B|C|\n|---|---|---|\n|1|2|3|\n";
const UNTERMINATED: &str = "|A|B|\n|---|---|\n|1|2|";
const CLEAN: &str = "| A | B |\n| --- | --- |\n| 1 | 2 |\n";
const HUNK: &str = "@@ -1,3 +1,3 @@";
const TABLES: usize = 4;
const PROSE_PER_TABLE: usize = 340;
#[cfg(unix)]
const CLOSED_PIPE_FILES: usize = 500;
#[cfg(unix)]
const CLOSED_PIPE_PADDING: usize = 180;
#[cfg(not(unix))]
const CLOSED_PIPE_FILES: usize = 250;
#[cfg(not(unix))]
const CLOSED_PIPE_PADDING: usize = 80;
const FIRST_TABLE_DELETION: &str = "-|A0|B0|";
const PROSE_DELETION: &str = "-prose line";
const REPEATS: usize = 10;
fn spread_out() -> String {
let mut lines = Vec::new();
let mut prose = 0;
for table in 0..TABLES {
lines.push(format!("|A{table}|B{table}|"));
lines.push(String::from("|---|---|"));
lines.push(format!("|{table}0|{table}1|"));
for _ in 0..PROSE_PER_TABLE {
lines.push(format!("prose line {prose}"));
prose += 1;
}
}
let mut content = lines.join("\n");
content.push('\n');
content
}
fn run_in(directory: &Path, args: &[&str]) -> std::process::Output {
Command::cargo_bin("mdtablefix")
.expect("cargo binary")
.current_dir(directory)
.args(args)
.output()
.expect("run mdtablefix")
}
fn stdout_of(output: &std::process::Output) -> String {
String::from_utf8(output.stdout.clone()).expect("stdout is UTF-8")
}
fn stderr_of(output: &std::process::Output) -> String {
String::from_utf8(output.stderr.clone()).expect("stderr is UTF-8")
}
fn status_of(output: &std::process::Output) -> i32 { output.status.code().unwrap_or(-1) }
fn repeated_output(directory: &Path, args: &[&str], expected_status: i32) -> String {
let mut first: Option<String> = None;
for attempt in 0..REPEATS {
let output = run_in(directory, args);
assert_eq!(
status_of(&output),
expected_status,
"attempt {attempt} exited {} rather than {expected_status}: {}",
status_of(&output),
stderr_of(&output)
);
let stdout = stdout_of(&output);
match &first {
None => first = Some(stdout),
Some(expected) => assert_eq!(
&stdout, expected,
"attempt {attempt} differed from the first run"
),
}
}
first.expect("REPEATS is not zero, so at least one run happened")
}
#[test]
fn diff_is_deterministic_across_ten_runs() {
let dir = tempdir().expect("create temporary directory");
fs::write(dir.path().join("ragged.md"), RAGGED).expect("write fixture");
let stdout = repeated_output(dir.path(), &["--diff", "ragged.md"], 1);
assert!(
stdout.contains(HUNK),
"ten identical empty outputs would be vacuous: {stdout:?}"
);
}
#[test]
fn diff_is_deterministic_above_the_threshold() {
let dir = tempdir().expect("create temporary directory");
fs::write(dir.path().join("spread.md"), spread_out()).expect("write fixture");
let stdout = repeated_output(dir.path(), &["--diff", "spread.md"], 1);
assert!(
stdout.contains(FIRST_TABLE_DELETION),
"the tables must be diffed, not skipped: {stdout:?}"
);
let replaced = stdout.matches(PROSE_DELETION).count();
assert_eq!(
replaced,
0,
"the diff must localise the change to the tables; {replaced} of the {} untouched prose \
lines came back as deleted, which is a wholesale replacement rather than a diff",
TABLES * PROSE_PER_TABLE
);
}
#[test]
fn diff_is_independent_of_its_siblings() {
let dir = tempdir().expect("create temporary directory");
fs::write(dir.path().join("alpha.md"), RAGGED).expect("write fixture");
fs::write(dir.path().join("bravo.md"), ALT_RAGGED).expect("write fixture");
let alpha = run_in(dir.path(), &["--diff", "alpha.md"]);
let bravo = run_in(dir.path(), &["--diff", "bravo.md"]);
assert_eq!(status_of(&alpha), 1, "{}", stderr_of(&alpha));
assert_eq!(status_of(&bravo), 1, "{}", stderr_of(&bravo));
let alone_alpha = stdout_of(&alpha);
let alone_bravo = stdout_of(&bravo);
assert_ne!(
alone_alpha, alone_bravo,
"the two fixtures must produce different diffs, or this test is vacuous"
);
let together = run_in(dir.path(), &["--diff", "alpha.md", "bravo.md"]);
let reversed = run_in(dir.path(), &["--diff", "bravo.md", "alpha.md"]);
assert_eq!(
stdout_of(&together),
format!("{alone_alpha}{alone_bravo}"),
"a file's diff must not depend on its siblings"
);
assert_eq!(
stdout_of(&reversed),
format!("{alone_bravo}{alone_alpha}"),
"reversing the arguments must reverse the blocks and nothing else"
);
}
type Snapshot = Vec<(String, u64, Option<SystemTime>)>;
fn snapshot(directory: &Path) -> Snapshot {
let mut entries: Snapshot = fs::read_dir(directory)
.expect("read directory")
.map(|entry| {
let entry = entry.expect("read directory entry");
let metadata = entry.metadata().expect("read metadata");
(
entry.file_name().to_string_lossy().into_owned(),
metadata.len(),
metadata.modified().ok(),
)
})
.collect();
entries.sort();
entries
}
#[test]
fn directory_snapshot_unchanged() {
let dir = tempdir().expect("create temporary directory");
fs::write(dir.path().join("clean.md"), CLEAN).expect("write fixture");
fs::write(dir.path().join("ragged.md"), RAGGED).expect("write fixture");
let before = snapshot(dir.path());
let output = run_in(
dir.path(),
&["--diff", "clean.md", "ragged.md", "missing.md"],
);
assert_eq!(status_of(&output), 2, "an unreadable file must exit 2");
assert!(
stderr_of(&output).contains("missing.md"),
"the run must name the unreadable file, so the error is a read failure rather than a \
rejected command line: {}",
stderr_of(&output)
);
assert!(
stdout_of(&output).contains(HUNK),
"one unreadable file must not suppress the diff of the readable ones: {:?}",
stdout_of(&output)
);
assert_eq!(
snapshot(dir.path()),
before,
"--diff must leave entry set, lengths, and modification times untouched"
);
}
#[test]
fn an_unterminated_original_is_marked() {
let dir = tempdir().expect("create temporary directory");
fs::write(dir.path().join("ragged.md"), UNTERMINATED).expect("write fixture");
let output = run_in(dir.path(), &["--diff", "ragged.md"]);
assert_eq!(status_of(&output), 1, "drift must exit 1");
assert_eq!(
stdout_of(&output),
"--- 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 a_closed_pipe_is_a_successful_early_exit() {
use std::{
io::Read as _,
process::{Command, Stdio},
};
let dir = tempdir().expect("create temporary directory");
let names: Vec<String> = (0..CLOSED_PIPE_FILES)
.map(|index| format!("{index:0>3}-{}.md", "d".repeat(CLOSED_PIPE_PADDING)))
.collect();
for name in &names {
fs::write(dir.path().join(name), RAGGED).expect("write fixture");
}
let mut child = Command::new(env!("CARGO_BIN_EXE_mdtablefix"))
.arg("--diff")
.args(&names)
.current_dir(dir.path())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn mdtablefix");
let mut stdout = child.stdout.take().expect("child stdout is piped");
let mut first = [0u8; 1];
stdout.read_exact(&mut first).expect("read one byte");
drop(stdout);
let output = child.wait_with_output().expect("wait for mdtablefix");
let stderr = String::from_utf8_lossy(&output.stderr);
#[cfg(unix)]
assert_eq!(
output.status.code(),
Some(0),
"a closed pipe is an early exit, not a failure: {stderr}"
);
#[cfg(not(unix))]
assert!(
matches!(output.status.code(), Some(0 | 1)),
"a closed pipe is an early exit or a completed run, never an undocumented status: {stderr}"
);
assert!(
!stderr.contains("panicked"),
"the write failure is handled, not panicked on: {stderr}"
);
}