use std::fs;
use std::process::Command;
use tempfile::tempdir;
fn run_rumdl(dir: &std::path::Path, args: &[&str]) -> (bool, String, String) {
let rumdl_exe = env!("CARGO_BIN_EXE_rumdl");
let output = Command::new(rumdl_exe)
.current_dir(dir)
.args(args)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout).to_string();
let stderr = String::from_utf8_lossy(&output.stderr).to_string();
(output.status.success(), stdout, stderr)
}
#[test]
fn test_flavor_cli_option_recognized() {
let temp_dir = tempdir().unwrap();
let md_path = temp_dir.path().join("test.md");
fs::write(&md_path, "# Test\n\nSome content.\n").unwrap();
let (success, stdout, stderr) = run_rumdl(temp_dir.path(), &["check", "--flavor", "mkdocs", "test.md"]);
assert!(success, "Command should succeed. stderr: {stderr}, stdout: {stdout}");
}
#[test]
fn test_flavor_cli_all_variants() {
let temp_dir = tempdir().unwrap();
let md_path = temp_dir.path().join("test.md");
fs::write(&md_path, "# Test\n\nSome content.\n").unwrap();
for flavor in [
"standard",
"gfm",
"github",
"commonmark",
"mkdocs",
"mdx",
"quarto",
"qmd",
"rmd",
"rmarkdown",
"obsidian",
"kramdown",
"jekyll",
] {
let (success, stdout, stderr) = run_rumdl(temp_dir.path(), &["check", "--flavor", flavor, "test.md"]);
assert!(
success,
"Command should succeed for flavor '{flavor}'. stderr: {stderr}, stdout: {stdout}"
);
}
}
#[test]
fn test_flavor_cli_invalid_value() {
let temp_dir = tempdir().unwrap();
let md_path = temp_dir.path().join("test.md");
fs::write(&md_path, "# Test\n\nSome content.\n").unwrap();
let (success, _stdout, stderr) = run_rumdl(temp_dir.path(), &["check", "--flavor", "invalid_flavor", "test.md"]);
assert!(!success, "Command should fail for invalid flavor");
assert!(
stderr.contains("invalid_flavor") || stderr.contains("possible values"),
"Error should mention invalid value. stderr: {stderr}"
);
}
#[test]
fn test_flavor_cli_overrides_config() {
let temp_dir = tempdir().unwrap();
let config_content = r#"
[global]
flavor = "standard"
"#;
fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap();
let md_content = r#"# Test
!!! note "MkDocs Admonition"
This should trigger MD022 in standard mode but not in mkdocs mode.
"#;
fs::write(temp_dir.path().join("test.md"), md_content).unwrap();
let (_success_std, stdout_std, _) = run_rumdl(temp_dir.path(), &["check", "test.md"]);
let (_success_mkdocs, stdout_mkdocs, _stderr_mkdocs) =
run_rumdl(temp_dir.path(), &["check", "--flavor", "mkdocs", "test.md"]);
println!("Standard mode: {stdout_std}");
println!("MkDocs mode: {stdout_mkdocs}");
}
#[test]
fn test_flavor_cli_with_output_format() {
let temp_dir = tempdir().unwrap();
let md_path = temp_dir.path().join("test.md");
fs::write(&md_path, "# Test\n\nSome content.\n").unwrap();
let (success, stdout, stderr) = run_rumdl(
temp_dir.path(),
&["check", "--flavor", "mkdocs", "--output-format", "json", "test.md"],
);
assert!(success, "Command should succeed with both options. stderr: {stderr}");
assert!(
stdout.trim().is_empty() || stdout.starts_with('[') || stdout.starts_with('{'),
"Output should be valid JSON. stdout: {stdout}"
);
}
#[test]
fn test_flavor_cli_with_enable_disable() {
let temp_dir = tempdir().unwrap();
let md_path = temp_dir.path().join("test.md");
fs::write(&md_path, "# Test\n\nSome content.\n").unwrap();
let (success, _stdout, stderr) = run_rumdl(
temp_dir.path(),
&["check", "--flavor", "mkdocs", "--enable", "MD001,MD003", "test.md"],
);
assert!(
success,
"Command should succeed with --flavor and --enable. stderr: {stderr}"
);
let (success, _stdout, stderr) = run_rumdl(
temp_dir.path(),
&["check", "--flavor", "quarto", "--disable", "MD013", "test.md"],
);
assert!(
success,
"Command should succeed with --flavor and --disable. stderr: {stderr}"
);
}
#[test]
fn test_flavor_mdx_jsx_support() {
let temp_dir = tempdir().unwrap();
let mdx_content = r#"# MDX Test
<CustomComponent prop="value">
Some content inside a custom component.
</CustomComponent>
Regular paragraph.
"#;
fs::write(temp_dir.path().join("test.mdx"), mdx_content).unwrap();
let (_success, _stdout, _stderr) = run_rumdl(temp_dir.path(), &["check", "--flavor", "mdx", "test.mdx"]);
}
#[test]
fn test_flavor_quarto_support() {
let temp_dir = tempdir().unwrap();
let qmd_content = r#"---
title: "Quarto Test"
---
# Quarto Document
:::{.callout-note}
This is a Quarto callout note.
:::
Regular paragraph.
"#;
fs::write(temp_dir.path().join("test.qmd"), qmd_content).unwrap();
let (_success, _stdout, _stderr) = run_rumdl(temp_dir.path(), &["check", "--flavor", "quarto", "test.qmd"]);
}
#[test]
fn test_obsidian_flavor_md018_tags() {
let temp_dir = tempdir().unwrap();
let md_content = r#"# Real Heading
#todo this is an Obsidian tag
#project/active nested tag
##Introduction
#123
"#;
fs::write(temp_dir.path().join("test.md"), md_content).unwrap();
let (success_std, stdout_std, _stderr_std) =
run_rumdl(temp_dir.path(), &["check", "--flavor", "standard", "test.md"]);
assert!(!success_std, "Standard flavor should find issues");
let std_md018_count = stdout_std.matches("MD018").count();
assert!(
std_md018_count >= 4,
"Standard flavor should flag at least 4 MD018 issues (#todo, #project/active, ##Introduction, #123). Found {std_md018_count}. stdout: {stdout_std}"
);
let (success_obs, stdout_obs, _stderr_obs) =
run_rumdl(temp_dir.path(), &["check", "--flavor", "obsidian", "test.md"]);
assert!(!success_obs, "Obsidian flavor should still find some issues");
let obs_md018_count = stdout_obs.matches("MD018").count();
assert_eq!(
obs_md018_count, 2,
"Obsidian flavor should flag exactly 2 MD018 issues (##Introduction, #123). Found {obs_md018_count}. stdout: {stdout_obs}"
);
assert!(
!stdout_obs.contains("test.md:3:"),
"#todo (line 3) should NOT be flagged in Obsidian flavor. stdout: {stdout_obs}"
);
assert!(
!stdout_obs.contains("test.md:5:"),
"#project/active (line 5) should NOT be flagged in Obsidian flavor. stdout: {stdout_obs}"
);
}
#[test]
fn test_obsidian_flavor_config_file() {
let temp_dir = tempdir().unwrap();
let config_content = r#"
[global]
flavor = "obsidian"
"#;
fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap();
let md_content = "#todo this is a tag\n";
fs::write(temp_dir.path().join("test.md"), md_content).unwrap();
let (success, stdout, stderr) = run_rumdl(temp_dir.path(), &["check", "test.md"]);
assert!(
success,
"Obsidian flavor from config should skip #todo tag. stdout: {stdout}, stderr: {stderr}"
);
assert!(
!stdout.contains("MD018"),
"#todo should NOT be flagged when flavor=obsidian in config. stdout: {stdout}"
);
}
#[test]
fn test_obsidian_flavor_fix_preserves_tags() {
let temp_dir = tempdir().unwrap();
let md_content = "#todo tag\n\n##Introduction\n";
let md_path = temp_dir.path().join("test.md");
fs::write(&md_path, md_content).unwrap();
let (success, _stdout, stderr) = run_rumdl(temp_dir.path(), &["check", "--fix", "--flavor", "obsidian", "test.md"]);
assert!(success, "Fix command should succeed. stderr: {stderr}");
let fixed_content = fs::read_to_string(&md_path).expect("Should read fixed file");
assert!(
fixed_content.contains("#todo tag"),
"#todo should be preserved in Obsidian flavor. Fixed content: {fixed_content}"
);
assert!(
fixed_content.contains("## Introduction"),
"##Introduction should be fixed to '## Introduction'. Fixed content: {fixed_content}"
);
}
#[test]
fn test_md018_magiclink_config() {
let temp_dir = tempdir().unwrap();
let config_content = r#"
[MD018]
magiclink = true
"#;
fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap();
let md_content = r#"# Real Heading
#10 discusses the issue
#37 is another reference
#Summary
"#;
fs::write(temp_dir.path().join("test.md"), md_content).unwrap();
let (success, stdout, _stderr) = run_rumdl(temp_dir.path(), &["check", "test.md"]);
assert!(!success, "Should find issues (at least #Summary)");
let md018_count = stdout.matches("MD018").count();
assert_eq!(
md018_count, 1,
"With magiclink=true, should flag exactly 1 MD018 issue (#Summary). Found {md018_count}. stdout: {stdout}"
);
assert!(
!stdout.contains("test.md:3:"),
"#10 (line 3) should NOT be flagged with magiclink=true. stdout: {stdout}"
);
assert!(
!stdout.contains("test.md:5:"),
"#37 (line 5) should NOT be flagged with magiclink=true. stdout: {stdout}"
);
}
#[test]
fn test_md018_without_magiclink_config() {
let temp_dir = tempdir().unwrap();
let md_content = r#"# Real Heading
#10 discusses the issue
#Summary
"#;
fs::write(temp_dir.path().join("test.md"), md_content).unwrap();
let (success, stdout, _stderr) = run_rumdl(temp_dir.path(), &["check", "test.md"]);
assert!(!success, "Should find issues");
let md018_count = stdout.matches("MD018").count();
assert_eq!(
md018_count, 2,
"Without magiclink config, should flag 2 MD018 issues (#10, #Summary). Found {md018_count}. stdout: {stdout}"
);
}
#[test]
fn test_md018_magiclink_fix_preserves_refs() {
let temp_dir = tempdir().unwrap();
let config_content = r#"
[MD018]
magiclink = true
"#;
fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap();
let md_content = "#10 is an issue\n\n#Summary\n";
let md_path = temp_dir.path().join("test.md");
fs::write(&md_path, md_content).unwrap();
let (success, _stdout, stderr) = run_rumdl(temp_dir.path(), &["check", "--fix", "test.md"]);
assert!(success, "Fix command should succeed. stderr: {stderr}");
let fixed_content = fs::read_to_string(&md_path).expect("Should read fixed file");
assert!(
fixed_content.contains("#10 is an issue"),
"#10 should be preserved with magiclink=true. Fixed content: {fixed_content}"
);
assert!(
fixed_content.contains("# Summary"),
"#Summary should be fixed to '# Summary'. Fixed content: {fixed_content}"
);
}
#[test]
fn test_md018_tags_config_standard_flavor() {
let temp_dir = tempdir().unwrap();
let config_content = r#"
[MD018]
tags = true
"#;
fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap();
let md_content = r#"# Real Heading
#todo this is a tag
#project/active nested tag
##Introduction
#123
"#;
fs::write(temp_dir.path().join("test.md"), md_content).unwrap();
let (success, stdout, _stderr) = run_rumdl(temp_dir.path(), &["check", "test.md"]);
assert!(!success, "Should find issues (##Introduction, #123)");
let md018_count = stdout.matches("MD018").count();
assert_eq!(
md018_count, 2,
"With tags=true, should flag exactly 2 MD018 issues (##Introduction, #123). Found {md018_count}. stdout: {stdout}"
);
assert!(
!stdout.contains("test.md:3:"),
"#todo (line 3) should NOT be flagged with tags=true. stdout: {stdout}"
);
assert!(
!stdout.contains("test.md:5:"),
"#project/active (line 5) should NOT be flagged with tags=true. stdout: {stdout}"
);
}
#[test]
fn test_md018_tags_config_override_obsidian() {
let temp_dir = tempdir().unwrap();
let config_content = r#"
[global]
flavor = "obsidian"
[MD018]
tags = false
"#;
fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap();
let md_content = r#"# Real Heading
#todo
#project/active
"#;
fs::write(temp_dir.path().join("test.md"), md_content).unwrap();
let (success, stdout, _stderr) = run_rumdl(temp_dir.path(), &["check", "test.md"]);
assert!(!success, "Should find issues with tags=false");
let md018_count = stdout.matches("MD018").count();
assert_eq!(
md018_count, 2,
"With tags=false in Obsidian flavor, should flag tag patterns. Found {md018_count}. stdout: {stdout}"
);
}
#[test]
fn test_md018_tags_config_fix_preserves_tags() {
let temp_dir = tempdir().unwrap();
let config_content = r#"
[MD018]
tags = true
"#;
fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap();
let md_content = "#todo\n\n#Summary\n";
let md_path = temp_dir.path().join("test.md");
fs::write(&md_path, md_content).unwrap();
let (success, _stdout, stderr) = run_rumdl(temp_dir.path(), &["check", "--fix", "test.md"]);
assert!(success, "Fix command should succeed. stderr: {stderr}");
let fixed_content = fs::read_to_string(&md_path).expect("Should read fixed file");
assert!(
fixed_content.contains("#todo"),
"#todo should be preserved with tags=true. Fixed content: {fixed_content}"
);
assert!(
fixed_content.contains("#Summary"),
"#Summary should be preserved with tags=true (matches tag pattern). Fixed content: {fixed_content}"
);
}
#[test]
fn test_per_file_flavor_fix_coordination() {
let temp_dir = tempdir().unwrap();
let config_content = r#"
[global]
enable = ["MD013"]
line-length = 80
[per-file-flavor]
"docs/**/*.md" = "mkdocs"
[MD013]
line-length = 80
reflow = true
"#;
fs::write(temp_dir.path().join(".rumdl.toml"), config_content).unwrap();
let docs_dir = temp_dir.path().join("docs");
fs::create_dir(&docs_dir).unwrap();
let md_content = r#"# Test
!!! note "Important Note"
This is a very long line inside an MkDocs admonition that exceeds the 80 character line length limit and should be reflowed by the fix command.
"#;
let md_path = docs_dir.join("test.md");
fs::write(&md_path, md_content).unwrap();
let (success, _stdout, stderr) = run_rumdl(temp_dir.path(), &["check", "--fix", "docs/test.md"]);
assert!(success, "Fix command should succeed. stderr: {stderr}");
let fixed_content = fs::read_to_string(&md_path).expect("Should read fixed file");
let original_long_line = " This is a very long line inside an MkDocs admonition that exceeds the 80 character line length limit and should be reflowed by the fix command.";
assert!(
!fixed_content.contains(original_long_line),
"Long line should have been modified by fix.\n\
This proves per-file-flavor was respected in fix coordination.\n\
If the line is unchanged, fix coordination likely used global flavor (standard) \n\
instead of per-file flavor (mkdocs), failing to recognize admonition content.\n\
Fixed content:\n{fixed_content}\n\
stderr: {stderr}"
);
}