use assert_cmd::cargo::cargo_bin_cmd;
use std::fs;
use std::path::PathBuf;
use tempfile::tempdir;
fn create_dummy_config(dir: &tempfile::TempDir) -> PathBuf {
let config_path = dir.path().join("dummy_config.toml");
fs::write(&config_path, "").unwrap();
config_path
}
#[test]
fn test_cli_with_config_and_rules() {
let temp_dir = tempdir().unwrap();
let _config_path = temp_dir.path().join("rumdl.toml");
let config_content = r#"
[general]
line_length = 150
[rules]
disabled = ["MD033"]
"#;
fs::write(&_config_path, config_content).unwrap();
let markdown_path = temp_dir.path().join("test.md");
let markdown_content = r#"# Test Document
## Heading with no blank line below
Some content.
* List item with incorrect indentation
* Nested item
* Deeply nested
<div>HTML content that should be ignored due to config</div>
This is a line that would normally exceed the default line length limit, but we've set it to 150 characters.
<!-- Some comment -->
# Indented heading (MD023 violation)
"#;
fs::write(&markdown_path, markdown_content).unwrap();
let mut cmd = cargo_bin_cmd!("rumdl");
let assert = cmd
.arg("check")
.arg(&markdown_path)
.arg("--config")
.arg(&_config_path)
.assert();
let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert.code(1);
assert!(output.contains("MD005"), "Expected MD005 in output: {output}");
assert!(output.contains("MD023"));
}
#[test]
fn test_multiple_files_with_fix() {
let temp_dir = tempdir().unwrap();
let file1_path = temp_dir.path().join("file1.md");
let file1_content = r#"# File 1
## Indented heading
Some content with trailing spaces
And more content.
"#;
fs::write(&file1_path, file1_content).unwrap();
let file2_path = temp_dir.path().join("file2.md");
let file2_content = r#"# File 2
* List item 1
* List item 2
<div>Some HTML</div>
No blank line at end"#;
fs::write(&file2_path, file2_content).unwrap();
let mut cmd = cargo_bin_cmd!("rumdl");
let assert = cmd
.arg("check")
.arg(&file1_path)
.arg(&file2_path)
.arg("--fix") .assert();
let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert!(output.contains("Fixed"));
assert.code(1);
let fixed_file1 = fs::read_to_string(&file1_path).unwrap();
let fixed_file2 = fs::read_to_string(&file2_path).unwrap();
assert!(!fixed_file1.contains(" ## Indented heading"));
assert!(fixed_file1.contains("## Indented heading"));
assert!(fixed_file2.contains("No blank line at end\n"));
}
#[test]
fn test_init_load_apply_config() {
let temp_dir = tempdir().unwrap();
let _config_path = temp_dir.path().join(".rumdl.toml");
let mut init_cmd = cargo_bin_cmd!("rumdl");
init_cmd.arg("init").current_dir(temp_dir.path()).assert().success();
assert!(_config_path.exists());
let config_content = fs::read_to_string(&_config_path).unwrap();
assert!(config_content.contains("line-length"));
assert!(config_content.contains("rules"));
let markdown_path = temp_dir.path().join("test.md");
let long_line = "A ".repeat(100);
fs::write(&markdown_path, format!("# Test\n\n{long_line}\n")).unwrap();
let mut cmd = cargo_bin_cmd!("rumdl");
let assert = cmd
.arg("check")
.arg(&markdown_path)
.current_dir(temp_dir.path())
.assert();
assert.code(1); }
#[test]
fn test_rules_interaction() {
let temp_dir = tempdir().unwrap();
let markdown_path = temp_dir.path().join("complex.md");
let _config_path = create_dummy_config(&temp_dir);
let markdown_content = r#"<!-- Test document -->
# Heading with no blank line below
## Subheading also with no space
### Indented heading
<div>
<h4>HTML heading that should be a Markdown heading</h4>
* List item 1
* List item 2
## HTML subheading that creates invalid nesting
</div>
* First level
* Second level
* Third level
* Fourth level with **emphasis as heading**
* Normal item
* Another list
* With item
*Invalid item (missing space)
Empty link: []()
* List item
with content that isn't properly indented
## Heading Without A Proper [Link](#nonexistent)
Heading with trailing punctuation:
-----------------------------------
## Heading with trailing punctuation!
Link to [non-existent heading](#nowhere)
"#;
fs::write(&markdown_path, markdown_content).unwrap();
let mut cmd = cargo_bin_cmd!("rumdl");
let assert = cmd
.arg("check")
.arg(&markdown_path)
.arg("--no-config") .assert();
let output = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
assert.code(1);
assert!(output.contains("MD022"));
assert!(output.contains("MD023"));
assert!(output.contains("MD033")); assert!(output.contains("MD042"));
assert!(output.contains("MD051"));
let mut fix_cmd = cargo_bin_cmd!("rumdl");
let fix_assert = fix_cmd
.arg("check")
.arg(&markdown_path)
.arg("--fix")
.arg("--no-config") .assert();
let fix_output = String::from_utf8(fix_assert.get_output().stdout.clone()).unwrap();
assert!(fix_output.contains("Fixed"));
fix_assert.code(1);
let fixed_content = fs::read_to_string(&markdown_path).unwrap();
assert!(!fixed_content.contains("# Heading with no blank line below\n##"));
assert!(!fixed_content.contains(" ### Indented"));
let mut recheck_cmd = cargo_bin_cmd!("rumdl");
let recheck = recheck_cmd.arg("check").arg(&markdown_path).assert();
let recheck_output = String::from_utf8(recheck.get_output().stdout.clone()).unwrap();
assert!(
recheck_output.split('\n').filter(|line| line.contains("MD")).count()
< output.split('\n').filter(|line| line.contains("MD")).count()
);
}
#[test]
fn test_cli_options() {
let temp_dir = tempdir().unwrap();
let _config_path = create_dummy_config(&temp_dir);
let markdown_path = temp_dir.path().join("format_test.md");
let markdown_content = r#"# Test Document
## No blank line
<div>Some HTML</div>
* List item
1.Ordered item without space
"#;
fs::write(&markdown_path, markdown_content).unwrap();
let mut cmd = cargo_bin_cmd!("rumdl");
let assert = cmd
.arg("check")
.arg(&markdown_path)
.arg("--no-config") .arg("--no-cache") .assert();
let default_output = String::from_utf8(assert.get_output().stdout.clone()).unwrap();
let stderr = String::from_utf8(assert.get_output().stderr.clone()).unwrap();
let code = assert.get_output().status.code().unwrap_or(-1);
assert!(code == 0 || code == 1, "Unexpected exit code: {code}");
assert!(default_output.contains("MD022"));
assert!(default_output.contains("MD033"));
assert!(default_output.contains("MD030"));
if !stderr.is_empty() {
assert!(stderr.contains("Deprecation warning"));
}
let mut disabled_cmd = cargo_bin_cmd!("rumdl");
let disabled_assert = disabled_cmd
.arg("check")
.arg(&markdown_path)
.arg("--disable")
.arg("MD022,MD033,MD030") .arg("--no-config") .arg("--no-cache") .assert();
let disabled_output = String::from_utf8(disabled_assert.get_output().stdout.clone()).unwrap();
let disabled_code = disabled_assert.get_output().status.code().unwrap_or(-1);
assert!(
disabled_code == 0,
"Expected exit code 0 (no issues found), got {disabled_code}. Output: {disabled_output}"
);
assert!(!disabled_output.contains("MD022"));
assert!(!disabled_output.contains("MD033"));
assert!(!disabled_output.contains("MD030"));
let mut enabled_cmd = cargo_bin_cmd!("rumdl");
let enabled_assert = enabled_cmd
.arg("check")
.arg(&markdown_path)
.arg("--enable")
.arg("MD030") .arg("--no-config") .arg("--no-cache") .assert();
let enabled_output = String::from_utf8(enabled_assert.get_output().stdout.clone()).unwrap();
enabled_assert.code(1); assert!(!enabled_output.contains("MD022"));
assert!(!enabled_output.contains("MD033"));
assert!(enabled_output.contains("MD030"));
let options_test_path = temp_dir.path().join("options_test.md");
fs::write(&options_test_path, "# Test\n\n<div>HTML</div>\n").unwrap();
let mut default_cmd_options = cargo_bin_cmd!("rumdl");
let default_assert_options = default_cmd_options
.arg("check")
.arg(&options_test_path)
.arg("--no-config") .arg("--no-cache") .assert();
let default_output_options = String::from_utf8(default_assert_options.get_output().stdout.clone()).unwrap();
assert!(default_output_options.contains("MD033"));
assert!(!default_output_options.contains("MD047")); default_assert_options.code(1);
}
#[test]
fn test_specific_rule_triggers() {
let temp_dir = tempdir().unwrap();
let md022_path = temp_dir.path().join("md022_test.md");
let md022_content = r#"# First heading
## Second heading without blank line above
Text right after heading
## Third heading
"#;
fs::write(&md022_path, md022_content).unwrap();
let mut md022_cmd = cargo_bin_cmd!("rumdl");
let md022_assert = md022_cmd.arg("check").arg(&md022_path).arg("--no-config").assert();
let md022_output = String::from_utf8(md022_assert.get_output().stdout.clone()).unwrap();
assert!(
md022_output.contains("MD022"),
"MD022 should trigger for headings without blank lines"
);
let md030_path = temp_dir.path().join("md030_test.md");
let md030_content = r#"# List spacing test
* Too many spaces
* Way too many spaces
* Normal spacing
"#;
fs::write(&md030_path, md030_content).unwrap();
let mut md030_cmd = cargo_bin_cmd!("rumdl");
let md030_assert = md030_cmd.arg("check").arg(&md030_path).arg("--no-config").assert();
let md030_output = String::from_utf8(md030_assert.get_output().stdout.clone()).unwrap();
assert!(
md030_output.contains("MD030"),
"MD030 should trigger for incorrect spacing after list markers"
);
let md032_path = temp_dir.path().join("md032_test.md");
let md032_content = r#"# List blank lines test
Some text immediately followed by list:
* First item
* Second item
More text immediately after list.
Another paragraph.
1. Ordered list without blank line above
2. Second item
"#;
fs::write(&md032_path, md032_content).unwrap();
let mut md032_cmd = cargo_bin_cmd!("rumdl");
let md032_assert = md032_cmd.arg("check").arg(&md032_path).arg("--no-config").assert();
let md032_output = String::from_utf8(md032_assert.get_output().stdout.clone()).unwrap();
assert!(
md032_output.contains("MD032"),
"MD032 should trigger for lists without blank lines around them"
);
let md033_path = temp_dir.path().join("md033_test.md");
let md033_content = r#"# HTML test
<div>This is inline HTML</div>
<span style="color: red">Styled text</span>
<script>alert('JS')</script>
"#;
fs::write(&md033_path, md033_content).unwrap();
let mut md033_cmd = cargo_bin_cmd!("rumdl");
let md033_assert = md033_cmd.arg("check").arg(&md033_path).arg("--no-config").assert();
let md033_output = String::from_utf8(md033_assert.get_output().stdout.clone()).unwrap();
assert!(md033_output.contains("MD033"), "MD033 should trigger for inline HTML");
let invalid_list_path = temp_dir.path().join("invalid_list_test.md");
let invalid_list_content = r#"# Invalid list test
*Bad item (no space after asterisk)
* Good item
This is text with *emphasis* not a list.
"#;
fs::write(&invalid_list_path, invalid_list_content).unwrap();
let mut invalid_cmd = cargo_bin_cmd!("rumdl");
let invalid_assert = invalid_cmd
.arg("check")
.arg(&invalid_list_path)
.arg("--no-config")
.arg("--no-cache")
.arg("--enable")
.arg("MD030") .assert();
let invalid_output = String::from_utf8(invalid_assert.get_output().stdout.clone()).unwrap();
assert!(
!invalid_output.contains("MD030"),
"MD030 should not trigger on invalid list syntax like '*Bad item'"
);
}