use std::process::Command;
use tempfile::tempdir;
fn rumdl() -> Command {
Command::new(env!("CARGO_BIN_EXE_rumdl"))
}
const TOOL_ERROR: i32 = 2;
#[test]
fn invalid_utf8_file_is_a_tool_error() {
let dir = tempdir().unwrap();
let path = dir.path().join("bad.md");
std::fs::write(&path, [b'#', b' ', 0xff, 0xfe, b'\n']).unwrap();
let status = rumdl()
.args(["check", "--no-cache"])
.arg(&path)
.status()
.expect("run rumdl check");
assert_eq!(
status.code(),
Some(TOOL_ERROR),
"an unreadable (invalid UTF-8) file must exit with the tool-error code, not report success"
);
}
#[cfg(unix)]
#[test]
fn unreadable_file_is_a_tool_error() {
use std::os::unix::fs::PermissionsExt;
let dir = tempdir().unwrap();
let path = dir.path().join("noperm.md");
std::fs::write(&path, "# ok\n").unwrap();
std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o000)).unwrap();
let status = rumdl()
.args(["check", "--no-cache"])
.arg(&path)
.status()
.expect("run rumdl check");
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o644));
assert_eq!(
status.code(),
Some(TOOL_ERROR),
"a file that cannot be read must exit with the tool-error code"
);
}
#[test]
fn nonexistent_target_is_a_tool_error() {
let dir = tempdir().unwrap();
let missing = dir.path().join("does-not-exist-xyz");
let status = rumdl()
.args(["check", "--no-cache"])
.arg(&missing)
.status()
.expect("run rumdl check");
assert_eq!(
status.code(),
Some(TOOL_ERROR),
"a nonexistent target path must exit with the tool-error code, not the violations code"
);
}
#[test]
fn deny_config_warnings_flags_unknown_rule_in_config_file() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("clean.md"), "# Title\n\nText.\n").unwrap();
std::fs::write(dir.path().join(".rumdl.toml"), "[global]\nenable = [\"MD999\"]\n").unwrap();
let status = rumdl()
.args(["check", "--no-cache", "--deny-config-warnings", "clean.md"])
.current_dir(dir.path())
.status()
.expect("run rumdl check");
assert_eq!(
status.code(),
Some(TOOL_ERROR),
"an unknown rule in the config file must exit 2 under --deny-config-warnings"
);
}
#[test]
fn config_warnings_are_non_fatal_by_default() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("clean.md"), "# Title\n\nText.\n").unwrap();
std::fs::write(dir.path().join(".rumdl.toml"), "[global]\nenable = [\"MD999\"]\n").unwrap();
let status = rumdl()
.args(["check", "--no-cache", "clean.md"])
.current_dir(dir.path())
.status()
.expect("run rumdl check");
assert_eq!(
status.code(),
Some(0),
"config warnings must not affect the exit code by default"
);
}
#[test]
fn deny_config_warnings_flags_unknown_rule_in_cli_flag() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("clean.md"), "# Title\n\nText.\n").unwrap();
let status = rumdl()
.args([
"check",
"--no-cache",
"--deny-config-warnings",
"--disable",
"MD9999",
"clean.md",
])
.current_dir(dir.path())
.status()
.expect("run rumdl check");
assert_eq!(status.code(), Some(TOOL_ERROR));
}
#[test]
fn deny_config_warnings_clean_config_exits_zero() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("clean.md"), "# Title\n\nText.\n").unwrap();
let status = rumdl()
.args(["check", "--no-cache", "--deny-config-warnings", "clean.md"])
.current_dir(dir.path())
.status()
.expect("run rumdl check");
assert_eq!(status.code(), Some(0), "no config problem means the flag has no effect");
}
#[test]
fn deny_config_warnings_flags_unknown_rule_in_inline_comment() {
let dir = tempdir().unwrap();
std::fs::write(
dir.path().join("inline.md"),
"# Title\n\nSome text.<!-- rumdl-disable-line asdf -->\n",
)
.unwrap();
let status = rumdl()
.args(["check", "--no-cache", "--deny-config-warnings", "inline.md"])
.current_dir(dir.path())
.status()
.expect("run rumdl check");
assert_eq!(
status.code(),
Some(TOOL_ERROR),
"an unknown rule in an inline disable comment must exit 2 under the flag"
);
}
#[test]
fn inline_config_warning_is_non_fatal_by_default() {
let dir = tempdir().unwrap();
std::fs::write(
dir.path().join("inline.md"),
"# Title\n\nSome text.<!-- rumdl-disable-line asdf -->\n",
)
.unwrap();
let status = rumdl()
.args(["check", "--no-cache", "inline.md"])
.current_dir(dir.path())
.status()
.expect("run rumdl check");
assert_eq!(
status.code(),
Some(0),
"an inline config warning must not affect the exit code by default"
);
}
#[test]
fn deny_config_warnings_stdin_inline_comment() {
use std::io::Write;
let mut child = rumdl()
.args(["check", "--no-cache", "--stdin", "--deny-config-warnings"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn rumdl");
child
.stdin
.take()
.unwrap()
.write_all(b"# Title\n\nText.<!-- rumdl-disable-line asdf -->\n")
.unwrap();
let status = child.wait().expect("wait rumdl");
assert_eq!(status.code(), Some(TOOL_ERROR));
}
#[test]
fn deny_config_warnings_stdin_fmt_inline_comment() {
use std::io::Write;
let mut child = rumdl()
.args(["fmt", "--no-cache", "--stdin", "--deny-config-warnings"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn rumdl");
child
.stdin
.take()
.unwrap()
.write_all(b"# Title\n\nText.<!-- rumdl-disable-line asdf -->\n")
.unwrap();
let status = child.wait().expect("wait rumdl");
assert_eq!(
status.code(),
Some(TOOL_ERROR),
"fmt --stdin must honor --deny-config-warnings despite its separate exit path"
);
}
#[test]
fn stdin_inline_config_warning_non_fatal_by_default() {
use std::io::Write;
let mut child = rumdl()
.args(["check", "--no-cache", "--stdin"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.spawn()
.expect("spawn rumdl");
child
.stdin
.take()
.unwrap()
.write_all(b"# Title\n\nText.<!-- rumdl-disable-line asdf -->\n")
.unwrap();
let status = child.wait().expect("wait rumdl");
assert_eq!(
status.code(),
Some(0),
"stdin inline config warning must not affect exit code by default"
);
}
#[test]
fn deny_config_warnings_silent_still_exits_two() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("clean.md"), "# Title\n\nText.\n").unwrap();
std::fs::write(dir.path().join(".rumdl.toml"), "[global]\nenable = [\"MD999\"]\n").unwrap();
let output = rumdl()
.args(["check", "--no-cache", "--deny-config-warnings", "--silent", "clean.md"])
.current_dir(dir.path())
.output()
.expect("run rumdl check");
assert_eq!(output.status.code(), Some(TOOL_ERROR));
assert!(
String::from_utf8_lossy(&output.stderr).is_empty(),
"--silent must suppress the printed warning even while the flag makes it fatal. stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn deny_config_warnings_take_precedence_over_violations() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("dirty.md"), "no heading here\n").unwrap();
std::fs::write(dir.path().join(".rumdl.toml"), "[global]\nenable = [\"MD999\"]\n").unwrap();
let status = rumdl()
.args(["check", "--no-cache", "--deny-config-warnings", "dirty.md"])
.current_dir(dir.path())
.status()
.expect("run rumdl check");
assert_eq!(
status.code(),
Some(TOOL_ERROR),
"a config problem must exit 2 even when Markdown violations (exit 1) are also present"
);
}
#[test]
fn violations_without_config_problem_exit_one() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("dirty.md"), "no heading here\n").unwrap();
let status = rumdl()
.args(["check", "--no-cache", "--deny-config-warnings", "dirty.md"])
.current_dir(dir.path())
.status()
.expect("run rumdl check");
assert_eq!(
status.code(),
Some(1),
"a plain Markdown violation exits 1; the flag only changes config-problem exits"
);
}
#[test]
fn deny_config_warnings_applies_to_fmt() {
let dir = tempdir().unwrap();
std::fs::write(dir.path().join("clean.md"), "# Title\n\nText.\n").unwrap();
std::fs::write(dir.path().join(".rumdl.toml"), "[global]\nenable = [\"MD999\"]\n").unwrap();
let status = rumdl()
.args(["fmt", "--no-cache", "--deny-config-warnings", "clean.md"])
.current_dir(dir.path())
.status()
.expect("run rumdl fmt");
assert_eq!(status.code(), Some(TOOL_ERROR));
}