use rumdl_lib::config::Config;
use rumdl_lib::rules;
use serial_test::serial;
use std::fs;
use std::path::Path;
use tempfile::tempdir;
#[test]
fn test_per_file_ignores_integration_actual_linting() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join(".rumdl.toml");
let readme_path = temp_dir.path().join("README.md");
let docs_path = temp_dir.path().join("docs.md");
let config_content = r#"
[per-file-ignores]
"README.md" = ["MD033"]
"#;
fs::write(&config_path, config_content).unwrap();
let readme_content = "# Test\n\n<div>HTML content</div>\n";
let docs_content = "# Test\n\n<div>HTML content</div>\n";
fs::write(&readme_path, readme_content).unwrap();
fs::write(&docs_path, docs_content).unwrap();
let sourced = rumdl_lib::config::SourcedConfig::load(Some(config_path.to_str().unwrap()), None).unwrap();
let config: Config = sourced.into_validated_unchecked().into();
let all_rules = rules::all_rules(&config);
let ignored_readme = config.get_ignored_rules_for_file(Path::new("README.md"));
let readme_rules: Vec<_> = all_rules
.iter()
.filter(|rule| !ignored_readme.contains(rule.name()))
.collect();
let ignored_docs = config.get_ignored_rules_for_file(Path::new("docs.md"));
let docs_rules: Vec<_> = all_rules
.iter()
.filter(|rule| !ignored_docs.contains(rule.name()))
.collect();
assert!(!readme_rules.iter().any(|r| r.name() == "MD033"));
assert!(docs_rules.iter().any(|r| r.name() == "MD033"));
let readme_warnings = rumdl_lib::lint(
readme_content,
&readme_rules
.iter()
.map(|r| dyn_clone::clone_box(&***r))
.collect::<Vec<_>>(),
false,
config.markdown_flavor(),
None,
None,
)
.unwrap();
let docs_warnings = rumdl_lib::lint(
docs_content,
&docs_rules
.iter()
.map(|r| dyn_clone::clone_box(&***r))
.collect::<Vec<_>>(),
false,
config.markdown_flavor(),
None,
None,
)
.unwrap();
assert!(!readme_warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD033")));
assert!(docs_warnings.iter().any(|w| w.rule_name.as_deref() == Some("MD033")));
}
#[test]
fn test_per_file_ignores_combined_with_global_disable() {
let temp_dir = tempdir().unwrap();
let config_path = temp_dir.path().join(".rumdl.toml");
let config_content = r#"
[global]
disable = ["MD013"]
[per-file-ignores]
"README.md" = ["MD033"]
"#;
fs::write(&config_path, config_content).unwrap();
let sourced = rumdl_lib::config::SourcedConfig::load(Some(config_path.to_str().unwrap()), None).unwrap();
let config: Config = sourced.into_validated_unchecked().into();
assert!(config.global.disable.contains(&"MD013".to_string()));
assert_eq!(
config.per_file_ignores.get("README.md"),
Some(&vec!["MD033".to_string()])
);
let ignored_readme = config.get_ignored_rules_for_file(Path::new("README.md"));
assert!(ignored_readme.contains("MD033"));
assert_eq!(ignored_readme.len(), 1);
let ignored_other = config.get_ignored_rules_for_file(Path::new("other.md"));
assert!(ignored_other.is_empty());
}
#[test]
fn test_per_file_ignores_config_in_subdirectory() {
let temp_dir = tempdir().unwrap();
let config_dir = temp_dir.path().join(".config");
fs::create_dir(&config_dir).unwrap();
let config_path = config_dir.join("rumdl.toml");
let config_content = r#"
[per-file-ignores]
"LICENSE.md" = ["MD050"]
"#;
fs::write(&config_path, config_content).unwrap();
let license_path = temp_dir.path().join("LICENSE.md");
let license_content = "# License\n\n__Bold text__ with underscore.\n";
fs::write(&license_path, license_content).unwrap();
let git_dir = temp_dir.path().join(".git");
fs::create_dir(&git_dir).unwrap();
let sourced = rumdl_lib::config::SourcedConfig::load(Some(config_path.to_str().unwrap()), None).unwrap();
let config: Config = sourced.into_validated_unchecked().into();
assert!(config.project_root.is_some(), "project_root should be set");
let project_root = config.project_root.as_ref().unwrap();
assert!(
project_root.ends_with(temp_dir.path().file_name().unwrap()),
"project_root should point to temp dir (containing .git), not .config"
);
assert!(
config.per_file_ignores.contains_key("LICENSE.md"),
"per_file_ignores should contain LICENSE.md"
);
let ignored_rules = config.get_ignored_rules_for_file(&license_path);
assert!(
ignored_rules.contains("MD050"),
"MD050 should be ignored for LICENSE.md, but ignored_rules = {ignored_rules:?}"
);
let other_path = temp_dir.path().join("README.md");
let ignored_rules_other = config.get_ignored_rules_for_file(&other_path);
assert!(
!ignored_rules_other.contains("MD050"),
"MD050 should NOT be ignored for README.md"
);
}
#[test]
#[serial]
fn test_per_file_ignores_with_actual_relative_path() {
let temp_dir = tempdir().unwrap();
let config_dir = temp_dir.path().join(".config");
fs::create_dir(&config_dir).unwrap();
let config_path = config_dir.join("rumdl.toml");
let config_content = r#"
[per-file-ignores]
"CHANGELOG.md" = ["MD024"]
"#;
fs::write(&config_path, config_content).unwrap();
let git_dir = temp_dir.path().join(".git");
fs::create_dir(&git_dir).unwrap();
let changelog_path = temp_dir.path().join("CHANGELOG.md");
fs::write(&changelog_path, "# Changelog\n").unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let relative_config_path = ".config/rumdl.toml";
let load_result = rumdl_lib::config::SourcedConfig::load(Some(relative_config_path), None);
std::env::set_current_dir(&original_dir).unwrap();
let sourced = load_result.expect("Should load config from relative path");
let config: Config = sourced.into_validated_unchecked().into();
assert!(
config.project_root.is_some(),
"project_root should be set when loading from relative path"
);
let project_root = config.project_root.as_ref().unwrap();
assert!(
!project_root.as_os_str().is_empty(),
"project_root should NOT be empty string (this was the bug)"
);
assert!(
project_root.join(".git").exists() || project_root == temp_dir.path(),
"project_root should be the directory containing .git"
);
assert!(
config.per_file_ignores.contains_key("CHANGELOG.md"),
"per_file_ignores should contain CHANGELOG.md"
);
let ignored_rules = config.get_ignored_rules_for_file(&changelog_path);
assert!(
ignored_rules.contains("MD024"),
"MD024 should be ignored for CHANGELOG.md, but ignored_rules = {ignored_rules:?}"
);
}
#[test]
#[serial]
fn test_relative_path_with_single_component() {
let temp_dir = tempdir().unwrap();
let config_dir = temp_dir.path().join("configs");
fs::create_dir(&config_dir).unwrap();
let config_path = config_dir.join("lint.toml");
let config_content = r#"
[per-file-ignores]
"docs/*.md" = ["MD013"]
"#;
fs::write(&config_path, config_content).unwrap();
fs::create_dir(temp_dir.path().join(".git")).unwrap();
let docs_dir = temp_dir.path().join("docs");
fs::create_dir(&docs_dir).unwrap();
let readme_path = docs_dir.join("README.md");
fs::write(&readme_path, "# Docs\n").unwrap();
let original_dir = std::env::current_dir().unwrap();
std::env::set_current_dir(temp_dir.path()).unwrap();
let load_result = rumdl_lib::config::SourcedConfig::load(Some("configs/lint.toml"), None);
std::env::set_current_dir(&original_dir).unwrap();
let sourced = load_result.expect("Should load config");
let config: Config = sourced.into_validated_unchecked().into();
let project_root = config.project_root.as_ref().expect("project_root should be set");
assert!(
!project_root.as_os_str().is_empty(),
"project_root must not be empty string"
);
let ignored = config.get_ignored_rules_for_file(&readme_path);
assert!(
ignored.contains("MD013"),
"MD013 should be ignored for docs/README.md via glob pattern"
);
}