use super::*;
#[test]
fn infinite_while_loop_is_bounded_with_a_diagnostic() {
let report = Parser::default()
.parse_bytes(
"loop.1",
b".TH LOOP 1\n.SH BODY\n.while 1 \\{\\\nloop\n.\\}\n.SH AFTER\nretained\n",
)
.expect("return the finite prefix of a looping manual");
let mut visible = Vec::new();
collect_visible_text(&report.document.root, &mut visible);
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("infinite loop")),
"loop budget must remain observable: {:?}",
report.diagnostics
);
assert!(
visible.contains(&"retained"),
"parsing must continue after the bounded loop"
);
assert!(
visible.iter().filter(|value| **value == "loop").count() <= 10_000,
"the loop body must not exceed the documented budget"
);
}
#[test]
fn aggregate_while_replays_are_bounded_across_statements() {
let mut source =
String::from(".TH AGGREGATE 1\n.SH BODY\n.de M\n.while 1 \\{\\\nreplayed\n.\\}\n..\n");
for _ in 0..3 {
source.push_str(".M\n");
}
source.push_str(".SH AFTER\nretained aggregate tail\n");
let report = Parser::default()
.parse_bytes("aggregate.1", source.as_bytes())
.expect("return the finite prefix across multiple loops");
let mut visible = Vec::new();
collect_visible_text(&report.document.root, &mut visible);
let replayed = visible.iter().filter(|value| **value == "replayed").count();
assert!(
replayed <= 10_003,
"three loop statements must share one replay budget: {replayed}"
);
assert!(visible.contains(&"retained aggregate tail"));
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("infinite loop")),
"aggregate exhaustion must remain observable: {:?}",
report.diagnostics
);
}
#[test]
fn recursive_user_macro_retains_content_after_the_cycle() {
let report = Parser::default()
.parse_bytes(
"recursive.7",
b".TH RECUR 7\n.SH NAME\nrecur \\- x\n.de R\n. R\n..\n.R\n.SH DESC\ntail marker ZZTAIL\n",
)
.expect("return the complete document around recursive macro input");
let mut visible = Vec::new();
collect_visible_text(&report.document.root, &mut visible);
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("infinite loop")),
"recursion limit must remain observable: {:?}",
report.diagnostics
);
let visible = visible.join(" ");
assert!(visible.contains("recur"), "{visible}");
assert!(visible.contains("tail marker ZZTAIL"), "{visible}");
}
#[test]
fn deeply_nested_callable_mdoc_macros_are_bounded_in_the_native_parser() {
let mut source = String::from(
".Dd August 24, 2026\n.Dt DEEP-MDOC 1\n.Os\n.Sh NAME\n.Nm deep-mdoc\n.Nd bounded callable macros\n.Sh BODY\n.Op ",
);
for _ in 0..50_000 {
source.push_str("Op ");
}
source.push_str("nested tail marker\n.Sh AFTER\nretained document tail\n");
let report = Parser::default()
.parse_bytes("deep-mdoc.1", source.as_bytes())
.expect("return a finite document for deeply nested callable macros");
let mut visible = Vec::new();
collect_visible_text(&report.document.root, &mut visible);
let visible = visible.join(" ");
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.message.contains("infinite loop")),
"macro depth exhaustion must remain observable: {:?}",
report.diagnostics
);
assert!(visible.contains("nested tail marker"), "{visible}");
assert!(visible.contains("retained document tail"), "{visible}");
}
#[test]
fn deeply_nested_input_is_bounded_instead_of_overflowing_the_stack() {
let depth = 180;
let mut source = String::from(".TH DEEP 1\n.SH BODY\n");
for _ in 0..depth {
source.push_str(".RS\n");
}
source.push_str("deep\n");
let report = Parser::default()
.parse_bytes("deep.1", source.as_bytes())
.expect("deeply nested source parses");
assert!(
measured_depth(&report.document.root) <= 300,
"tree depth must be bounded by the copy cap"
);
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code() == Some(DiagnosticCode::SyntaxTreeDepthLimit)),
"node truncation must remain observable: {:?}",
report.diagnostics
);
}
#[test]
fn deeply_nested_equation_is_bounded_instead_of_overflowing_the_stack() {
let depth = 5_000;
let mut equation = String::new();
for _ in 0..depth {
equation.push_str("sqrt { ");
}
equation.push('x');
for _ in 0..depth {
equation.push_str(" }");
}
let source = format!(".TH DEEP 1\n.SH BODY\n.EQ\n{equation}\n.EN\n");
let report = Parser::default()
.parse_bytes("deep-eqn.1", source.as_bytes())
.expect("deeply nested equation parses");
let node = find_kind(&report.document.root, NodeKind::Equation).expect("equation node");
let rendered = node.equation.as_deref().expect("equation text");
assert!(
rendered.len() < 2_000,
"equation text must be bounded by the copy cap, got {} bytes",
rendered.len()
);
assert!(
report
.diagnostics
.iter()
.any(|diagnostic| diagnostic.code() == Some(DiagnosticCode::EquationTreeDepthLimit)),
"equation truncation must remain observable: {:?}",
report.diagnostics
);
}