use std::fs;
use assert_cmd::Command;
use proptest::prelude::*;
use rstest::rstest;
use similar::{ChangeTag, TextDiff};
use tempfile::tempdir;
#[path = "check_prediction/corpus.rs"]
mod corpus;
use corpus::CORPUS;
const FLAGS: [&str; 8] = [
"--wrap",
"--renumber",
"--breaks",
"--ellipsis",
"--fences",
"--footnotes",
"--code-emphasis",
"--headings",
];
struct Run {
status: i32,
stdout: String,
stderr: String,
bytes: Vec<u8>,
path: String,
}
fn run(mode: &[&str], input: &str, flags: &[&str]) -> Run {
let directory = tempdir().expect("create temporary directory");
let file = directory.path().join("input.md");
fs::write(&file, input).expect("write the document copy");
let output = Command::cargo_bin("mdtablefix")
.expect("cargo binary")
.args(mode)
.args(flags)
.arg(&file)
.output()
.expect("run mdtablefix");
Run {
status: output.status.code().unwrap_or(-1),
stdout: String::from_utf8(output.stdout).expect("stdout is UTF-8"),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
bytes: fs::read(&file).expect("read the copy back"),
path: file.to_string_lossy().into_owned(),
}
}
fn line_delta(before: &str, after: &str) -> (usize, usize) {
let mut insertions = 0;
let mut deletions = 0;
for change in TextDiff::from_lines(before, after).iter_all_changes() {
match change.tag() {
ChangeTag::Insert => insertions += 1,
ChangeTag::Delete => deletions += 1,
ChangeTag::Equal => {}
}
}
(insertions, deletions)
}
fn selected_flags(mask: u8) -> Vec<&'static str> {
FLAGS
.iter()
.enumerate()
.filter(|(index, _)| mask & (1 << index) != 0)
.map(|(_, flag)| *flag)
.collect()
}
fn predict(name: &str, input: &str, flags: &[&str]) -> Result<bool, TestCaseError> {
let printed = run(&[], input, flags);
let checked = run(&["--check"], input, flags);
let rewritten = run(&["--in-place"], input, flags);
prop_assert_eq!(
printed.status,
0,
"{}: printing failed under {:?}: {}",
name,
flags,
printed.stderr
);
prop_assert_eq!(
rewritten.bytes.as_slice(),
printed.stdout.as_bytes(),
"{}: --in-place must write exactly the bytes the printer prints, under {:?}",
name,
flags
);
prop_assert_eq!(
checked.bytes.as_slice(),
input.as_bytes(),
"{}: --check rewrote the document it was assessing",
name
);
prop_assert!(
checked.status == 0 || checked.status == 1,
"{}: --check exited {} on a readable document under {:?}: {}",
name,
checked.status,
flags,
checked.stderr
);
prop_assert_eq!(
rewritten.status,
0,
"{}: --in-place failed under {:?}: {}",
name,
flags,
rewritten.stderr
);
let changed = printed.stdout.as_bytes() != input.as_bytes();
prop_assert_eq!(
checked.status,
i32::from(changed),
"{}: --check must exit 1 exactly when the formatter changes the bytes, under {:?}",
name,
flags
);
if changed {
let (insertions, deletions) = line_delta(input, &printed.stdout);
prop_assert_eq!(
checked.stdout.lines().count(),
1,
"{}: a drifting document prints one report line under {:?}, printed {:?}",
name,
flags,
checked.stdout
);
prop_assert_eq!(
checked.stdout.trim_end_matches('\n'),
format!("{} +{insertions} -{deletions}", checked.path),
"{}: the report line must carry the rewrite's own delta under {:?}",
name,
flags
);
} else {
prop_assert!(
checked.stdout.is_empty(),
"{}: a clean document must print nothing under {:?}, printed {:?}",
name,
flags,
checked.stdout
);
}
Ok(changed)
}
fn assert_predicts(name: &str, input: &str, flags: &[&str]) -> bool {
predict(name, input, flags).unwrap_or_else(|error| panic!("{error}"))
}
#[rstest]
#[case(&[])]
#[case(&["--wrap"])]
#[case(&["--renumber"])]
#[case(&["--breaks"])]
#[case(&["--ellipsis"])]
#[case(&["--fences"])]
#[case(&["--footnotes"])]
#[case(&["--code-emphasis"])]
#[case(&["--headings"])]
#[case(&[
"--wrap",
"--renumber",
"--breaks",
"--ellipsis",
"--fences",
"--footnotes",
"--code-emphasis",
"--headings",
])]
fn check_predicts_in_place_over_the_corpus(#[case] flags: &[&str]) {
let mut drifted = 0;
let mut clean = 0;
for fixture in CORPUS {
let changed = assert_predicts(fixture.name, fixture.input, flags);
if changed {
drifted += 1;
} else {
clean += 1;
}
if let Some(flag) = fixture.flag
&& flags.contains(&flag)
{
assert!(
changed,
"{} must drift under {flag}, the flag it was chosen for",
fixture.name
);
}
}
assert!(
drifted > 0,
"no corpus document drifted under {flags:?}, so the prediction is untested"
);
assert!(
clean > 0,
"every corpus document drifted under {flags:?}, so the clean case is untested"
);
}
fn markdown_lines() -> impl Strategy<Value = &'static str> {
prop_oneof![
Just("|A|B|"),
Just("|---|---|"),
Just("|1|2|"),
Just(""),
Just("prose words here"),
Just("1. item"),
Just("***"),
Just("```sh"),
Just("echo hi"),
Just("```"),
Just("---"),
Just("Title"),
]
}
fn markdown_document() -> impl Strategy<Value = String> {
(
prop::collection::vec(markdown_lines(), 0..12),
prop_oneof![Just("\n"), Just("\r\n")],
any::<bool>(),
any::<bool>(),
)
.prop_map(|(lines, ending, bom, terminated)| {
let mut text = String::new();
if bom {
text.push('\u{FEFF}');
}
for (index, line) in lines.iter().enumerate() {
text.push_str(line);
if index + 1 < lines.len() || terminated {
text.push_str(ending);
}
}
text
})
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(48))]
#[test]
fn check_predicts_in_place_on_generated_documents(
document in markdown_document(),
mask in any::<u8>(),
) {
let flags = selected_flags(mask);
if let Err(error) = predict("generated", &document, &flags) {
prop_assert!(false, "{error}");
}
}
}