use std::fs;
use std::process::Command;
use tempfile::tempdir;
fn rumdl_bin() -> &'static str {
env!("CARGO_BIN_EXE_rumdl")
}
const LONG_LINE: &str =
"# Heading\n\nThis line is intentionally longer than twenty characters but shorter than two hundred.\n";
#[test]
fn inline_override_lowers_md013_line_length() {
let dir = tempdir().unwrap();
let file = dir.path().join("a.md");
fs::write(&file, LONG_LINE).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", "MD013.line_length=20", "a.md"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("MD013") || stderr.contains("MD013"),
"expected MD013 violation when line_length=20 via --config, got:\nstdout: {stdout}\nstderr: {stderr}"
);
}
#[test]
fn inline_override_raises_md013_line_length() {
let dir = tempdir().unwrap();
let file = dir.path().join("a.md");
fs::write(&file, LONG_LINE).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", "MD013.line_length=200", "a.md"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
output.status.success(),
"rumdl should exit 0 (no violations) with line_length=200 override, got code {:?}\nstdout: {stdout}\nstderr: {stderr}",
output.status.code()
);
assert!(
!stdout.contains("MD013"),
"did not expect MD013 violation when line_length=200 via --config, got:\n{stdout}"
);
}
#[test]
fn inline_override_beats_config_file() {
let dir = tempdir().unwrap();
fs::write(dir.path().join(".rumdl.toml"), "[MD013]\nline-length = 200\n").unwrap();
fs::write(dir.path().join("a.md"), LONG_LINE).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--config", "MD013.line_length=20", "a.md"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("MD013"),
"CLI override line_length=20 should beat .rumdl.toml line-length=200, got:\n{stdout}"
);
}
#[test]
fn inline_override_combines_with_config_file_path() {
let dir = tempdir().unwrap();
let cfg = dir.path().join("custom.toml");
fs::write(&cfg, "[MD013]\nline-length = 200\n").unwrap();
fs::write(dir.path().join("a.md"), LONG_LINE).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args([
"check",
"--config",
cfg.to_str().unwrap(),
"--config",
"MD013.line_length=20",
"a.md",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("MD013"),
"inline override should win over the file path passed in the same --config series, got:\n{stdout}"
);
}
#[test]
fn multiple_inline_overrides_apply() {
let dir = tempdir().unwrap();
let content = "Not a heading and this line is moderately long for the test\n";
fs::write(dir.path().join("a.md"), content).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args([
"check",
"--no-config",
"--config",
"MD013.line_length=10",
"--config",
"MD041.level=1",
"a.md",
])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("MD013"),
"MD013 should fire after override, got:\n{stdout}"
);
assert!(
stdout.contains("MD041"),
"MD041 should fire after override, got:\n{stdout}"
);
}
#[test]
fn inline_override_enables_md013_reflow() {
let dir = tempdir().unwrap();
let file = dir.path().join("a.md");
let content =
"This is a very long line that definitely exceeds forty characters and should be reflowed when reflow is on.\n";
fs::write(&file, content).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args([
"check",
"--fix",
"--no-config",
"--config",
"MD013.line_length=40",
"--config",
"MD013.reflow=true",
"a.md",
])
.output()
.unwrap();
let exit_code = output.status.code().unwrap_or(-1);
assert!(
exit_code == 0 || exit_code == 1,
"expected exit 0 or 1, got {exit_code}\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let fixed = fs::read_to_string(&file).unwrap();
let max = fixed.lines().map(str::len).max().unwrap_or(0);
assert!(
max <= 60,
"reflow should have wrapped lines (max line was {max} chars):\n{fixed}"
);
let original_max = content.lines().map(str::len).max().unwrap_or(0);
assert!(
max < original_max,
"post-fix max line ({max}) should be shorter than original ({original_max})"
);
}
#[test]
fn invalid_inline_override_errors_clearly() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("a.md"), "# H\n").unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", "this is not valid toml = =", "a.md"])
.output()
.unwrap();
assert!(
!output.status.success(),
"expected non-zero exit for invalid --config value"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.to_lowercase().contains("toml")
|| stderr.to_lowercase().contains("must either be a path")
|| stderr.to_lowercase().contains("key = value")
|| stderr.to_lowercase().contains("key=value"),
"stderr should explain the --config value is neither a path nor inline TOML, got:\n{stderr}"
);
}
#[test]
fn inline_override_accepts_lowercase_rule_id() {
let dir = tempdir().unwrap();
let file = dir.path().join("a.md");
fs::write(&file, LONG_LINE).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", "md013.line_length=20", "a.md"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("MD013"),
"lowercase md013 should normalize to MD013, got:\n{stdout}"
);
}
#[test]
fn two_file_paths_error() {
let dir = tempdir().unwrap();
let cfg1 = dir.path().join("a.toml");
let cfg2 = dir.path().join("b.toml");
fs::write(&cfg1, "").unwrap();
fs::write(&cfg2, "").unwrap();
fs::write(dir.path().join("x.md"), "# H\n").unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args([
"check",
"--config",
cfg1.to_str().unwrap(),
"--config",
cfg2.to_str().unwrap(),
"x.md",
])
.output()
.unwrap();
assert!(
!output.status.success(),
"expected non-zero exit when two file paths are passed via --config"
);
}
#[test]
fn inline_override_sets_global_line_length() {
let dir = tempdir().unwrap();
let file = dir.path().join("a.md");
fs::write(&file, LONG_LINE).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", "line-length=20", "a.md"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("MD013") || stderr.contains("MD013"),
"global line-length=20 should propagate to MD013 and trigger violation, got:\nstdout: {stdout}\nstderr: {stderr}"
);
}
#[test]
fn inline_override_explicit_global_table() {
let dir = tempdir().unwrap();
let file = dir.path().join("a.md");
fs::write(&file, LONG_LINE).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", "global.line-length=20", "a.md"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stdout.contains("MD013") || stderr.contains("MD013"),
"[global] line-length=20 should trigger MD013, got:\nstdout: {stdout}\nstderr: {stderr}"
);
}
#[test]
fn inline_override_global_disable() {
let dir = tempdir().unwrap();
let file = dir.path().join("a.md");
let content = format!("# H\n\n{}\n", vec!["word"; 30].join(" "));
fs::write(&file, content).unwrap();
let baseline = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "a.md"])
.output()
.unwrap();
assert!(
String::from_utf8_lossy(&baseline.stdout).contains("MD013"),
"test premise broken: MD013 should fire on long sentence by default"
);
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", r#"disable=["MD013"]"#, "a.md"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
output.status.success(),
"rumdl should exit 0 when MD013 disabled via --config disable=[\"MD013\"], got code {:?}\nstdout: {stdout}",
output.status.code()
);
assert!(
!stdout.contains("MD013"),
"MD013 should be suppressed by global disable=[\"MD013\"], got:\n{stdout}"
);
}
#[test]
fn inline_override_string_value() {
let dir = tempdir().unwrap();
let file = dir.path().join("a.md");
let content = "# ATX\n\nSetext\n======\n\nMore text.\n";
fs::write(&file, content).unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", r#"MD003.style="atx""#, "a.md"])
.output()
.unwrap();
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("MD003"),
"MD003 with style=\"atx\" should flag setext heading, got:\n{stdout}"
);
}
#[test]
fn inline_override_unknown_rule_warns() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("a.md"), "# H\n").unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", "MD9999.foo=1", "a.md"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.to_lowercase().contains("unknown rule") && stderr.contains("MD9999"),
"expected 'Unknown rule' warning for MD9999, got:\nstderr: {stderr}"
);
}
#[test]
fn inline_override_unknown_option_warns() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("a.md"), "# H\n").unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", "MD013.no_such_option=1", "a.md"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.to_lowercase().contains("unknown option") && stderr.contains("MD013"),
"expected 'Unknown option for rule MD013' warning, got:\nstderr: {stderr}"
);
}
#[test]
fn inline_override_unknown_global_warns() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("a.md"), "# H\n").unwrap();
let output = Command::new(rumdl_bin())
.current_dir(dir.path())
.args(["check", "--no-config", "--config", "totally_bogus_key=1", "a.md"])
.output()
.unwrap();
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.to_lowercase().contains("unknown global option") && stderr.contains("totally_bogus_key"),
"expected 'Unknown global option' warning for top-level key, got:\nstderr: {stderr}"
);
}