use proptest::prelude::*;
use surf_parse::error::FixSafety;
use surf_parse::lint::MAX_FIX_ITERATIONS;
fn arb_segment() -> impl Strategy<Value = String> {
prop_oneof![
Just("Plain paragraph text.\n".to_string()),
Just("## Heading\n\nMore text here.\n".to_string()),
Just("::callout[type=info]\nAll good.\n::\n".to_string()),
Just("| a | b |\n|---|---|\n| 1 | 2 |\n".to_string()),
"[A-Za-z ]{1,12}".prop_map(|t| format!("::section[title=\"{t}\"]\nSection body.\n::\n")),
Just("::table\n| x | y |\n|---|---|\n| 1 | 2 |\n::\n".to_string()),
Just("::callout{type=tip}\nCurly.\n::\n".to_string()),
"[A-Za-z !]{1,16}".prop_map(|t| format!("::callout[type=note] {t}\n::\n")),
Just(":::column\nToo deep.\n:::\n".to_string()),
Just("::callout\nHi\n::\n::\n".to_string()),
Just("::callouts[type=info]\nTypo.\n::\n".to_string()),
Just("# Café ☕ 日本語 🚀\n\n::callout{type=info}\n東京 emoji 🎉 body.\n::\n".to_string()),
]
}
fn arb_front_matter() -> impl Strategy<Value = String> {
prop_oneof![
Just(String::new()),
Just("---\ntitle: T\ntype: report\nstatus: active\n---\n\n".to_string()),
Just("---\ntitle: T\ntype: report\nstatus: Active\n---\n\n".to_string()),
Just("---\ntitle: T\ntype: report\nstatus: active\n\nBody starts here.\n".to_string()),
Just(
"---\ntitle: \"日本語 🚀\"\ntype: guide\nconfidence: high\ncreated: \"2026-06-01\"\n---\n\n"
.to_string()
),
]
}
fn arb_tail() -> impl Strategy<Value = String> {
prop_oneof![
Just(String::new()),
Just("::callout[type=warning]\nNever closed.\n".to_string()),
Just("::columns\n:::column\nNested, never closed.\n".to_string()),
]
}
fn arb_document() -> impl Strategy<Value = String> {
(
arb_front_matter(),
proptest::collection::vec(arb_segment(), 0..6),
arb_tail(),
)
.prop_map(|(fm, segments, tail)| format!("{fm}{}{tail}", segments.join("\n")))
}
proptest! {
#![proptest_config(ProptestConfig::with_cases(128))]
#[test]
fn apply_fixes_is_idempotent(doc in arb_document()) {
for tier in [FixSafety::Safe, FixSafety::Suggested] {
let once = surf_parse::apply_fixes(&doc, tier);
prop_assume!(once.iterations < MAX_FIX_ITERATIONS);
let twice = surf_parse::apply_fixes(&once.source, tier);
prop_assert_eq!(
&twice.source,
&once.source,
"second run must be a no-op (tier {:?})",
tier
);
prop_assert_eq!(twice.iterations, 0);
}
}
#[test]
fn applied_safe_fixes_converge(doc in arb_document()) {
let out = surf_parse::apply_fixes(&doc, FixSafety::Safe);
prop_assume!(out.iterations < MAX_FIX_ITERATIONS);
let recheck = surf_parse::check(&out.source);
let remaining: Vec<_> = recheck
.diagnostics
.iter()
.filter(|d| d.fix.as_ref().is_some_and(|f| f.safety == FixSafety::Safe))
.collect();
prop_assert!(
remaining.is_empty(),
"Safe-fixable diagnostics remain after convergence: {:?}",
remaining
);
}
#[test]
fn untouched_lines_are_byte_identical(
prefix_n in 0usize..5,
suffix_n in 0usize..5,
defect_idx in 0usize..4,
) {
let defects = [
"::callout{type=tip}\nCurly.\n::",
"::callout[type=note] Tail content!\n::",
"::section[title=\"S ☕\"]\nBody 日本語.\n::",
"::table\n| x |\n|---|\n| 1 |\n::",
];
let prefix: Vec<String> =
(0..prefix_n).map(|i| format!("Prefix line {i} — café ☕.")).collect();
let suffix: Vec<String> =
(0..suffix_n).map(|i| format!("Suffix line {i} — 東京.")).collect();
let doc = format!(
"{}\n\n{}\n\n{}\n",
prefix.join("\n"),
defects[defect_idx],
suffix.join("\n")
);
let out = surf_parse::apply_fixes(&doc, FixSafety::Safe);
prop_assert!(!out.applied.is_empty(), "defect must be fixed");
let out_lines: Vec<&str> = out.source.lines().collect();
for (i, line) in prefix.iter().enumerate() {
prop_assert_eq!(
out_lines.get(i).copied(),
Some(line.as_str()),
"prefix line {} must be untouched",
i
);
}
for (i, line) in suffix.iter().enumerate() {
let from_end = suffix.len() - i;
prop_assert_eq!(
out_lines.get(out_lines.len() - from_end).copied(),
Some(line.as_str()),
"suffix line {} must be untouched",
i
);
}
}
#[test]
fn apply_fixes_never_panics(input in "\\PC{0,400}") {
for tier in [FixSafety::Safe, FixSafety::Suggested] {
let out = surf_parse::apply_fixes(&input, tier);
let _ = surf_parse::check(&out.source);
}
}
#[test]
fn apply_fixes_never_panics_multiline(
lines in proptest::collection::vec(
prop_oneof![
"\\PC{0,40}",
Just("::".to_string()),
Just(":::".to_string()),
Just("---".to_string()),
Just("::callout{".to_string()),
Just("::section[title=\"x".to_string()),
],
0..30,
)
) {
let input = lines.join("\n");
let out = surf_parse::apply_fixes(&input, FixSafety::Suggested);
prop_assert!(out.iterations <= MAX_FIX_ITERATIONS);
let _ = surf_parse::apply_fixes_once(&input, FixSafety::Safe);
}
#[test]
fn apply_fixes_never_panics_any_string(input in any::<String>()) {
let _ = surf_parse::apply_fixes(&input, FixSafety::Suggested);
}
}