use std::fs;
use std::process::Command;
fn load_schema() -> serde_json::Value {
let schema_path = concat!(env!("CARGO_MANIFEST_DIR"), "/rumdl.schema.json");
let schema_content =
fs::read_to_string(schema_path).expect("Failed to read schema file - run 'cargo dev --write' first");
serde_json::from_str(&schema_content).expect("Failed to parse schema JSON")
}
fn toml_to_json(toml_str: &str) -> serde_json::Value {
let toml_value: toml::Value = toml::from_str(toml_str).expect("Failed to parse TOML");
let json_str = serde_json::to_string(&toml_value).expect("Failed to convert TOML to JSON");
serde_json::from_str(&json_str).expect("Failed to parse converted JSON")
}
fn validate_toml_config(toml_str: &str) -> Result<(), String> {
let schema = load_schema();
let instance = toml_to_json(toml_str);
let compiled = jsonschema::validator_for(&schema).expect("Failed to compile schema");
compiled
.validate(&instance)
.map_err(|err| format!("{} at {}", err, err.instance_path()))
}
#[test]
fn test_schema_exists() {
let schema = load_schema();
assert_eq!(schema["$schema"], "https://json-schema.org/draft/2020-12/schema");
assert_eq!(schema["title"], "Config");
}
#[test]
fn test_empty_config_is_valid() {
let toml = "";
assert!(validate_toml_config(toml).is_ok());
}
#[test]
fn test_minimal_global_config() {
let toml = r#"
[global]
disable = ["MD013"]
"#;
assert!(validate_toml_config(toml).is_ok());
}
#[test]
fn test_full_global_config() {
let toml = r#"
[global]
disable = ["MD013", "MD033"]
enable = ["MD001", "MD003"]
exclude = ["node_modules", "*.tmp"]
include = ["docs/*.md"]
respect_gitignore = true
line_length = 100
flavor = "mkdocs"
"#;
assert!(validate_toml_config(toml).is_ok());
}
#[test]
fn test_per_file_ignores() {
let toml = r#"
[per-file-ignores]
"README.md" = ["MD033"]
"docs/**/*.md" = ["MD013", "MD033"]
"#;
assert!(validate_toml_config(toml).is_ok());
}
#[test]
fn test_rule_specific_config() {
let toml = r#"
[MD003]
style = "atx"
[MD007]
indent = 4
[MD013]
line_length = 100
code_blocks = false
tables = false
headings = true
[MD044]
names = ["rumdl", "Markdown", "GitHub"]
code-blocks = true
"#;
assert!(validate_toml_config(toml).is_ok());
}
#[test]
fn test_complete_example_config() {
let toml = r#"
[global]
disable = ["MD013", "MD033"]
exclude = [".git", "node_modules", "dist"]
respect_gitignore = true
[per-file-ignores]
"README.md" = ["MD033"]
"docs/api/**/*.md" = ["MD013"]
[MD002]
level = 1
[MD003]
style = "atx"
[MD004]
style = "asterisk"
[MD007]
indent = 4
[MD013]
line_length = 100
code_blocks = false
tables = false
"#;
let result = validate_toml_config(toml);
if let Err(error) = &result {
eprintln!("Validation error: {error}");
}
assert!(result.is_ok());
}
#[test]
fn test_flavor_variants() {
for flavor in ["standard", "mkdocs"] {
let toml = format!(
r#"
[global]
flavor = "{flavor}"
"#
);
let result = validate_toml_config(&toml);
assert!(result.is_ok(), "Flavor '{flavor}' should be valid");
}
}
#[test]
fn test_example_file_validates() {
let example_path = concat!(env!("CARGO_MANIFEST_DIR"), "/rumdl.toml.example");
let toml_content = fs::read_to_string(example_path).expect("Failed to read rumdl.toml.example");
let result = validate_toml_config(&toml_content);
if let Err(error) = &result {
eprintln!("Validation error in rumdl.toml.example: {error}");
}
assert!(result.is_ok(), "rumdl.toml.example should validate against schema");
}
#[test]
fn test_project_rumdl_toml_validates() {
let config_path = concat!(env!("CARGO_MANIFEST_DIR"), "/.rumdl.toml");
if let Ok(toml_content) = fs::read_to_string(config_path) {
let result = validate_toml_config(&toml_content);
if let Err(error) = &result {
eprintln!("Validation error in .rumdl.toml: {error}");
}
assert!(result.is_ok(), ".rumdl.toml should validate against schema");
}
}
#[test]
fn test_invalid_global_property() {
let toml = r#"
[global]
invalid_property = "should not exist"
"#;
let result = validate_toml_config(toml);
let _ = result;
}
#[test]
fn test_invalid_flavor_value() {
let toml = r#"
[global]
flavor = "invalid_flavor"
"#;
let result = validate_toml_config(toml);
assert!(result.is_err(), "Invalid flavor should fail validation");
}
#[test]
fn test_invalid_type_for_disable() {
let toml = r#"
[global]
disable = "MD013" # Should be an array, not a string
"#;
let result = validate_toml_config(toml);
assert!(result.is_err(), "Wrong type for disable should fail validation");
}
#[test]
fn test_invalid_type_for_line_length() {
let toml = r#"
[global]
line-length = "100" # Should be a number, not a string
"#;
let result = validate_toml_config(toml);
assert!(result.is_err(), "Wrong type for line-length should fail validation");
}
#[test]
fn test_invalid_type_for_respect_gitignore() {
let toml = r#"
[global]
respect-gitignore = "true" # Should be boolean, not string
"#;
let result = validate_toml_config(toml);
assert!(
result.is_err(),
"Wrong type for respect-gitignore should fail validation"
);
}
#[test]
fn test_schema_file_is_up_to_date() {
use rumdl_lib::config::Config;
let schema = schemars::schema_for!(Config);
let mut schema_value: serde_json::Value = serde_json::to_value(&schema).expect("Failed to convert schema to Value");
if let Some(obj) = schema_value.as_object_mut() {
obj.insert(
"additionalProperties".to_string(),
serde_json::json!({ "$ref": "#/$defs/RuleConfig" }),
);
}
let generated = serde_json::to_string_pretty(&schema_value).expect("Failed to serialize schema");
let schema_path = concat!(env!("CARGO_MANIFEST_DIR"), "/rumdl.schema.json");
let on_disk =
fs::read_to_string(schema_path).expect("Failed to read rumdl.schema.json — run 'rumdl schema generate' first");
if on_disk != generated {
let first_diff = on_disk
.lines()
.zip(generated.lines())
.enumerate()
.find(|(_, (a, b))| a != b)
.map_or_else(
|| {
format!(
"Line count differs: {} (disk) vs {} (generated)",
on_disk.lines().count(),
generated.lines().count()
)
},
|(i, (a, b))| {
format!(
"First difference at line {}:\n disk: {}\n generated: {}",
i + 1,
a,
b
)
},
);
panic!(
"rumdl.schema.json is out of date. Run 'rumdl schema generate' (or 'make schema') to update it.\n{first_diff}"
);
}
}
#[test]
fn test_schema_globalconfig_uses_kebab_case() {
let schema = load_schema();
let global_config = &schema["$defs"]["GlobalConfig"]["properties"];
let expected_kebab_case_properties = [
"line-length",
"respect-gitignore",
"force-exclude",
"output-format",
"cache-dir",
];
let forbidden_snake_case_properties = [
"line_length",
"respect_gitignore",
"force_exclude",
"output_format",
"cache_dir",
];
for prop in expected_kebab_case_properties {
assert!(
global_config.get(prop).is_some(),
"Schema must have kebab-case property '{prop}' in GlobalConfig"
);
}
for prop in forbidden_snake_case_properties {
assert!(
global_config.get(prop).is_none(),
"Schema must NOT have snake_case property '{prop}' in GlobalConfig (use kebab-case instead)"
);
}
}
#[test]
fn test_schema_no_invalid_uint64_format() {
let schema = load_schema();
let schema_str = serde_json::to_string_pretty(&schema).expect("Failed to serialize schema");
assert!(
!schema_str.contains(r#""format": "uint64""#),
"Schema contains invalid 'format: uint64' which Ajv doesn't recognize. \
Use #[schemars(schema_with = ...)] for u64 fields to generate valid schemas."
);
}
#[test]
fn test_config_accepts_both_kebab_and_snake_case() {
use rumdl_lib::config::Config;
let kebab_toml = r#"
[global]
line-length = 100
respect-gitignore = false
force-exclude = true
"#;
let kebab_config: Config = toml::from_str(kebab_toml).expect("Kebab-case config should parse");
assert_eq!(kebab_config.global.line_length.get(), 100);
assert!(!kebab_config.global.respect_gitignore);
let snake_toml = r#"
[global]
line_length = 100
respect_gitignore = false
force_exclude = true
"#;
let snake_config: Config =
toml::from_str(snake_toml).expect("Snake_case config should parse for backward compatibility");
assert_eq!(snake_config.global.line_length.get(), 100);
assert!(!snake_config.global.respect_gitignore);
assert_eq!(kebab_config.global.line_length, snake_config.global.line_length);
assert_eq!(
kebab_config.global.respect_gitignore,
snake_config.global.respect_gitignore
);
}
#[test]
fn test_example_config_produces_no_warnings() {
let example_path = concat!(env!("CARGO_MANIFEST_DIR"), "/rumdl.toml.example");
let rumdl_exe = env!("CARGO_BIN_EXE_rumdl");
let temp_dir = tempfile::tempdir().expect("Failed to create temp dir");
let test_file = temp_dir.path().join("test.md");
fs::write(
&test_file,
r#"# Test Heading
Some content paragraph.
- List item 1
- List item 2
```python
x = 1
```
Another paragraph.
"#,
)
.expect("Failed to write test file");
let warning_patterns = [
"[config warning]",
"Unknown rule",
"deprecated",
"did you mean",
"unrecognized",
"invalid config",
"unknown key",
"unknown option",
];
for mode in ["check", "fix"] {
let args: Vec<&str> = if mode == "fix" {
vec!["check", "--fix", "--config", example_path, test_file.to_str().unwrap()]
} else {
vec!["check", "--config", example_path, test_file.to_str().unwrap()]
};
let output = Command::new(rumdl_exe)
.args(&args)
.output()
.expect("Failed to execute rumdl");
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let combined = format!("{stdout}\n{stderr}");
let combined_lower = combined.to_lowercase();
let mut found_warnings = Vec::new();
for pattern in &warning_patterns {
if combined_lower.contains(&pattern.to_lowercase()) {
found_warnings.push(*pattern);
}
}
if !found_warnings.is_empty() {
eprintln!("=== rumdl.toml.example produced config warnings in {mode} mode ===");
eprintln!("Matched patterns: {found_warnings:?}");
eprintln!("stdout: {stdout}");
eprintln!("stderr: {stderr}");
panic!(
"rumdl.toml.example should not produce any config warnings.\n\
This usually means a deprecated or unknown rule is configured.\n\
Please update rumdl.toml.example to remove invalid configurations."
);
}
if !output.status.success() && output.status.code() != Some(1) {
eprintln!("=== rumdl failed unexpectedly in {mode} mode ===");
eprintln!("Exit code: {:?}", output.status.code());
eprintln!("stdout: {stdout}");
eprintln!("stderr: {stderr}");
panic!(
"rumdl.toml.example caused rumdl to fail with unexpected exit code.\n\
This may indicate a config loading error."
);
}
}
}