use rumdl_lib::lint_context::LintContext;
use rumdl_lib::rule::Rule;
use rumdl_lib::rules::MD051LinkFragments;
use std::collections::HashSet;
#[test]
fn property_deterministic_fragment_generation() {
let rule = MD051LinkFragments::new();
let test_inputs = vec![
"Simple Heading",
"Complex: (Pattern) & More!!!",
"Unicode: Café & 中文",
"Punctuation!@#$%^&*()",
"",
" ",
"123 Numbers",
"Mixed_Case_With_Underscores",
"Arrows -> <- <-> <=>",
"Quotes \"Test\" 'Single'",
];
for input in test_inputs {
let content1 = format!("# {input}\n\n");
let content2 = format!("# {input}\n\n");
let ctx1 = LintContext::new(&content1, rumdl_lib::config::MarkdownFlavor::Standard, None);
let ctx2 = LintContext::new(&content2, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings1 = extract_generated_headings(&rule, &ctx1);
let headings2 = extract_generated_headings(&rule, &ctx2);
assert_eq!(
headings1, headings2,
"Fragment generation is not deterministic for input: '{input}'"
);
}
}
#[test]
fn property_valid_fragment_characters() {
let rule = MD051LinkFragments::new();
let test_inputs = vec![
"Normal Text",
"Symbols!@#$%^&*()",
"Unicode: 日本語",
"Emoji 🎉 Party",
"Control\u{0001}Chars",
"Zero\u{200B}Width",
"Mixed: A->B & C",
"Quotes \"Smart\" Quotes",
"Math: x² + y³ = z⁴",
"Currency: $100€ ¥200",
];
for input in test_inputs {
let content = format!("# {input}\n\n");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings = extract_generated_headings(&rule, &ctx);
for heading in headings {
let is_valid = heading.chars().all(|c| {
c.is_alphanumeric() || c == '-' || c == '_' || (c.is_alphabetic() && !is_emoji_or_symbol(c))
});
assert!(
is_valid,
"Generated fragment '{heading}' contains invalid characters for input: '{input}'"
);
}
}
}
#[test]
fn property_reasonable_fragment_length() {
let rule = MD051LinkFragments::new();
let extremely_long = "A".repeat(1000);
let unicode_long = "Unicode: ".to_string() + &"日".repeat(100);
let test_inputs = vec![
"",
"A",
"Short",
"This is a reasonably long heading with multiple words",
"Very long heading that goes on and on with lots of words and punctuation!!! Really very long indeed.",
&extremely_long, &unicode_long,
];
for input in test_inputs {
let content = format!("# {input}\n\n");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings = extract_generated_headings(&rule, &ctx);
for heading in headings {
assert!(
heading.len() <= input.len() * 2, "Generated fragment '{}' is unreasonably long ({} chars) for input '{}' ({} chars)",
heading,
heading.len(),
input,
input.len()
);
assert!(
!heading.contains("----"), "Generated fragment '{heading}' has excessive consecutive hyphens for input: '{input}'"
);
}
}
}
#[test]
fn property_similarity_preservation() {
let rule = MD051LinkFragments::new();
let similar_pairs = vec![
("Test Heading", "Test Heading"), ("Test & More", "Test&More"), ("API Reference", "API Reference"), ("Step 1", "Step1"), ("Hello World", "Hello\tWorld"), ("Method()", "Method()"), ("café", "cafe"), ];
for (input1, input2) in similar_pairs {
let content1 = format!("# {input1}\n\n");
let content2 = format!("# {input2}\n\n");
let ctx1 = LintContext::new(&content1, rumdl_lib::config::MarkdownFlavor::Standard, None);
let ctx2 = LintContext::new(&content2, rumdl_lib::config::MarkdownFlavor::Standard, None);
let headings1 = extract_generated_headings(&rule, &ctx1);
let headings2 = extract_generated_headings(&rule, &ctx2);
for (h1, h2) in headings1.iter().zip(headings2.iter()) {
let similarity = calculate_similarity(h1, h2);
assert!(
similarity > 0.5, "Similar inputs '{input1}' and '{input2}' produced dissimilar fragments '{h1}' and '{h2}' (similarity: {similarity:.2})"
);
}
}
}
#[test]
fn property_robustness_no_panics() {
let rule = MD051LinkFragments::new();
let many_emoji = "🎉".repeat(100);
let many_zero_width = "\u{200B}".repeat(50);
let very_long_string = "a".repeat(10000);
let multiline = format!("{}\n{}", "Line 1", "Line 2");
let edge_cases = vec![
"\0", "\u{FFFF}", &many_emoji, &many_zero_width, &very_long_string, &multiline, "\u{1F4A9}", "مرحبا بالعالم", "𝕳𝖊𝖑𝖑𝖔 𝖂𝖔𝖗𝖑𝖉", ];
for input in edge_cases {
let content = format!("# {input}\n\n[Link](#test)");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = std::panic::catch_unwind(|| rule.check(&ctx));
assert!(result.is_ok(), "Rule panicked on input: '{input:?}'");
if let Ok(Ok(warnings)) = result {
assert!(
warnings.len() <= 100,
"Suspiciously many warnings for input: '{input:?}'"
);
}
}
}
#[test]
fn property_mode_consistency() {
let github_rule = MD051LinkFragments::new();
let kramdown_rule = MD051LinkFragments::new();
let test_inputs = vec![
"Simple Text",
"test_with_underscores",
"Numbers 123",
"Punctuation!!!",
"",
"café",
"UPPERCASE",
"Mixed_Case",
];
for input in test_inputs {
let content = format!("# {input}\n\n");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let github_result = github_rule.check(&ctx);
let kramdown_result = kramdown_rule.check(&ctx);
assert!(github_result.is_ok(), "GitHub mode failed for: '{input}'");
assert!(kramdown_result.is_ok(), "Kramdown mode failed for: '{input}'");
if input.trim().is_empty() {
let github_headings = extract_generated_headings(&github_rule, &ctx);
let kramdown_headings = extract_generated_headings(&kramdown_rule, &ctx);
assert_eq!(
github_headings.len(),
kramdown_headings.len(),
"Different number of headings generated for empty input"
);
}
}
}
#[test]
fn property_performance_bounds() {
let rule = MD051LinkFragments::new();
let long_heading_100 = "Long heading ".repeat(100);
let very_long_heading_1000 = "Very long heading ".repeat(1000);
let size_tests = vec![
(10, "Short"),
(100, "Medium length heading with some words"),
(1000, &long_heading_100),
(10000, &very_long_heading_1000),
];
for (expected_size, base_input) in size_tests {
let input = if base_input.len() < expected_size {
format!("{} {}", base_input, "word ".repeat(expected_size / 5))
} else {
base_input.chars().take(expected_size).collect()
};
let content = format!("# {input}\n\n");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let start = std::time::Instant::now();
let _result = rule.check(&ctx).unwrap();
let duration = start.elapsed();
let max_duration_ms = (input.len() / 100 + 1) as u64;
assert!(
duration.as_millis() <= max_duration_ms as u128,
"Performance issue: took {}ms for {} character input (max allowed: {}ms)",
duration.as_millis(),
input.len(),
max_duration_ms
);
}
}
fn extract_generated_headings(_rule: &MD051LinkFragments, ctx: &LintContext) -> Vec<String> {
let mut fragments = Vec::new();
for line_info in &ctx.lines {
if let Some(heading) = &line_info.heading {
let text = &heading.text;
let fragment = text
.to_lowercase()
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' {
c
} else if c.is_whitespace() {
'-'
} else {
' '
}
})
.collect::<String>()
.split_whitespace()
.collect::<Vec<_>>()
.join("-");
if !fragment.is_empty() {
fragments.push(fragment);
}
}
}
fragments
}
fn is_emoji_or_symbol(c: char) -> bool {
matches!(c as u32,
0x1F300..=0x1F9FF | 0x2600..=0x26FF | 0x2700..=0x27BF | 0x1F000..=0x1F02F | 0x1F0A0..=0x1F0FF )
}
fn calculate_similarity(s1: &str, s2: &str) -> f64 {
let chars1: HashSet<char> = s1.chars().collect();
let chars2: HashSet<char> = s2.chars().collect();
let intersection = chars1.intersection(&chars2).count();
let union = chars1.union(&chars2).count();
if union == 0 {
1.0 } else {
intersection as f64 / union as f64
}
}
#[test]
fn property_fuzz_like_testing() {
let rule = MD051LinkFragments::new();
let generators = vec![
(0..128)
.map(|i| char::from(i as u8))
.filter(char::is_ascii_graphic)
.collect::<String>(),
"!@#$%^&*()[]{}|\\:;\"'<>?,./-=+_`~".to_string(),
"Hello世界مرحباПривет".to_string(),
"abc".repeat(100),
"!@#".repeat(50),
" - ".repeat(30),
"a".to_string(),
"ab".repeat(1000),
];
for input in generators {
for prefix in &["", " ", " ", "!"] {
for suffix in &["", " ", " ", "!"] {
let test_input = format!("{prefix}{input}{suffix}");
let content = format!("# {test_input}\n\n");
let ctx = LintContext::new(&content, rumdl_lib::config::MarkdownFlavor::Standard, None);
let result = rule.check(&ctx);
assert!(result.is_ok(), "Failed on fuzz input: '{test_input:?}'");
}
}
}
}