use rumdl_lib::config::SourcedConfig;
use rumdl_lib::config::types::ConfigError;
use std::fs;
use tempfile::tempdir;
fn load_config(path: &std::path::Path) -> Result<rumdl_lib::config::Config, ConfigError> {
SourcedConfig::load_with_discovery(Some(path.to_str().unwrap()), None, false)
.map(|s| s.into_validated_unchecked().into())
}
#[test]
fn test_extends_basic_inheritance() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(
&base,
r#"
[global]
disable = ["MD033"]
[MD013]
line-length = 80
"#,
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(
&child,
r#"extends = "base.rumdl.toml"
[MD013]
line-length = 120
"#,
)
.unwrap();
let config = load_config(&child).unwrap();
assert!(
config.global.disable.contains(&"MD033".to_string()),
"Child should inherit disable list from base"
);
let line_length = rumdl_lib::config::get_rule_config_value::<i64>(&config, "MD013", "line-length");
assert_eq!(line_length, Some(120), "Child should override line-length from base");
}
#[test]
fn test_extends_deep_chain() {
let dir = tempdir().unwrap();
let c = dir.path().join("c.rumdl.toml");
fs::write(
&c,
r#"
[global]
disable = ["MD041"]
"#,
)
.unwrap();
let b = dir.path().join("b.rumdl.toml");
fs::write(
&b,
r#"extends = "c.rumdl.toml"
[global]
disable = ["MD033"]
"#,
)
.unwrap();
let a = dir.path().join("a.rumdl.toml");
fs::write(
&a,
r#"extends = "b.rumdl.toml"
[global]
disable = ["MD013"]
"#,
)
.unwrap();
let config = load_config(&a).unwrap();
assert!(
config.global.disable.contains(&"MD013".to_string()),
"A's disable list should be applied"
);
}
#[test]
fn test_extends_child_overrides_all() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(
&base,
r#"
[global]
disable = ["MD033"]
flavor = "mkdocs"
"#,
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(
&child,
r#"extends = "base.rumdl.toml"
[global]
disable = ["MD013"]
flavor = "standard"
"#,
)
.unwrap();
let config = load_config(&child).unwrap();
assert!(
config.global.disable.contains(&"MD013".to_string()),
"Child's disable should be present"
);
assert!(
!config.global.disable.contains(&"MD033".to_string()),
"Base's disable should be replaced, not merged"
);
use rumdl_lib::config::MarkdownFlavor;
assert_eq!(
config.global.flavor,
MarkdownFlavor::Standard,
"Child flavor should override base flavor"
);
}
#[test]
fn test_extends_relative_path_resolution() {
let dir = tempdir().unwrap();
let sub_dir = dir.path().join("subdir");
fs::create_dir_all(&sub_dir).unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(
&base,
r#"
[global]
disable = ["MD001"]
"#,
)
.unwrap();
let child = sub_dir.join("child.rumdl.toml");
fs::write(
&child,
r#"extends = "../base.rumdl.toml"
[global]
disable = ["MD002"]
"#,
)
.unwrap();
let config = load_config(&child).unwrap();
assert!(
config.global.disable.contains(&"MD002".to_string()),
"Child's disable should be applied"
);
}
#[test]
fn test_extends_absolute_path_resolution() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(
&base,
r#"
[global]
disable = ["MD041"]
"#,
)
.unwrap();
let base_absolute = base.canonicalize().unwrap();
let child_content = format!(
r#"extends = "{}"
[global]
disable = ["MD013"]
"#,
base_absolute.display()
);
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, &child_content).unwrap();
let config = load_config(&child).unwrap();
assert!(
config.global.disable.contains(&"MD013".to_string()),
"Child's config should load with absolute path extends"
);
}
#[test]
fn test_extends_missing_base_file_gives_clear_error() {
let dir = tempdir().unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(
&child,
r#"extends = "nonexistent_base.rumdl.toml"
[global]
disable = ["MD013"]
"#,
)
.unwrap();
let result = load_config(&child);
assert!(result.is_err(), "Loading config with missing base should fail");
match result.unwrap_err() {
ConfigError::ExtendsNotFound { path, from } => {
assert!(
path.contains("nonexistent_base.rumdl.toml"),
"Error should mention missing file path, got path: {path}"
);
assert!(
from.contains("child.rumdl.toml"),
"Error should mention the referencing file, got from: {from}"
);
}
other => panic!("Expected ExtendsNotFound error, got: {other:?}"),
}
}
#[test]
fn test_extends_circular_reference_is_detected() {
let dir = tempdir().unwrap();
let a = dir.path().join("a.rumdl.toml");
let b = dir.path().join("b.rumdl.toml");
fs::write(&a, r#"extends = "b.rumdl.toml""#).unwrap();
fs::write(&b, r#"extends = "a.rumdl.toml""#).unwrap();
let result = load_config(&a);
assert!(result.is_err(), "Circular extends should produce an error");
match result.unwrap_err() {
ConfigError::CircularExtends { path, .. } => {
assert!(
path.contains("a.rumdl.toml") || path.contains("b.rumdl.toml"),
"Error should mention a file in the cycle, got: {path}"
);
}
other => panic!("Expected CircularExtends error, got: {other:?}"),
}
}
#[test]
fn test_extends_self_reference_is_detected() {
let dir = tempdir().unwrap();
let config = dir.path().join("self.rumdl.toml");
fs::write(&config, r#"extends = "self.rumdl.toml""#).unwrap();
let result = load_config(&config);
assert!(result.is_err(), "Self-referential extends should produce an error");
match result.unwrap_err() {
ConfigError::CircularExtends { .. } => {} other => panic!("Expected CircularExtends error, got: {other:?}"),
}
}
#[test]
fn test_extends_chain_propagation() {
let dir = tempdir().unwrap();
let c = dir.path().join("c.rumdl.toml");
fs::write(
&c,
r#"
[MD007]
indent = 4
"#,
)
.unwrap();
let b = dir.path().join("b.rumdl.toml");
fs::write(
&b,
r#"extends = "c.rumdl.toml"
[MD003]
style = "atx"
"#,
)
.unwrap();
let a = dir.path().join("a.rumdl.toml");
fs::write(&a, r#"extends = "b.rumdl.toml""#).unwrap();
let config = load_config(&a).unwrap();
let indent = rumdl_lib::config::get_rule_config_value::<i64>(&config, "MD007", "indent");
assert_eq!(indent, Some(4), "A should inherit MD007.indent from C via the chain");
let style = rumdl_lib::config::get_rule_config_value::<String>(&config, "MD003", "style");
assert_eq!(style, Some("atx".to_string()), "A should inherit MD003.style from B");
}
#[test]
fn test_extends_rule_config_child_overrides_base() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(
&base,
r#"
[MD013]
line-length = 80
[MD003]
style = "atx"
"#,
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(
&child,
r#"extends = "base.rumdl.toml"
[MD013]
line-length = 100
"#,
)
.unwrap();
let config = load_config(&child).unwrap();
let line_length = rumdl_lib::config::get_rule_config_value::<i64>(&config, "MD013", "line-length");
assert_eq!(line_length, Some(100), "Child should override MD013.line-length");
let style = rumdl_lib::config::get_rule_config_value::<String>(&config, "MD003", "style");
assert_eq!(
style,
Some("atx".to_string()),
"MD003.style should be inherited from base"
);
}
#[test]
fn test_extends_loaded_files_tracks_chain() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(&base, "").unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, r#"extends = "base.rumdl.toml""#).unwrap();
let sourced = SourcedConfig::load_with_discovery(Some(child.to_str().unwrap()), None, false).unwrap();
assert!(
sourced.loaded_files.len() >= 2,
"Both base and child should appear in loaded_files, got: {:?}",
sourced.loaded_files
);
let has_base = sourced.loaded_files.iter().any(|f| f.contains("base.rumdl.toml"));
let has_child = sourced.loaded_files.iter().any(|f| f.contains("child.rumdl.toml"));
assert!(has_base, "base.rumdl.toml should be in loaded_files");
assert!(has_child, "child.rumdl.toml should be in loaded_files");
}
#[test]
fn test_extends_extend_enable_union_semantics() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(
&base,
r#"
[global]
extend-enable = ["MD060"]
"#,
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(
&child,
r#"extends = "base.rumdl.toml"
[global]
extend-enable = ["MD063"]
"#,
)
.unwrap();
let config = load_config(&child).unwrap();
assert!(
config.global.extend_enable.contains(&"MD060".to_string()),
"Base's extend-enable should be present"
);
assert!(
config.global.extend_enable.contains(&"MD063".to_string()),
"Child's extend-enable should be present"
);
}
#[test]
fn test_extends_deep_chain_replace_semantics() {
let dir = tempdir().unwrap();
let c = dir.path().join("c.rumdl.toml");
fs::write(&c, "[global]\ndisable = [\"MD041\"]\n").unwrap();
let b = dir.path().join("b.rumdl.toml");
fs::write(&b, "extends = \"c.rumdl.toml\"\n[global]\ndisable = [\"MD033\"]\n").unwrap();
let a = dir.path().join("a.rumdl.toml");
fs::write(&a, "extends = \"b.rumdl.toml\"\n[global]\ndisable = [\"MD013\"]\n").unwrap();
let config = load_config(&a).unwrap();
assert_eq!(
config.global.disable,
vec!["MD013".to_string()],
"disable should contain only A's value (replace semantics)"
);
assert!(
!config.global.disable.contains(&"MD033".to_string()),
"B's disable should not leak into A"
);
assert!(
!config.global.disable.contains(&"MD041".to_string()),
"C's disable should not leak into A"
);
}
#[test]
fn test_extends_per_file_ignores_inherited() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(
&base,
r#"
[per-file-ignores]
"docs/*.md" = ["MD013"]
"README.md" = ["MD041"]
"#,
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(
&child,
r#"extends = "base.rumdl.toml"
[global]
line-length = 100
"#,
)
.unwrap();
let config = load_config(&child).unwrap();
assert!(
config.per_file_ignores.contains_key("docs/*.md"),
"Child should inherit per_file_ignores from base"
);
assert!(
config.per_file_ignores.contains_key("README.md"),
"Child should inherit all per_file_ignores patterns from base"
);
}
#[test]
fn test_extends_per_file_ignores_replaced_by_child() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(
&base,
r#"
[per-file-ignores]
"docs/*.md" = ["MD013"]
"#,
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(
&child,
r#"extends = "base.rumdl.toml"
[per-file-ignores]
"src/*.md" = ["MD041"]
"#,
)
.unwrap();
let config = load_config(&child).unwrap();
assert!(
config.per_file_ignores.contains_key("src/*.md"),
"Child's per_file_ignores should be present"
);
assert!(
!config.per_file_ignores.contains_key("docs/*.md"),
"Base's per_file_ignores should be replaced by child's"
);
}