use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn rumdl_binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_rumdl"))
}
fn setup_nested_project() -> (TempDir, PathBuf, PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let parent = temp_dir.path().to_path_buf();
let project = parent.join("project");
fs::create_dir(&project).expect("Failed to create project dir");
let config_content = r#"[global]
exclude = ["ignored.md"]
"#;
fs::write(project.join(".rumdl.toml"), config_content).expect("Failed to write config");
let test_content = "# Test\n\n\n\n# Another heading\n";
fs::write(project.join("test.md"), test_content).expect("Failed to write test.md");
let ignored_content = "# Ignored\n\n\n\n# Another heading\n";
fs::write(project.join("ignored.md"), ignored_content).expect("Failed to write ignored.md");
(temp_dir, parent, project)
}
#[test]
fn test_config_path_relative_to_cwd_not_project_root() {
let (_temp_dir, parent, _project) = setup_nested_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg("--config")
.arg("./project/.rumdl.toml") .arg("project")
.arg("--no-cache")
.current_dir(&parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("Config file not found") && !stderr.contains("error"),
"Config file should be found with relative path. stderr: {stderr}"
);
assert!(
stdout.contains("test.md") || stderr.contains("test.md"),
"test.md should be linted. stdout: {stdout}, stderr: {stderr}"
);
assert!(
!stdout.contains("ignored.md"),
"ignored.md should be excluded from linting results. stdout: {stdout}"
);
}
#[test]
fn test_exclude_patterns_relative_to_project_root_not_cwd() {
let (_temp_dir, parent, _project) = setup_nested_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg("project")
.arg("--no-cache")
.current_dir(&parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("1 file"),
"Only test.md should be processed (ignored.md excluded). stdout: {stdout}"
);
assert!(
!stdout.contains("ignored.md"),
"ignored.md should not appear in results. stdout: {stdout}"
);
}
#[test]
fn test_config_and_exclude_from_deeply_nested_cwd() {
let (_temp_dir, parent, _project) = setup_nested_project();
let unrelated = parent.join("other");
fs::create_dir(&unrelated).expect("Failed to create other dir");
let output = Command::new(rumdl_binary())
.arg("check")
.arg("--config")
.arg("../project/.rumdl.toml")
.arg("../project")
.arg("--no-cache")
.current_dir(&unrelated)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!stderr.contains("Config file not found"),
"Config should be found via ../project/.rumdl.toml. stderr: {stderr}"
);
assert!(
!stdout.contains("ignored.md"),
"ignored.md should be excluded. stdout: {stdout}"
);
}
#[test]
fn test_explicit_config_overrides_autodiscovery() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let base = temp_dir.path();
let project = base.join("project");
fs::create_dir(&project).expect("Failed to create project dir");
let project_config = r#"[global]
exclude = ["excluded_by_project.md"]
"#;
fs::write(project.join(".rumdl.toml"), project_config).expect("Failed to write project config");
let external_config = r#"[global]
exclude = ["excluded_by_external.md"]
"#;
let external_config_path = base.join("external.toml");
fs::write(&external_config_path, external_config).expect("Failed to write external config");
let content = "# Test\n\n\n\n# Violation\n";
fs::write(project.join("excluded_by_project.md"), content).expect("Failed to write file");
fs::write(project.join("excluded_by_external.md"), content).expect("Failed to write file");
fs::write(project.join("normal.md"), content).expect("Failed to write file");
let output = Command::new(rumdl_binary())
.arg("check")
.arg("--config")
.arg(external_config_path.to_str().unwrap())
.arg("project")
.arg("--no-cache")
.current_dir(base)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("excluded_by_external.md"),
"excluded_by_external.md should be excluded by explicit config. stdout: {stdout}"
);
assert!(
stdout.contains("excluded_by_project.md"),
"excluded_by_project.md should be linted (project config overridden). stdout: {stdout}"
);
}
fn setup_path_pattern_project() -> (TempDir, PathBuf, PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let parent = temp_dir.path().to_path_buf();
let project = parent.join("project");
fs::create_dir(&project).expect("Failed to create project dir");
fs::create_dir(project.join("subdir")).expect("Failed to create subdir");
fs::create_dir(project.join("docs")).expect("Failed to create docs dir");
fs::create_dir_all(project.join("generated/deep/nested")).expect("Failed to create nested dirs");
let config_content = r#"[global]
exclude = [
"subdir/ignored.md",
"docs/*",
"generated/**/*.md"
]
"#;
fs::write(project.join(".rumdl.toml"), config_content).expect("Failed to write config");
let content = "# Test\n\n\n\n# Another heading\n";
fs::write(project.join("root.md"), content).expect("Failed to write root.md");
fs::write(project.join("subdir/other.md"), content).expect("Failed to write other.md");
fs::write(project.join("subdir/ignored.md"), content).expect("Failed to write ignored.md");
fs::write(project.join("docs/api.md"), content).expect("Failed to write api.md");
fs::write(project.join("docs/guide.md"), content).expect("Failed to write guide.md");
fs::write(project.join("generated/deep/nested/file.md"), content).expect("Failed to write nested file");
(temp_dir, parent, project)
}
#[test]
fn test_path_pattern_subdir_file_from_project_root() {
let (_temp_dir, _parent, project) = setup_path_pattern_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg(".")
.arg("--no-cache")
.current_dir(&project)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("subdir/ignored.md") && !stdout.contains("ignored.md:"),
"subdir/ignored.md should be excluded. stdout: {stdout}"
);
assert!(
stdout.contains("other.md"),
"subdir/other.md should be linted. stdout: {stdout}"
);
}
#[test]
fn test_path_pattern_subdir_file_from_parent_directory() {
let (_temp_dir, parent, _project) = setup_path_pattern_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg("project")
.arg("--no-cache")
.current_dir(&parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("subdir/ignored.md") && !stdout.contains("ignored.md:"),
"subdir/ignored.md should be excluded when running from parent. stdout: {stdout}"
);
assert!(
stdout.contains("other.md"),
"subdir/other.md should be linted. stdout: {stdout}"
);
}
#[test]
fn test_glob_pattern_docs_star_from_project_root() {
let (_temp_dir, _parent, project) = setup_path_pattern_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg(".")
.arg("--no-cache")
.current_dir(&project)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("api.md"),
"docs/api.md should be excluded. stdout: {stdout}"
);
assert!(
!stdout.contains("guide.md"),
"docs/guide.md should be excluded. stdout: {stdout}"
);
}
#[test]
fn test_glob_pattern_docs_star_from_parent_directory() {
let (_temp_dir, parent, _project) = setup_path_pattern_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg("project")
.arg("--no-cache")
.current_dir(&parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("api.md"),
"docs/api.md should be excluded when running from parent. stdout: {stdout}"
);
assert!(
!stdout.contains("guide.md"),
"docs/guide.md should be excluded when running from parent. stdout: {stdout}"
);
}
#[test]
fn test_deep_glob_pattern_from_project_root() {
let (_temp_dir, _parent, project) = setup_path_pattern_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg(".")
.arg("--no-cache")
.current_dir(&project)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("generated") && !stdout.contains("nested"),
"generated/**/*.md files should be excluded. stdout: {stdout}"
);
}
#[test]
fn test_deep_glob_pattern_from_parent_directory() {
let (_temp_dir, parent, _project) = setup_path_pattern_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg("project")
.arg("--no-cache")
.current_dir(&parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("generated") && !stdout.contains("nested"),
"generated/**/*.md should be excluded when running from parent. stdout: {stdout}"
);
}
#[test]
fn test_path_pattern_from_sibling_directory() {
let (_temp_dir, parent, _project) = setup_path_pattern_project();
let sibling = parent.join("sibling");
fs::create_dir(&sibling).expect("Failed to create sibling dir");
let output = Command::new(rumdl_binary())
.arg("check")
.arg("../project")
.arg("--no-cache")
.current_dir(&sibling)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("ignored.md:"),
"subdir/ignored.md should be excluded from sibling. stdout: {stdout}"
);
assert!(
!stdout.contains("api.md"),
"docs/api.md should be excluded from sibling. stdout: {stdout}"
);
assert!(
!stdout.contains("generated"),
"generated/**/*.md should be excluded from sibling. stdout: {stdout}"
);
assert!(stdout.contains("root.md"), "root.md should be linted. stdout: {stdout}");
}
#[test]
fn test_path_pattern_with_explicit_config_flag() {
let (_temp_dir, parent, project) = setup_path_pattern_project();
let config_path = project.join(".rumdl.toml");
let output = Command::new(rumdl_binary())
.arg("check")
.arg("--config")
.arg(config_path.to_str().unwrap())
.arg("project")
.arg("--no-cache")
.current_dir(&parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("ignored.md:"),
"subdir/ignored.md should be excluded with explicit config. stdout: {stdout}"
);
assert!(
!stdout.contains("api.md"),
"docs/api.md should be excluded with explicit config. stdout: {stdout}"
);
}
#[test]
fn test_multiple_nested_subdirs_pattern() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let parent = temp_dir.path();
let project = parent.join("project");
fs::create_dir(&project).expect("Failed to create project");
fs::create_dir_all(project.join("a/b/c/d")).expect("Failed to create nested dirs");
let config = r#"[global]
exclude = ["a/b/c/d/deep.md", "a/b/mid.md", "a/shallow.md"]
"#;
fs::write(project.join(".rumdl.toml"), config).expect("Failed to write config");
let content = "# Test\n\n\n\n# Violation\n";
fs::write(project.join("root.md"), content).expect("Failed to write root.md");
fs::write(project.join("a/shallow.md"), content).expect("Failed to write shallow.md");
fs::write(project.join("a/b/mid.md"), content).expect("Failed to write mid.md");
fs::write(project.join("a/b/c/d/deep.md"), content).expect("Failed to write deep.md");
fs::write(project.join("a/b/c/d/other.md"), content).expect("Failed to write other.md");
let output = Command::new(rumdl_binary())
.arg("check")
.arg("project")
.arg("--no-cache")
.current_dir(parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("shallow.md:"),
"a/shallow.md should be excluded. stdout: {stdout}"
);
assert!(
!stdout.contains("mid.md:"),
"a/b/mid.md should be excluded. stdout: {stdout}"
);
assert!(
!stdout.contains("deep.md:"),
"a/b/c/d/deep.md should be excluded. stdout: {stdout}"
);
assert!(stdout.contains("root.md"), "root.md should be linted. stdout: {stdout}");
assert!(
stdout.contains("other.md"),
"a/b/c/d/other.md should be linted (not excluded). stdout: {stdout}"
);
}
#[test]
fn test_absolute_config_path_works() {
let (_temp_dir, parent, project) = setup_nested_project();
let config_absolute = project.join(".rumdl.toml");
let output = Command::new(rumdl_binary())
.arg("check")
.arg("--config")
.arg(config_absolute.to_str().unwrap())
.arg("project")
.arg("--no-cache")
.current_dir(&parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("ignored.md"),
"ignored.md should be excluded with absolute config path. stdout: {stdout}"
);
}
#[test]
fn test_github_action_scenario() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let repo_root = temp_dir.path();
let github_dir = repo_root.join(".github");
fs::create_dir(&github_dir).expect("Failed to create .github dir");
let config = r#"[global]
exclude = ["vendor/**", "node_modules/**", ".github/**"]
"#;
fs::write(repo_root.join(".rumdl.toml"), config).expect("Failed to write config");
let content = "# Test\n\n\n\n# Violation\n";
fs::write(repo_root.join("README.md"), content).expect("Failed to write README.md");
let vendor = repo_root.join("vendor");
fs::create_dir(&vendor).expect("Failed to create vendor dir");
fs::write(vendor.join("external.md"), content).expect("Failed to write external.md");
let output = Command::new(rumdl_binary())
.arg("check")
.arg(".")
.arg("--no-cache")
.current_dir(repo_root)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
stdout.contains("README.md"),
"README.md should be linted. stdout: {stdout}"
);
assert!(
!stdout.contains("external.md"),
"vendor/external.md should be excluded. stdout: {stdout}"
);
}
#[test]
fn test_pyproject_toml_exclude_from_different_cwd() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let parent = temp_dir.path();
let project = parent.join("project");
fs::create_dir(&project).expect("Failed to create project dir");
let pyproject = r#"[tool.rumdl]
exclude = ["ignored.md"]
"#;
fs::write(project.join("pyproject.toml"), pyproject).expect("Failed to write pyproject.toml");
let content = "# Test\n\n\n\n# Violation\n";
fs::write(project.join("test.md"), content).expect("Failed to write test.md");
fs::write(project.join("ignored.md"), content).expect("Failed to write ignored.md");
let output = Command::new(rumdl_binary())
.arg("check")
.arg("project")
.arg("--no-cache")
.current_dir(parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(stdout.contains("test.md"), "test.md should be linted. stdout: {stdout}");
assert!(
!stdout.contains("ignored.md"),
"ignored.md should be excluded via pyproject.toml. stdout: {stdout}"
);
}
fn setup_directory_only_pattern_project() -> (TempDir, PathBuf, PathBuf) {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let parent = temp_dir.path().to_path_buf();
let project = parent.join("project");
fs::create_dir(&project).expect("Failed to create project dir");
fs::create_dir_all(project.join("content/blog/2014/archived")).expect("Failed to create 2014 dirs");
fs::create_dir(project.join("content/blog/2015")).expect("Failed to create 2015 dir");
fs::create_dir(project.join("content/pages")).expect("Failed to create pages dir");
let config_content = r#"[global]
exclude = ["content/blog/2014"]
"#;
fs::write(project.join(".rumdl.toml"), config_content).expect("Failed to write config");
let content = "# Test\n\n\n\n# Another heading\n";
fs::write(project.join("content/blog/2014/old-post.md"), content).expect("Failed to write old-post.md");
fs::write(project.join("content/blog/2014/archived/deep.md"), content).expect("Failed to write deep.md");
fs::write(project.join("content/blog/2015/new-post.md"), content).expect("Failed to write new-post.md");
fs::write(project.join("content/pages/about.md"), content).expect("Failed to write about.md");
(temp_dir, parent, project)
}
#[test]
fn test_directory_only_pattern_excludes_contents() {
let (_temp_dir, _parent, project) = setup_directory_only_pattern_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg(".")
.arg("--no-cache")
.current_dir(&project)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("old-post.md"),
"content/blog/2014/old-post.md should be excluded. stdout: {stdout}"
);
assert!(
!stdout.contains("deep.md"),
"content/blog/2014/archived/deep.md should be excluded. stdout: {stdout}"
);
assert!(
stdout.contains("new-post.md"),
"content/blog/2015/new-post.md should be linted. stdout: {stdout}"
);
assert!(
stdout.contains("about.md"),
"content/pages/about.md should be linted. stdout: {stdout}"
);
}
#[test]
fn test_directory_only_pattern_from_parent_directory() {
let (_temp_dir, parent, _project) = setup_directory_only_pattern_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg("project")
.arg("--no-cache")
.current_dir(&parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("old-post.md"),
"2014/old-post.md should be excluded from parent. stdout: {stdout}"
);
assert!(
!stdout.contains("deep.md"),
"2014/archived/deep.md should be excluded from parent. stdout: {stdout}"
);
assert!(
stdout.contains("new-post.md"),
"2015/new-post.md should be linted. stdout: {stdout}"
);
}
#[test]
fn test_directory_only_pattern_with_explicit_config() {
let (_temp_dir, parent, project) = setup_directory_only_pattern_project();
let output = Command::new(rumdl_binary())
.arg("check")
.arg("--config")
.arg(project.join(".rumdl.toml").to_str().unwrap())
.arg("project")
.arg("--no-cache")
.current_dir(&parent)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("old-post.md"),
"2014/old-post.md should be excluded with explicit config. stdout: {stdout}"
);
}
#[test]
fn test_mixed_directory_and_glob_patterns() {
let temp_dir = TempDir::new().expect("Failed to create temp dir");
let project = temp_dir.path().join("project");
fs::create_dir(&project).expect("Failed to create project dir");
fs::create_dir_all(project.join("vendor/lib")).expect("Failed to create vendor dirs");
fs::create_dir(project.join("docs")).expect("Failed to create docs dir");
fs::create_dir(project.join("generated")).expect("Failed to create generated dir");
let config = r#"[global]
exclude = [
"vendor",
"docs/*.tmp.md",
"generated/**"
]
"#;
fs::write(project.join(".rumdl.toml"), config).expect("Failed to write config");
let content = "# Test\n\n\n\n# Violation\n";
fs::write(project.join("vendor/external.md"), content).expect("Failed to write file");
fs::write(project.join("vendor/lib/nested.md"), content).expect("Failed to write file");
fs::write(project.join("docs/guide.md"), content).expect("Failed to write guide.md");
fs::write(project.join("docs/temp.tmp.md"), content).expect("Failed to write temp file");
fs::write(project.join("generated/output.md"), content).expect("Failed to write output");
fs::write(project.join("README.md"), content).expect("Failed to write README");
let output = Command::new(rumdl_binary())
.arg("check")
.arg(".")
.arg("--no-cache")
.current_dir(&project)
.output()
.expect("Failed to execute rumdl");
let stdout = String::from_utf8_lossy(&output.stdout);
assert!(
!stdout.contains("external.md"),
"vendor/external.md should be excluded. stdout: {stdout}"
);
assert!(
!stdout.contains("nested.md"),
"vendor/lib/nested.md should be excluded. stdout: {stdout}"
);
assert!(
!stdout.contains("temp.tmp.md"),
"docs/temp.tmp.md should be excluded. stdout: {stdout}"
);
assert!(
stdout.contains("guide.md"),
"docs/guide.md should be linted. stdout: {stdout}"
);
assert!(
!stdout.contains("output.md"),
"generated/output.md should be excluded. stdout: {stdout}"
);
assert!(
stdout.contains("README.md"),
"README.md should be linted. stdout: {stdout}"
);
}