use std::fs;
use std::path::Path;
use std::process::{Command, Output};
use tempfile::TempDir;
const ABSENT_LINTER: &str = "rumdl-absent-linter";
const ABSENT_FORMATTER: &str = "rumdl-absent-formatter";
const YAML_DOC: &str = "# T\n\n```yaml\nkey: value\n```\n";
const BOTH_SLOTS: &str = "lint = [\"absent-lint\"], format = [\"absent-fmt\"]";
const LINT_ONLY: &str = "lint = [\"absent-lint\"]";
const FORMAT_ONLY: &str = "format = [\"absent-fmt\"]";
fn setup_with(slots: &str, extra: &str) -> TempDir {
let dir = tempfile::tempdir().unwrap();
let config = format!(
"[code-block-tools]\nenabled = true\nnormalize-language = \"exact\"\n{extra}\n\n\
[code-block-tools.tools.absent-lint]\ncommand = [\"{ABSENT_LINTER}\", \"-\"]\nstdin = true\nstdout = true\n\n\
[code-block-tools.tools.absent-fmt]\ncommand = [\"{ABSENT_FORMATTER}\", \"-\"]\nstdin = true\nstdout = true\n\n\
[code-block-tools.languages]\nyaml = {{ {slots} }}\n"
);
fs::write(dir.path().join(".rumdl.toml"), config).unwrap();
fs::write(dir.path().join("t.md"), YAML_DOC).unwrap();
dir
}
fn setup(extra: &str) -> TempDir {
setup_with(BOTH_SLOTS, extra)
}
fn run(dir: &Path, args: &[&str]) -> Output {
Command::new(env!("CARGO_BIN_EXE_rumdl"))
.current_dir(dir)
.args(args)
.arg("--no-cache")
.arg("t.md")
.output()
.unwrap()
}
fn stdout_of(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).to_string()
}
fn stderr_of(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).to_string()
}
fn json_findings(output: &Output) -> Vec<(u64, String, String)> {
let stdout = stdout_of(output);
let parsed: serde_json::Value = serde_json::from_str(&stdout).unwrap_or_else(|e| {
panic!("stdout was not JSON ({e}): {stdout}");
});
parsed
.as_array()
.expect("JSON output is an array")
.iter()
.map(|f| {
(
f["line"].as_u64().expect("line"),
f["rule"].as_str().expect("rule").to_string(),
f["message"].as_str().expect("message").to_string(),
)
})
.collect()
}
const FAIL: &str = "code-block-tools.on-missing-tool-binary = \"fail\"";
const IGNORE: &str = "code-block-tools.on-missing-tool-binary = \"ignore\"";
#[test]
fn fmt_reports_the_missing_formatter_in_json_rather_than_an_empty_list() {
let dir = setup_with(FORMAT_ONLY, "");
let output = run(dir.path(), &["fmt", "--output-format", "json", "--config", FAIL]);
let findings = json_findings(&output);
assert_eq!(
findings.len(),
1,
"a run that formatted nothing must not report an empty list: {findings:?}"
);
let (line, rule, message) = &findings[0];
assert_eq!(*line, 3, "reported against the code block, not the file");
assert_eq!(rule, "code-block-tools");
assert!(
message.contains(ABSENT_FORMATTER),
"the message names the binary: {message}"
);
assert_eq!(output.status.code(), Some(2));
}
#[test]
fn check_and_fmt_report_the_missing_tool_the_same_way() {
let lint_dir = setup_with(LINT_ONLY, "");
let format_dir = setup_with(FORMAT_ONLY, "");
let check = run(lint_dir.path(), &["check", "--output-format", "json", "--config", FAIL]);
let fmt = run(format_dir.path(), &["fmt", "--output-format", "json", "--config", FAIL]);
let check_findings = json_findings(&check);
let fmt_findings = json_findings(&fmt);
assert_eq!(check_findings.len(), 1, "{check_findings:?}");
assert_eq!(fmt_findings.len(), 1, "{fmt_findings:?}");
assert_eq!(check_findings[0].0, fmt_findings[0].0);
assert_eq!(check_findings[0].1, fmt_findings[0].1);
assert_eq!(
check_findings[0].2.replace(ABSENT_LINTER, ""),
fmt_findings[0].2.replace(ABSENT_FORMATTER, "")
);
assert!(check_findings[0].2.contains(ABSENT_LINTER));
assert!(fmt_findings[0].2.contains(ABSENT_FORMATTER));
}
#[test]
fn check_fix_reports_the_missing_formatter_in_json() {
let dir = setup("");
let output = run(
dir.path(),
&["check", "--fix", "--output-format", "json", "--config", FAIL],
);
let findings = json_findings(&output);
assert!(
findings
.iter()
.any(|(_, rule, message)| rule == "code-block-tools" && message.contains(ABSENT_FORMATTER)),
"{findings:?}"
);
assert_eq!(output.status.code(), Some(2));
}
#[test]
fn a_tool_error_under_warn_is_not_reported_as_a_finding() {
let dir = setup("on-error = \"warn\"");
let output = run(dir.path(), &["fmt", "--output-format", "json", "--config", IGNORE]);
assert_eq!(json_findings(&output), vec![], "stderr: {}", stderr_of(&output));
assert_eq!(output.status.code(), Some(0));
}
#[test]
fn the_default_reports_a_missing_tool_as_a_config_warning() {
let dir = setup("");
let output = run(dir.path(), &["check"]);
let stderr = stderr_of(&output);
assert!(stderr.contains("[config warning]"), "stderr: {stderr}");
assert!(stderr.contains(ABSENT_LINTER), "the warning names the binary: {stderr}");
assert_eq!(output.status.code(), Some(0), "stderr: {stderr}");
}
#[test]
fn the_default_fails_the_run_under_deny_config_warnings() {
let dir = setup("");
let output = run(dir.path(), &["check", "--deny-config-warnings"]);
assert_eq!(
output.status.code(),
Some(2),
"the guard a pre-commit hook sets has to catch this: {}",
stderr_of(&output)
);
}
#[test]
fn fmt_names_the_formatter_and_check_names_the_linter() {
let dir = setup("");
let check = stderr_of(&run(dir.path(), &["check"]));
assert!(check.contains(ABSENT_LINTER), "{check}");
assert!(!check.contains(ABSENT_FORMATTER), "{check}");
let fmt = stderr_of(&run(dir.path(), &["fmt"]));
assert!(fmt.contains(ABSENT_FORMATTER), "{fmt}");
assert!(!fmt.contains(ABSENT_LINTER), "{fmt}");
}
#[test]
fn ignore_stays_silent_even_under_deny_config_warnings() {
let dir = setup("");
let output = run(dir.path(), &["check", "--deny-config-warnings", "--config", IGNORE]);
let stderr = stderr_of(&output);
assert!(!stderr.contains(ABSENT_LINTER), "stderr: {stderr}");
assert_eq!(output.status.code(), Some(0), "stderr: {stderr}");
}
#[test]
fn fail_reports_the_missing_tool_once_rather_than_twice() {
let dir = setup("");
let output = run(dir.path(), &["check", "--config", FAIL]);
let stderr = stderr_of(&output);
assert!(!stderr.contains("[config warning]"), "stderr: {stderr}");
assert_eq!(output.status.code(), Some(1));
}
#[test]
fn no_code_block_tools_says_nothing_about_tools_it_will_not_run() {
let dir = setup("");
let output = run(
dir.path(),
&["check", "--no-code-block-tools", "--deny-config-warnings"],
);
let stderr = stderr_of(&output);
assert!(!stderr.contains(ABSENT_LINTER), "stderr: {stderr}");
assert_eq!(output.status.code(), Some(0), "stderr: {stderr}");
}
#[test]
fn a_disabled_section_reports_nothing() {
let dir = tempfile::tempdir().unwrap();
fs::write(
dir.path().join(".rumdl.toml"),
format!(
"[code-block-tools]\nenabled = false\n\n\
[code-block-tools.tools.absent-lint]\ncommand = [\"{ABSENT_LINTER}\"]\n\n\
[code-block-tools.languages]\nyaml = {{ lint = [\"absent-lint\"] }}\n"
),
)
.unwrap();
fs::write(dir.path().join("t.md"), YAML_DOC).unwrap();
let output = run(dir.path(), &["check", "--deny-config-warnings"]);
let stderr = stderr_of(&output);
assert!(!stderr.contains(ABSENT_LINTER), "stderr: {stderr}");
assert_eq!(output.status.code(), Some(0), "stderr: {stderr}");
}
#[test]
fn warn_on_the_language_setting_says_it_does_nothing() {
let dir = setup("");
let output = run(
dir.path(),
&[
"check",
"--config",
"code-block-tools.on-missing-language-definition = \"warn\"",
],
);
let stderr = stderr_of(&output);
assert!(
stderr.contains("on-missing-language-definition") && stderr.contains("behaves as"),
"stderr: {stderr}"
);
}