use std::fs;
use std::io::Write;
use std::process::{Command, Output, Stdio};
const COMMON: &[&str] = &["--no-cache", "--no-config", "--color", "never"];
fn both_routes(args: &[&str], input: &[u8]) -> [(&'static str, Output); 2] {
let dir = tempfile::tempdir().unwrap();
fs::write(dir.path().join("doc.md"), input).unwrap();
let file = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.current_dir(dir.path())
.args(args)
.args(COMMON)
.arg("doc.md")
.output()
.unwrap();
let mut child = Command::new(env!("CARGO_BIN_EXE_rumdl"))
.current_dir(dir.path())
.args(args)
.args(COMMON)
.args(["-", "--stdin-filename", "doc.md"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.unwrap();
child.stdin.take().unwrap().write_all(input).unwrap();
let stdin = child.wait_with_output().unwrap();
[("file", file), ("stdin", stdin)]
}
fn describe(route: &str, output: &Output) -> String {
format!(
"{route}: exit {:?}\nstdout:\n{}\nstderr:\n{}",
output.status.code(),
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
}
fn assert_lists(args: &[&str], input: &[u8], expected: &[&str], diffs: bool) {
for (route, output) in both_routes(args, input) {
let context = describe(route, &output);
let stdout = String::from_utf8(output.stdout).unwrap();
let listed: Vec<&str> = stdout.lines().filter(|line| line.starts_with("doc.md:")).collect();
assert_eq!(listed, expected, "{context}");
assert_eq!(stdout.contains("\n@@ "), diffs, "{context}");
assert_eq!(output.status.code(), Some(1), "{context}");
}
}
#[test]
fn a_finding_the_diff_resolves_is_not_listed_though_it_carries_no_fix() {
assert_lists(
&["check", "--diff"],
b"# T\n\n```text\ncode\n```\n\n indented\n",
&[],
true,
);
}
#[test]
fn a_finding_whose_fix_the_run_does_not_apply_is_listed() {
assert_lists(
&["check", "--diff", "--unfixable", "MD022"],
b"# Title\ntext\n",
&["doc.md:1:1: [MD022] Expected 1 blank line below heading"],
false,
);
}
#[test]
fn a_finding_is_listed_where_it_sits_in_the_document_the_diff_applies_to() {
assert_lists(
&["check", "--diff"],
b"# Title\ntext\n\n[a][missing]\n",
&["doc.md:4:1: [MD052] Reference 'missing' not found"],
true,
);
}
#[test]
fn a_finding_is_told_apart_from_an_identical_one_the_diff_resolves() {
let document = format!(
"# T\n{}{}\n\n{}x\n",
"a".repeat(75),
" ".repeat(15),
&"word ".repeat(18)[..89]
);
assert_lists(
&["check", "--diff"],
document.as_bytes(),
&["doc.md:4:81: [MD013] Line length 90 exceeds 80 characters"],
true,
);
}
#[test]
fn a_json_lines_preview_prints_every_finding_and_no_diff() {
let modes: &[(&[&str], i32)] = &[
(&["check", "--diff"], 1),
(&["fmt", "--check"], 1),
(&["fmt", "--diff"], 0),
];
for (mode, code) in modes {
let args = [mode, &["--output-format", "json-lines"][..]].concat();
for (route, output) in both_routes(&args, b"# Title\ntext\n\n[a][missing]\n") {
let context = format!("{args:?} {}", describe(route, &output));
assert_eq!(output.status.code(), Some(*code), "{context}");
let findings: Vec<(String, u64)> = String::from_utf8_lossy(&output.stdout)
.lines()
.map(|line| {
let finding: serde_json::Value = serde_json::from_str(line)
.unwrap_or_else(|error| panic!("{line:?} is not JSON: {error}\n{context}"));
(
finding["rule"].as_str().unwrap().to_string(),
finding["line"].as_u64().unwrap(),
)
})
.collect();
assert_eq!(
findings,
[("MD022".to_string(), 1), ("MD052".to_string(), 4)],
"{context}"
);
}
}
}