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() {
err @ ConfigError::ExtendsNotFound { .. } => {
let message = err.to_string();
assert!(
message.contains("nonexistent_base.rumdl.toml"),
"Error should mention the missing target, got: {message}"
);
assert!(
message.contains("child.rumdl.toml"),
"Error should mention the referencing file, got: {message}"
);
}
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"
);
}
#[test]
fn test_extends_origin_attribution() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
let child = dir.path().join(".rumdl.toml");
fs::write(
&base,
r#"exclude = ["drafts"]
[MD013]
line-length = 100
"#,
)
.unwrap();
fs::write(
&child,
r#"extends = "base.rumdl.toml"
enable = ["MD001", "MD013"]
[MD013]
line-length = 120
"#,
)
.unwrap();
let sourced = SourcedConfig::load_with_discovery(Some(child.to_str().unwrap()), None, false).unwrap();
let origin_of = |origin: &Option<String>| -> String {
origin
.as_deref()
.and_then(|f| std::path::Path::new(f).file_name())
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default()
};
assert_eq!(origin_of(&sourced.global.exclude.origin), "base.rumdl.toml");
assert_eq!(origin_of(&sourced.global.enable.origin), ".rumdl.toml");
let md013 = sourced.rules.get("MD013").expect("MD013 config present");
let line_length = md013.values.get("line-length").expect("line-length set");
assert_eq!(toml::Value::Integer(120), line_length.value);
assert_eq!(origin_of(&line_length.origin), ".rumdl.toml");
}
struct EnvVarGuard {
key: String,
}
impl EnvVarGuard {
fn set(key: &str, value: &std::path::Path) -> Self {
unsafe { std::env::set_var(key, value) };
Self { key: key.to_string() }
}
}
impl Drop for EnvVarGuard {
fn drop(&mut self) {
unsafe { std::env::remove_var(&self.key) };
}
}
#[test]
#[serial_test::serial]
fn test_extends_env_var_expansion() {
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 _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_BASE_DIR", dir.path());
let child = dir.path().join("child.rumdl.toml");
fs::write(
&child,
"extends = '$RUMDL_TEST_EXTENDS_BASE_DIR/base.rumdl.toml'\n\n[MD013]\nline-length = 120\n",
)
.unwrap();
let config = load_config(&child).unwrap();
assert!(
config.global.disable.contains(&"MD033".to_string()),
"child should inherit the base via an env-var-expanded extends path"
);
let line_length = rumdl_lib::config::get_rule_config_value::<i64>(&config, "MD013", "line-length");
assert_eq!(line_length, Some(120), "child override should still apply");
}
#[test]
#[serial_test::serial]
fn test_extends_env_var_braced_form() {
let dir = tempdir().unwrap();
let base = dir.path().join("base.rumdl.toml");
fs::write(&base, "[global]\ndisable = [\"MD041\"]\n").unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_BRACED_DIR", dir.path());
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '${RUMDL_TEST_EXTENDS_BRACED_DIR}/base.rumdl.toml'\n").unwrap();
let config = load_config(&child).unwrap();
assert!(
config.global.disable.contains(&"MD041".to_string()),
"braced ${{VAR}} form should resolve and inherit the base"
);
}
#[test]
#[serial_test::serial]
fn test_extends_undefined_env_var_gives_clear_error() {
let dir = tempdir().unwrap();
unsafe { std::env::remove_var("RUMDL_TEST_DEFINITELY_UNSET_667") };
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_DEFINITELY_UNSET_667/base.rumdl.toml'\n").unwrap();
match load_config(&child).unwrap_err() {
ConfigError::ExtendsUndefinedVar { var, from } => {
assert_eq!(
var, "$RUMDL_TEST_DEFINITELY_UNSET_667",
"error should name the missing variable"
);
assert!(
from.contains("child.rumdl.toml"),
"error should name the referencing file, got from: {from}"
);
}
other => panic!("Expected ExtendsUndefinedVar error, got: {other:?}"),
}
}
#[test]
#[serial_test::serial]
fn test_extends_env_var_cycle_is_detected() {
let dir = tempdir().unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_SELF_DIR", dir.path());
let cfg = dir.path().join("self.rumdl.toml");
fs::write(&cfg, "extends = '$RUMDL_TEST_EXTENDS_SELF_DIR/self.rumdl.toml'\n").unwrap();
match load_config(&cfg).unwrap_err() {
ConfigError::CircularExtends { .. } => {} other => panic!("Expected CircularExtends via env-expanded path, got: {other:?}"),
}
}
const SECRET: &str = "s3cr3t-value-do-not-leak";
#[track_caller]
fn assert_reference_named_without_secret(message: &str, written_as: &str) {
assert!(
!message.contains(SECRET),
"message disclosed the expanded environment variable: {message}"
);
assert!(
message.contains(written_as),
"message should name the extends value as written ({written_as}), got: {message}"
);
assert!(
message.contains("child.rumdl.toml"),
"message should name the referencing config, got: {message}"
);
}
#[test]
#[serial_test::serial]
fn test_extends_missing_target_does_not_disclose_expanded_path() {
let dir = tempdir().unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_SECRET_DIR", &dir.path().join(SECRET));
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_SECRET_DIR/base.toml'\n").unwrap();
match load_config(&child).unwrap_err() {
err @ ConfigError::ExtendsNotFound { .. } => {
let message = err.to_string();
assert_reference_named_without_secret(&message, "$RUMDL_TEST_EXTENDS_SECRET_DIR/base.toml");
assert!(
message.contains("RUMDL_TEST_EXTENDS_SECRET_DIR"),
"naming the substituted variable is what makes the error diagnosable, got: {message}"
);
}
other => panic!("Expected ExtendsNotFound, got: {other:?}"),
}
}
#[test]
#[serial_test::serial]
fn test_extends_unreadable_target_does_not_disclose_expanded_path() {
let dir = tempdir().unwrap();
let target = dir.path().join(SECRET);
fs::create_dir(&target).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_SECRET_TARGET", &target);
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_SECRET_TARGET'\n").unwrap();
let message = load_config(&child).unwrap_err().to_string();
assert_reference_named_without_secret(&message, "$RUMDL_TEST_EXTENDS_SECRET_TARGET");
}
#[test]
#[serial_test::serial]
fn test_extends_parse_error_does_not_quote_target_contents() {
let dir = tempdir().unwrap();
let target = dir.path().join("private-key.pem");
fs::write(&target, format!("-----BEGIN PRIVATE KEY-----\n{SECRET}\n")).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_SECRET_FILE", &target);
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_SECRET_FILE'\n").unwrap();
let message = load_config(&child).unwrap_err().to_string();
assert!(
!message.contains("BEGIN PRIVATE KEY"),
"parse error quoted a line of the target file: {message}"
);
assert_reference_named_without_secret(&message, "$RUMDL_TEST_EXTENDS_SECRET_FILE");
assert!(
message.contains("line 1"),
"the position still has to be reported so the owner of the file can find it, got: {message}"
);
}
#[test]
fn test_direct_config_parse_error_still_quotes_the_line() {
let dir = tempdir().unwrap();
let config = dir.path().join("broken.rumdl.toml");
fs::write(&config, "[global]\nthis line is not toml\n").unwrap();
let message = load_config(&config).unwrap_err().to_string();
assert!(
message.contains("this line is not toml"),
"a directly named config should still have its offending line quoted, got: {message}"
);
}
#[test]
#[serial_test::serial]
fn test_extends_cycle_does_not_disclose_expanded_paths() {
let dir = tempdir().unwrap();
let secret_dir = dir.path().join(SECRET);
fs::create_dir(&secret_dir).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_CYCLE_DIR", &secret_dir);
let base = secret_dir.join("base.rumdl.toml");
fs::write(&base, "extends = '../child.rumdl.toml'\n").unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_CYCLE_DIR/base.rumdl.toml'\n").unwrap();
let message = load_config(&child).unwrap_err().to_string();
assert!(
!message.contains(SECRET),
"the cycle report disclosed the expanded environment variable: {message}"
);
assert!(
message.contains("$RUMDL_TEST_EXTENDS_CYCLE_DIR/base.rumdl.toml"),
"the cycle report should still name the reference as written, got: {message}"
);
}
fn validation_warnings(config: &std::path::Path) -> Vec<String> {
let sourced = SourcedConfig::load_with_discovery(Some(config.to_str().unwrap()), None, false).unwrap();
let rules = rumdl_lib::all_rules(&rumdl_lib::config::Config::default());
let registry = rumdl_lib::config::RuleRegistry::from_rules(&rules);
rumdl_lib::config::validate_config_sourced(&sourced, ®istry)
.into_iter()
.map(|w| w.message)
.collect()
}
#[test]
#[serial_test::serial]
fn test_extends_unknown_key_warning_does_not_disclose_expanded_path() {
let dir = tempdir().unwrap();
let secret_dir = dir.path().join(SECRET);
fs::create_dir(&secret_dir).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_WARN_DIR", &secret_dir);
let base = secret_dir.join("base.rumdl.toml");
fs::write(&base, "[global]\nprod-database-password = true\n").unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_WARN_DIR/base.rumdl.toml'\n").unwrap();
let messages = validation_warnings(&child);
let warning = messages
.iter()
.find(|m| m.contains("Unknown global option"))
.unwrap_or_else(|| panic!("expected a warning for the unknown key, got: {messages:?}"));
assert!(
!warning.contains("prod-database-password"),
"the warning repeated a key read out of the extends target: {warning}"
);
assert!(
warning.contains("<withheld>"),
"the warning should say a key was withheld, got: {warning}"
);
assert_reference_named_without_secret(warning, "$RUMDL_TEST_EXTENDS_WARN_DIR/base.rumdl.toml");
}
#[test]
#[serial_test::serial]
fn test_extends_unknown_section_warning_withholds_the_section_name() {
let dir = tempdir().unwrap();
let secret_dir = dir.path().join(SECRET);
fs::create_dir(&secret_dir).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_SECTION_DIR", &secret_dir);
let base = secret_dir.join("base.rumdl.toml");
fs::write(&base, "[prod-signing-key-id]\nenabled = true\n").unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_SECTION_DIR/base.rumdl.toml'\n").unwrap();
let messages = validation_warnings(&child);
let warning = messages
.iter()
.find(|m| m.starts_with("Unknown rule in"))
.unwrap_or_else(|| panic!("expected a warning for the unknown section, got: {messages:?}"));
assert!(
!warning.contains("prod-signing-key-id"),
"the warning repeated a section name read out of the extends target: {warning}"
);
assert!(
warning.contains("<withheld>"),
"the warning should say a section name was withheld, got: {warning}"
);
assert_reference_named_without_secret(warning, "$RUMDL_TEST_EXTENDS_SECTION_DIR/base.rumdl.toml");
}
#[test]
#[serial_test::serial]
fn test_extends_unknown_rule_option_is_withheld() {
let dir = tempdir().unwrap();
let secret_dir = dir.path().join(SECRET);
fs::create_dir(&secret_dir).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_OPTION_DIR", &secret_dir);
let base = secret_dir.join("base.rumdl.toml");
fs::write(&base, "[MD013]\nline-length = 80\nprod-database-password = true\n").unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_OPTION_DIR/base.rumdl.toml'\n").unwrap();
let messages = validation_warnings(&child);
let warning = messages
.iter()
.find(|m| m.contains("Unknown option for rule MD013"))
.unwrap_or_else(|| panic!("expected a warning for the unknown option, got: {messages:?}"));
assert!(
!warning.contains("prod-database-password"),
"the warning repeated an option key read out of the extends target: {warning}"
);
assert!(
warning.contains("<withheld>"),
"the warning should say an option was withheld, got: {warning}"
);
assert_reference_named_without_secret(warning, "$RUMDL_TEST_EXTENDS_OPTION_DIR/base.rumdl.toml");
let config = load_config(&child).unwrap();
assert_eq!(
rumdl_lib::config::get_rule_config_value::<i64>(&config, "MD013", "line-length"),
Some(80),
"the recognized option in the same table should still be inherited"
);
}
#[test]
fn test_direct_config_unknown_key_is_still_named() {
let dir = tempdir().unwrap();
let config = dir.path().join("named.rumdl.toml");
fs::write(
&config,
"[global]\nnot-a-global-option = true\nenable = [\"not-a-rule-name\"]\n\n[MD9999]\nenabled = true\n\n[MD013]\nnot-an-md013-option = 1\n",
)
.unwrap();
let messages = validation_warnings(&config);
assert!(
messages.iter().any(|m| m.contains("not-a-rule-name")),
"a directly named config should have its unknown rule name quoted back, got: {messages:?}"
);
assert!(
messages.iter().any(|m| m.contains("not-a-global-option")),
"a directly named config should have its unknown key quoted back, got: {messages:?}"
);
assert!(
messages.iter().any(|m| m.contains("MD9999")),
"a directly named config should have its unknown section quoted back, got: {messages:?}"
);
assert!(
messages.iter().any(|m| m.contains("not-an-md013-option")),
"a directly named config should have its unknown rule option quoted back, got: {messages:?}"
);
assert!(
messages.iter().all(|m| !m.contains("<withheld>")),
"nothing should be withheld for a config the user named, got: {messages:?}"
);
}
#[test]
fn test_extends_invalid_value_warning_does_not_echo_the_value() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(&base, format!("[per-file-flavor]\n\"*.md\" = \"{SECRET}\"\n")).unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_FLAVOR_DIR/base.rumdl.toml'\n").unwrap();
let doc = dir.path().join("doc.md");
fs::write(&doc, "# Title\n").unwrap();
let run = |config: &std::path::Path| -> String {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache", "--config"])
.arg(config)
.arg(&doc)
.env("RUST_LOG", "warn")
.env("RUMDL_TEST_EXTENDS_FLAVOR_DIR", &target_dir)
.output()
.expect("failed to run the rumdl binary");
String::from_utf8_lossy(&output.stderr).into_owned()
};
let through_extends = run(&child);
assert!(
!through_extends.contains(SECRET),
"the warning echoed a value read out of the extends target: {through_extends}"
);
assert!(
through_extends.contains("Invalid flavor"),
"the problem still has to be reported, got: {through_extends}"
);
let directly = run(&base);
assert!(
directly.contains(SECRET),
"a directly named config should still name the invalid value, got: {directly}"
);
}
#[test]
#[serial_test::serial]
fn test_extends_unknown_rule_name_in_global_list_is_withheld() {
let dir = tempdir().unwrap();
let secret_dir = dir.path().join(SECRET);
fs::create_dir(&secret_dir).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_LIST_DIR", &secret_dir);
let base = secret_dir.join("base.rumdl.toml");
fs::write(&base, format!("[global]\nenable = [\"MD013\", \"{SECRET}\"]\n")).unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_LIST_DIR/base.rumdl.toml'\n").unwrap();
let messages = validation_warnings(&child);
let warning = messages
.iter()
.find(|m| m.contains("Unknown rule in global.enable"))
.unwrap_or_else(|| panic!("expected a warning for the unknown rule name, got: {messages:?}"));
assert!(
!warning.contains(SECRET),
"the warning repeated a rule name read out of the extends target: {warning}"
);
assert!(
warning.contains("<withheld>"),
"withholding has to stay visible, got: {warning}"
);
let config = load_config(&child).unwrap();
assert_eq!(
config.global.enable.iter().filter(|r| *r == "MD013").count(),
1,
"the recognized rule name should survive, got: {:?}",
config.global.enable
);
}
#[test]
fn test_extends_invalid_glob_pattern_is_withheld() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(
&base,
format!("[global]\nenable = [\"MD013\"]\n\n[per-file-ignores]\n\"[{SECRET}\" = [\"MD013\"]\n"),
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_GLOB_DIR/base.rumdl.toml'\n").unwrap();
let doc = dir.path().join("doc.md");
fs::write(&doc, format!("# Title\n\n{}\n", ["word"; 40].join(" "))).unwrap();
let run = |config: &std::path::Path| -> String {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache", "--config"])
.arg(config)
.arg(&doc)
.env("RUST_LOG", "warn")
.env("RUMDL_TEST_EXTENDS_GLOB_DIR", &target_dir)
.output()
.expect("failed to run the rumdl binary");
String::from_utf8_lossy(&output.stderr).into_owned()
};
let through_extends = run(&child);
assert!(
!through_extends.contains(SECRET),
"the warning echoed a pattern read out of the extends target: {through_extends}"
);
assert!(
through_extends.contains("Invalid glob pattern in per-file-ignores"),
"the problem still has to be reported, got: {through_extends}"
);
let directly = run(&base);
assert!(
directly.contains(SECRET),
"a directly named config should still name the invalid pattern, got: {directly}"
);
}
#[test]
#[serial_test::serial]
fn test_extends_valid_glob_pattern_survives_a_withheld_one() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_GLOB2_DIR", &target_dir);
let base = target_dir.join("base.rumdl.toml");
fs::write(
&base,
format!("[per-file-ignores]\n\"[{SECRET}\" = [\"MD013\"]\n\"docs/*.md\" = [\"MD041\"]\n"),
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_GLOB2_DIR/base.rumdl.toml'\n").unwrap();
let config = load_config(&child).unwrap();
assert_eq!(
config.per_file_ignores.keys().collect::<Vec<_>>(),
vec!["docs/*.md"],
"only the pattern that does not compile should go, got: {:?}",
config.per_file_ignores
);
}
#[test]
fn test_extends_invalid_walk_patterns_are_withheld() {
for setting in ["include", "exclude"] {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(&base, format!("[global]\n{setting} = [\"[{SECRET}\"]\n")).unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_WALK_DIR/base.rumdl.toml'\n").unwrap();
fs::write(dir.path().join("doc.md"), "# Title\n").unwrap();
let run = |config: &std::path::Path| -> String {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache", "--config"])
.arg(config)
.current_dir(dir.path())
.env("RUMDL_TEST_EXTENDS_WALK_DIR", &target_dir)
.output()
.expect("failed to run the rumdl binary");
String::from_utf8_lossy(&output.stderr).into_owned()
};
let through_extends = run(&child);
assert!(
!through_extends.contains(SECRET),
"the {setting} warning echoed a pattern read out of the extends target: {through_extends}"
);
assert!(
through_extends.contains(&format!("Invalid {setting} pattern in")),
"the problem still has to be reported, got: {through_extends}"
);
let directly = run(&base);
assert!(
directly.contains(SECRET),
"a directly named config should still name the invalid {setting} pattern, got: {directly}"
);
}
}
#[test]
#[serial_test::serial]
fn test_extends_valid_walk_patterns_survive_a_withheld_one() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_WALK2_DIR", &target_dir);
let base = target_dir.join("base.rumdl.toml");
fs::write(
&base,
format!("[global]\ninclude = [\"[{SECRET}\", \"docs/**\"]\nexclude = [\"[{SECRET}\", \"drafts\"]\n"),
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_WALK2_DIR/base.rumdl.toml'\n").unwrap();
let config = load_config(&child).unwrap();
assert_eq!(
config.global.include,
vec![format!("[{SECRET}"), "docs/**".to_string()],
"the include patterns should reach the walk as written"
);
let withheld = config.global.include_withheld.as_deref().unwrap_or_default();
assert!(
withheld.starts_with("'$RUMDL_TEST_EXTENDS_WALK2_DIR/base.rumdl.toml' (referenced from ")
&& !withheld.contains(SECRET),
"the walk needs the origin to report a pattern it cannot use without quoting it, got: {withheld}"
);
assert_eq!(
config.global.exclude,
vec!["drafts".to_string()],
"only the exclude pattern that does not compile should go"
);
}
#[test]
fn test_extends_absolute_include_survives_a_project_root_name() {
let dir = tempdir().unwrap();
let mut arms = Vec::new();
for arm in ["notes [2019-2021]", "named [2019-2021]"] {
let project = dir.path().join(arm);
fs::create_dir_all(project.join("docs")).unwrap();
fs::create_dir(project.join(".git")).unwrap();
fs::write(project.join("docs/note.md.jinja"), "no heading here\n").unwrap();
let canonical = rumdl_lib::discovery::canonicalize_for_matching(&project).unwrap();
let include = format!("include = ['{}/docs/*.md.jinja']\n", canonical.display()).replace('\\', "/");
arms.push((project, include));
}
let (extending, extending_include) = &arms[0];
let base_dir = dir.path().join("shared");
fs::create_dir(&base_dir).unwrap();
let base = base_dir.join("base.rumdl.toml");
fs::write(&base, format!("[global]\n{extending_include}")).unwrap();
fs::write(
extending.join(".rumdl.toml"),
"extends = '$RUMDL_TEST_EXTENDS_ABSINC_DIR/base.rumdl.toml'\n",
)
.unwrap();
let (direct, direct_include) = &arms[1];
fs::write(direct.join(".rumdl.toml"), format!("[global]\n{direct_include}")).unwrap();
let run = |project: &std::path::Path| -> String {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache"])
.current_dir(project)
.env("RUMDL_TEST_EXTENDS_ABSINC_DIR", &base_dir)
.output()
.expect("failed to run the rumdl binary");
String::from_utf8_lossy(&output.stdout).into_owned()
};
let directly = run(direct);
assert!(
directly.contains("note.md.jinja"),
"the absolute include should select the file when the config is the project's own, got: {directly}"
);
let through_extends = run(extending);
assert!(
through_extends.contains("note.md.jinja"),
"the same include reached through extends should select the same file, got: {through_extends}"
);
}
#[test]
fn test_extends_unmatched_include_is_not_quoted_in_the_empty_run_notice() {
let dir = tempdir().unwrap();
let base_dir = dir.path().join("shared");
fs::create_dir(&base_dir).unwrap();
let base = base_dir.join("base.rumdl.toml");
fs::write(&base, format!("[global]\ninclude = [\"docs/{SECRET}/*.md\"]\n")).unwrap();
let project = dir.path().join("project");
fs::create_dir_all(project.join("docs")).unwrap();
fs::write(project.join("docs/note.md"), "no heading here\n").unwrap();
fs::write(
project.join(".rumdl.toml"),
"extends = '$RUMDL_TEST_EXTENDS_NOMATCH_DIR/base.rumdl.toml'\n",
)
.unwrap();
let run = |cwd: &std::path::Path| -> String {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache"])
.current_dir(cwd)
.env("RUMDL_TEST_EXTENDS_NOMATCH_DIR", &base_dir)
.output()
.expect("failed to run the rumdl binary");
format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
)
};
let through_extends = run(&project);
assert!(
!through_extends.contains(SECRET),
"the empty-run notice quoted a pattern read out of the extends target: {through_extends}"
);
assert!(
through_extends.contains("1 include pattern in '$RUMDL_TEST_EXTENDS_NOMATCH_DIR/base.rumdl.toml'")
&& through_extends.contains("matches no file"),
"the notice still has to say an include selected nothing and where it came from, got: {through_extends}"
);
let named = dir.path().join("named");
fs::create_dir_all(named.join("docs")).unwrap();
fs::write(named.join("docs/note.md"), "no heading here\n").unwrap();
fs::write(
named.join(".rumdl.toml"),
format!("[global]\ninclude = [\"docs/{SECRET}/*.md\"]\n"),
)
.unwrap();
let directly = run(&named);
assert!(
directly.contains(&format!("include pattern 'docs/{SECRET}/*.md' matches no file")),
"a directly named config should still have its unmatched pattern quoted, got: {directly}"
);
}
#[test]
fn test_extends_uncompilable_include_is_reported_without_quoting_it() {
let dir = tempdir().unwrap();
let base_dir = dir.path().join("shared");
fs::create_dir(&base_dir).unwrap();
let base = base_dir.join("base.rumdl.toml");
fs::write(&base, format!("[global]\ninclude = [\"docs/[{SECRET}/*.md\"]\n")).unwrap();
let project = dir.path().join("project");
fs::create_dir_all(project.join("docs")).unwrap();
fs::write(project.join("docs/note.md"), "no heading here\n").unwrap();
fs::write(
project.join(".rumdl.toml"),
"extends = '$RUMDL_TEST_EXTENDS_BADINC_DIR/base.rumdl.toml'\n",
)
.unwrap();
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache"])
.current_dir(&project)
.env("RUMDL_TEST_EXTENDS_BADINC_DIR", &base_dir)
.output()
.expect("failed to run the rumdl binary");
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
let combined = format!("{stderr}{}", String::from_utf8_lossy(&output.stdout));
assert!(
!combined.contains(SECRET),
"the walk echoed a pattern read out of the extends target: {combined}"
);
assert!(
stderr.contains("Invalid include pattern in") && stderr.contains("<withheld>"),
"the problem still has to be reported, got: {stderr}"
);
let named = dir.path().join("named");
fs::create_dir_all(named.join("docs")).unwrap();
fs::write(named.join("docs/note.md"), "no heading here\n").unwrap();
fs::write(
named.join(".rumdl.toml"),
format!("[global]\ninclude = [\"docs/[{SECRET}/*.md\"]\n"),
)
.unwrap();
let directly = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache"])
.current_dir(&named)
.output()
.expect("failed to run the rumdl binary");
assert!(
String::from_utf8_lossy(&directly.stderr).contains(SECRET),
"a directly named config should still name the invalid include pattern, got: {}",
String::from_utf8_lossy(&directly.stderr)
);
}
#[test]
fn test_extends_invalid_rule_option_value_is_withheld() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(&base, format!("[MD003]\nstyle = \"{SECRET}\"\n")).unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_VALUE_DIR/base.rumdl.toml'\n").unwrap();
let doc = dir.path().join("doc.md");
fs::write(&doc, "# Title\n").unwrap();
let run = |config: &std::path::Path| -> String {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache", "--config"])
.arg(config)
.arg(&doc)
.env("RUMDL_TEST_EXTENDS_VALUE_DIR", &target_dir)
.output()
.expect("failed to run the rumdl binary");
String::from_utf8_lossy(&output.stderr).into_owned()
};
let through_extends = run(&child);
assert!(
!through_extends.contains(SECRET),
"the warning echoed a rule option value read out of the extends target: {through_extends}"
);
assert!(
through_extends.contains("Invalid configuration for rule MD003: <withheld>"),
"the problem still has to be reported: {through_extends}"
);
let directly = run(&base);
assert!(
directly.contains(SECRET),
"a directly named config should still name the invalid value, got: {directly}"
);
}
#[test]
#[serial_test::serial]
fn test_extends_valid_rule_option_values_still_apply() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let _guard = EnvVarGuard::set("RUMDL_TEST_EXTENDS_VALUE2_DIR", &target_dir);
let base = target_dir.join("base.rumdl.toml");
fs::write(&base, "[MD003]\nstyle = \"setext\"\n").unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_VALUE2_DIR/base.rumdl.toml'\n").unwrap();
let config = load_config(&child).unwrap();
assert_eq!(
config.rules["MD003"].values["style"].as_str(),
Some("setext"),
"the extended file's value should survive being marked"
);
}
#[test]
fn test_rule_option_value_named_by_the_extending_config_is_shown() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(&base, format!("[MD003]\nstyle = \"{SECRET}\"\n")).unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(
&child,
"extends = '$RUMDL_TEST_EXTENDS_VALUE3_DIR/base.rumdl.toml'\n[MD003]\nstyle = \"child-typo\"\n",
)
.unwrap();
let doc = dir.path().join("doc.md");
fs::write(&doc, "# Title\n").unwrap();
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache", "--config"])
.arg(&child)
.arg(&doc)
.env("RUMDL_TEST_EXTENDS_VALUE3_DIR", &target_dir)
.output()
.expect("failed to run the rumdl binary");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("child-typo"),
"the extending config's own value should be quoted back, got: {stderr}"
);
assert!(
!stderr.contains(SECRET),
"the overridden value should not appear at all, got: {stderr}"
);
}
#[test]
fn test_extends_uncompilable_rule_pattern_is_withheld() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(&base, format!("[MD051]\nignored-pattern = \"[{SECRET}\"\n")).unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_REGEX_DIR/base.rumdl.toml'\n").unwrap();
let doc = dir.path().join("doc.md");
fs::write(&doc, "# Title\n").unwrap();
let run = |config: &std::path::Path| -> String {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache", "--config"])
.arg(config)
.arg(&doc)
.env("RUST_LOG", "warn")
.env("RUMDL_TEST_EXTENDS_REGEX_DIR", &target_dir)
.output()
.expect("failed to run the rumdl binary");
String::from_utf8_lossy(&output.stderr).into_owned()
};
let through_extends = run(&child);
assert!(
!through_extends.contains(SECRET),
"the warning echoed a pattern read out of the extends target: {through_extends}"
);
assert!(
through_extends.contains("Invalid ignored-pattern for MD051: <withheld>"),
"the problem still has to be reported: {through_extends}"
);
let directly = run(&base);
assert!(
directly.contains(SECRET),
"a directly named config should still name the invalid pattern, got: {directly}"
);
}
#[test]
fn test_extends_compilable_rule_pattern_still_applies() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(&base, "[MD051]\nignored-pattern = \".*\"\n").unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_REGEX2_DIR/base.rumdl.toml'\n").unwrap();
let doc = dir.path().join("doc.md");
fs::write(&doc, "# Title\n\n[link](#no-such-anchor)\n").unwrap();
let run = |config: Option<&std::path::Path>| -> String {
let mut command = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"));
command.args(["check", "--no-cache"]);
match config {
Some(path) => {
command.arg("--config").arg(path);
}
None => {
command.arg("--no-config");
}
}
let output = command
.arg(&doc)
.env("RUMDL_TEST_EXTENDS_REGEX2_DIR", &target_dir)
.output()
.expect("failed to run the rumdl binary");
String::from_utf8_lossy(&output.stdout).into_owned()
};
let without = run(None);
assert!(
without.contains("MD051"),
"the fragment should be reported without the pattern, got: {without}"
);
let with = run(Some(&child));
assert!(
!with.contains("MD051"),
"the extended file's pattern should still filter, got: {with}"
);
}
#[test]
fn test_extends_unknown_code_block_tool_is_withheld() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("secrets");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(
&base,
format!("[code-block-tools]\nenabled = true\n\n[code-block-tools.languages.python]\nlint = [\"{SECRET}\"]\n"),
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = '$RUMDL_TEST_EXTENDS_TOOL_DIR/base.rumdl.toml'\n").unwrap();
let doc = dir.path().join("doc.md");
fs::write(&doc, "# Title\n\n```python\nx = 1\n```\n").unwrap();
let run = |config: &std::path::Path| -> String {
let output = std::process::Command::new(env!("CARGO_BIN_EXE_rumdl"))
.args(["check", "--no-cache", "--config"])
.arg(config)
.arg(&doc)
.env("RUST_LOG", "warn")
.env("RUMDL_TEST_EXTENDS_TOOL_DIR", &target_dir)
.output()
.expect("failed to run the rumdl binary");
String::from_utf8_lossy(&output.stderr).into_owned()
};
let through_extends = run(&child);
assert!(
!through_extends.contains(SECRET),
"the warning echoed a tool id read out of the extends target: {through_extends}"
);
assert!(
through_extends.contains("Unknown tool <withheld> configured for language <withheld>"),
"the problem still has to be reported: {through_extends}"
);
let directly = run(&base);
assert!(
directly.contains(SECRET),
"a directly named config should still name the unknown tool, got: {directly}"
);
}
#[test]
fn test_extends_nested_reference_is_withheld() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("shared");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(&base, format!("extends = 'missing-{SECRET}/further.rumdl.toml'\n")).unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = 'shared/base.rumdl.toml'\n").unwrap();
match load_config(&child).unwrap_err() {
err @ ConfigError::ExtendsNotFound { .. } => {
let message = err.to_string();
assert!(
!message.contains(SECRET),
"the error quoted an extends value read out of the extends target: {message}"
);
assert!(
message.contains("<withheld>"),
"withholding must be visible rather than leaving an empty name, got: {message}"
);
assert!(
message.contains("'shared/base.rumdl.toml'"),
"the file holding the unusable reference is what makes this fixable, got: {message}"
);
}
other => panic!("Expected ExtendsNotFound, got: {other:?}"),
}
let direct = dir.path().join("direct.rumdl.toml");
fs::write(&direct, format!("extends = 'missing-{SECRET}/further.rumdl.toml'\n")).unwrap();
let message = load_config(&direct).unwrap_err().to_string();
assert!(
message.contains(SECRET),
"a directly named config should still have its own reference quoted, got: {message}"
);
}
#[test]
fn test_extends_warning_about_a_nested_target_withholds_its_name() {
let dir = tempdir().unwrap();
let target_dir = dir.path().join("shared");
fs::create_dir(&target_dir).unwrap();
let deep = target_dir.join(format!("{SECRET}.rumdl.toml"));
fs::write(&deep, "[global]\nprod-database-password = true\n").unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(&base, format!("extends = '{SECRET}.rumdl.toml'\n")).unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = 'shared/base.rumdl.toml'\n").unwrap();
let unknown_key = |config: &std::path::Path| -> String {
let messages = validation_warnings(config);
messages
.iter()
.find(|m| m.contains("Unknown global option"))
.unwrap_or_else(|| panic!("expected a warning for the unknown key, got: {messages:?}"))
.clone()
};
let deep_warning = unknown_key(&child);
assert!(
!deep_warning.contains(SECRET),
"the warning named the deeper file with text out of the file that reached for it: {deep_warning}"
);
assert!(
deep_warning.contains("(referenced from 'shared/base.rumdl.toml')"),
"which file reached for it is the extending config's own text and still has to be said, got: {deep_warning}"
);
let shallow = dir.path().join("shallow.rumdl.toml");
fs::write(&shallow, format!("extends = 'shared/{SECRET}.rumdl.toml'\n")).unwrap();
let shallow_warning = unknown_key(&shallow);
assert!(
shallow_warning.contains(SECRET),
"a reference the named config wrote itself should still be quoted, got: {shallow_warning}"
);
}
#[test]
#[serial_test::serial]
fn test_extends_nested_undefined_variable_is_withheld() {
unsafe { std::env::remove_var("RUMDL_TEST_UNSET_CUSTOMER_ACME_INTERNAL") };
let dir = tempdir().unwrap();
let target_dir = dir.path().join("shared");
fs::create_dir(&target_dir).unwrap();
let base = target_dir.join("base.rumdl.toml");
fs::write(
&base,
"extends = '$RUMDL_TEST_UNSET_CUSTOMER_ACME_INTERNAL/base.toml'\n",
)
.unwrap();
let child = dir.path().join("child.rumdl.toml");
fs::write(&child, "extends = 'shared/base.rumdl.toml'\n").unwrap();
match load_config(&child).unwrap_err() {
ConfigError::ExtendsUndefinedVar { var, from } => {
assert_eq!(var, "<withheld>", "the variable name is text out of the extends target");
assert!(
from.contains("base.rumdl.toml"),
"the file that referenced it still has to be named, got from: {from}"
);
}
other => panic!("Expected ExtendsUndefinedVar, got: {other:?}"),
}
let direct = dir.path().join("direct.rumdl.toml");
fs::write(
&direct,
"extends = '$RUMDL_TEST_UNSET_CUSTOMER_ACME_INTERNAL/base.toml'\n",
)
.unwrap();
match load_config(&direct).unwrap_err() {
ConfigError::ExtendsUndefinedVar { var, .. } => {
assert_eq!(var, "$RUMDL_TEST_UNSET_CUSTOMER_ACME_INTERNAL");
}
other => panic!("Expected ExtendsUndefinedVar, got: {other:?}"),
}
}