use std::fs;
use std::process::Command;
use tempfile::tempdir;
fn rumdl_bin() -> &'static str {
env!("CARGO_BIN_EXE_rumdl")
}
fn create_warning_only_file(dir: &std::path::Path) -> std::path::PathBuf {
let path = dir.join("warning_only.md");
fs::write(
&path,
r#"# Test
- Item 1
- Nested item with wrong indent (3 spaces instead of 2 or 4)
"#,
)
.unwrap();
path
}
fn create_error_file(dir: &std::path::Path) -> std::path::PathBuf {
let path = dir.join("error.md");
fs::write(
&path,
r#"# Test
[empty link]()
"#,
)
.unwrap();
path
}
fn create_config(dir: &std::path::Path) {
fs::write(
dir.join(".rumdl.toml"),
r#"[global]
enable = ["MD007", "MD042"]
"#,
)
.unwrap();
}
#[test]
fn test_fail_on_never_with_errors_exits_zero() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let error_file = create_error_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", error_file.to_str().unwrap(), "--fail-on", "never"])
.output()
.expect("Failed to execute command");
assert!(
output.status.success(),
"--fail-on never should exit 0 even with errors\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_never_with_warnings_exits_zero() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let warning_file = create_warning_only_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", warning_file.to_str().unwrap(), "--fail-on", "never"])
.output()
.expect("Failed to execute command");
assert!(
output.status.success(),
"--fail-on never should exit 0 even with warnings\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_error_with_only_warnings_exits_zero() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let warning_file = create_warning_only_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", warning_file.to_str().unwrap(), "--fail-on", "error"])
.output()
.expect("Failed to execute command");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("MD007") || !output.status.success() || stderr.is_empty(),
"Expected MD007 warning to be reported or file to have no issues"
);
assert!(
output.status.success(),
"--fail-on error should exit 0 when only warnings exist\nstderr: {stderr}"
);
}
#[test]
fn test_fail_on_error_with_errors_exits_one() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let error_file = create_error_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", error_file.to_str().unwrap(), "--fail-on", "error"])
.output()
.expect("Failed to execute command");
assert!(
!output.status.success(),
"--fail-on error should exit 1 when errors exist\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
assert_eq!(
output.status.code(),
Some(1),
"Expected exit code 1 for errors with --fail-on error"
);
}
#[test]
fn test_fail_on_any_with_warnings_exits_one() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let warning_file = create_warning_only_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", warning_file.to_str().unwrap(), "--fail-on", "any"])
.output()
.expect("Failed to execute command");
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("MD007") {
assert!(
!output.status.success(),
"--fail-on any should exit 1 on any violation\nstderr: {stderr}"
);
}
}
#[test]
fn test_fail_on_any_with_errors_exits_one() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let error_file = create_error_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", error_file.to_str().unwrap(), "--fail-on", "any"])
.output()
.expect("Failed to execute command");
assert!(
!output.status.success(),
"--fail-on any should exit 1 on errors\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_default_is_any() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let error_file = create_error_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", error_file.to_str().unwrap()])
.output()
.expect("Failed to execute command");
assert!(
!output.status.success(),
"Default --fail-on (any) should exit 1 on violations\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_with_no_violations_exits_zero() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let clean_file = temp_dir.path().join("clean.md");
fs::write(&clean_file, "# Clean File\n\nNo issues here.\n").unwrap();
for mode in ["any", "error", "never"] {
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", clean_file.to_str().unwrap(), "--fail-on", mode])
.output()
.expect("Failed to execute command");
assert!(
output.status.success(),
"--fail-on {} should exit 0 when no violations\nstderr: {}",
mode,
String::from_utf8_lossy(&output.stderr)
);
}
}
#[test]
fn test_fail_on_with_stdin() {
let output = Command::new(rumdl_bin())
.args(["check", "--stdin", "--fail-on", "never", "--enable", "MD042"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take().unwrap().write_all(b"# Test\n\n[empty]()\n").unwrap();
child.wait_with_output()
})
.expect("Failed to execute command");
assert!(
output.status.success(),
"--fail-on never should exit 0 with stdin errors\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = Command::new(rumdl_bin())
.args(["check", "--stdin", "--fail-on", "error", "--enable", "MD042"])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take().unwrap().write_all(b"# Test\n\n[empty]()\n").unwrap();
child.wait_with_output()
})
.expect("Failed to execute command");
assert!(
!output.status.success(),
"--fail-on error should exit 1 with stdin errors\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_invalid_value_rejected() {
let output = Command::new(rumdl_bin())
.args(["check", ".", "--fail-on", "invalid"])
.output()
.expect("Failed to execute command");
assert!(!output.status.success(), "Invalid --fail-on value should be rejected");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("invalid") || stderr.contains("error"),
"Error message should indicate invalid value\nstderr: {stderr}"
);
}
fn create_mixed_file(dir: &std::path::Path) -> std::path::PathBuf {
let path = dir.join("mixed.md");
fs::write(
&path,
r#"# Test
- Item 1
- Nested item with wrong indent
[empty link]()
"#,
)
.unwrap();
path
}
fn create_unfixable_error_file(dir: &std::path::Path) -> std::path::PathBuf {
let path = dir.join("unfixable_error.md");
fs::write(
&path,
r#"# Test
[empty link]()
"#,
)
.unwrap();
path
}
#[test]
fn test_fail_on_error_with_mixed_warnings_and_errors_exits_one() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let mixed_file = create_mixed_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", mixed_file.to_str().unwrap(), "--fail-on", "error"])
.output()
.expect("Failed to execute command");
let combined_output = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
combined_output.contains("MD042"),
"Error MD042 should be reported\noutput: {combined_output}"
);
assert!(
!output.status.success(),
"--fail-on error should exit 1 when errors exist (even with warnings)\noutput: {combined_output}"
);
}
#[test]
fn test_fail_on_error_with_multiple_files_mixed_severities() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let warning_file = create_warning_only_file(temp_dir.path());
let error_file = create_error_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args([
"check",
warning_file.to_str().unwrap(),
error_file.to_str().unwrap(),
"--fail-on",
"error",
])
.output()
.expect("Failed to execute command");
assert!(
!output.status.success(),
"--fail-on error should exit 1 when any file has errors\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_error_with_multiple_warning_only_files_exits_zero() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let warning_file1 = temp_dir.path().join("warning1.md");
let warning_file2 = temp_dir.path().join("warning2.md");
fs::write(&warning_file1, "# Test\n\n- Item\n - Bad indent\n").unwrap();
fs::write(&warning_file2, "# Test\n\n- Item\n - Bad indent\n").unwrap();
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args([
"check",
warning_file1.to_str().unwrap(),
warning_file2.to_str().unwrap(),
"--fail-on",
"error",
])
.output()
.expect("Failed to execute command");
assert!(
output.status.success(),
"--fail-on error should exit 0 when all files have only warnings\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_never_still_reports_issues() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let error_file = create_error_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", error_file.to_str().unwrap(), "--fail-on", "never"])
.output()
.expect("Failed to execute command");
assert!(output.status.success(), "Should exit 0");
let combined_output = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
combined_output.contains("MD042"),
"Issues should still be reported even with --fail-on never\noutput: {combined_output}"
);
}
#[test]
fn test_fail_on_error_still_reports_warnings() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let warning_file = create_warning_only_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", warning_file.to_str().unwrap(), "--fail-on", "error"])
.output()
.expect("Failed to execute command");
assert!(output.status.success(), "Should exit 0 for warnings-only");
let combined_output = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
assert!(
combined_output.contains("MD007"),
"Warnings should still be reported even with --fail-on error\noutput: {combined_output}"
);
}
#[test]
fn test_fail_on_with_fix_mode_remaining_errors() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let error_file = create_unfixable_error_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", error_file.to_str().unwrap(), "--fix", "--fail-on", "error"])
.output()
.expect("Failed to execute command");
assert!(
!output.status.success(),
"--fix with --fail-on error should exit 1 when unfixable errors remain\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_with_fix_mode_remaining_warnings_only() {
let temp_dir = tempdir().unwrap();
fs::write(
temp_dir.path().join(".rumdl.toml"),
r#"[global]
enable = ["MD009"]
"#,
)
.unwrap();
let test_file = temp_dir.path().join("trailing.md");
fs::write(&test_file, "# Test \n\nSome text \n").unwrap();
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", test_file.to_str().unwrap(), "--fix", "--fail-on", "error"])
.output()
.expect("Failed to execute command");
assert!(
output.status.success(),
"--fix with --fail-on error should exit 0 when only warnings existed and were fixed\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_with_fmt_subcommand() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let error_file = create_unfixable_error_file(temp_dir.path());
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["fmt", error_file.to_str().unwrap(), "--fail-on", "error"])
.output()
.expect("Failed to execute command");
assert!(
output.status.success(),
"fmt should always exit 0 (format mode ignores --fail-on)\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_stdin_with_warning_only() {
let output = Command::new(rumdl_bin())
.args([
"check",
"--stdin",
"--fail-on",
"error",
"--enable",
"MD009", ])
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()
.and_then(|mut child| {
use std::io::Write;
child
.stdin
.take()
.unwrap()
.write_all(b"# Test \n\nTrailing spaces \n")
.unwrap();
child.wait_with_output()
})
.expect("Failed to execute command");
assert!(
output.status.success(),
"--fail-on error with stdin warnings should exit 0\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn test_fail_on_directory_scan_with_mixed_files() {
let temp_dir = tempdir().unwrap();
create_config(temp_dir.path());
let subdir = temp_dir.path().join("docs");
fs::create_dir(&subdir).unwrap();
fs::write(subdir.join("warning.md"), "# Test\n\n- Item\n - Bad\n").unwrap();
fs::write(subdir.join("error.md"), "# Test\n\n[empty]()\n").unwrap();
fs::write(subdir.join("clean.md"), "# Clean\n\nNo issues.\n").unwrap();
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", "docs", "--fail-on", "error"])
.output()
.expect("Failed to execute command");
assert!(
!output.status.success(),
"Directory scan with --fail-on error should exit 1 when any file has errors\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let output = Command::new(rumdl_bin())
.current_dir(temp_dir.path())
.args(["check", "docs", "--fail-on", "never"])
.output()
.expect("Failed to execute command");
assert!(
output.status.success(),
"Directory scan with --fail-on never should exit 0\nstderr: {}",
String::from_utf8_lossy(&output.stderr)
);
}