1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
//! Performance regression guard for issue #406.
//!
//! Layout-heavy HTML (Outlook email digests, marketing newsletters) frequently
//! nests dozens of `<table>` tags inside a single outer cell to control visual
//! flow. Each nested table's column-width measurement pre-pass used to call
//! `walk_node` on every descendant table, recursing combinatorially:
//! `cells × nested-cells × deeply-nested-cells × ...`.
//! The per-cell output cap (`MAX_CELL_WIDTH = 200`) bounded the *discarded*
//! string output but not the measurement CPU.
//!
//! The fix in `walk_node`'s `"table"` arm short-circuits to descendant text
//! content when `Context::measure_width_only` is set (only true during the
//! outer table's width pre-pass), keeping the measurement walk linear in
//! descendant character count.
//!
//! This test fails if a synthetic worst-case nested-layout fixture cannot be
//! converted within a generous wall-clock budget on release builds. Without
//! the fix, the same fixture took minutes-to-hours and effectively hung the
//! conversion pipeline.
use std::time::{Duration, Instant};
fn nested_layout_cells(depth: usize, breadth: usize) -> String {
use std::fmt::Write as _;
if depth == 0 {
return "leaf".to_string();
}
let inner = nested_layout_cells(depth - 1, breadth);
let mut cells = String::new();
for _ in 0..breadth {
write!(cells, "<td><table><tr><td>{inner}</td></tr></table></td>").unwrap();
}
format!("<table><tr>{cells}</tr></table>")
}
#[test]
fn nested_layout_tables_convert_within_wall_clock_budget() {
// depth=4, breadth=4 → 4^4 = 256 leaf cells across 341 nested tables.
// Pre-#406 this took >30s on release builds. Post-fix it completes in
// under a second; the 10s budget leaves slack for noisy CI runners.
let html = format!(
"<html><body><table><tr><td>{}</td></tr></table></body></html>",
nested_layout_cells(4, 4)
);
let start = Instant::now();
let result = html_to_markdown_rs::convert(&html, None).expect("conversion must succeed");
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(10),
"issue #406: nested-layout-table conversion took {elapsed:?} (budget: 10s). \
Was the measure-width-only short-circuit in walk_node removed?"
);
let content = result.content.unwrap_or_default();
assert!(
content.contains("leaf"),
"leaf text must appear in output (otherwise the short-circuit is dropping content)"
);
}
#[test]
fn nested_layout_tables_render_within_outer_cell_width_cap() {
// Deeper nesting (5 levels × 3 breadth) to stress the pre-pass beyond what
// the previous cap-only approach handled. Without skipping nested-table
// dispatch during measurement, this would explode CPU even though the
// per-cell output cap kept the final string size bounded.
let html = format!(
"<html><body><table><tr><td>{}</td></tr></table></body></html>",
nested_layout_cells(5, 3)
);
let start = Instant::now();
let _ = html_to_markdown_rs::convert(&html, None).expect("conversion must succeed");
let elapsed = start.elapsed();
assert!(
elapsed < Duration::from_secs(10),
"issue #406: 5-deep × 3-wide nested-table conversion took {elapsed:?} (budget: 10s)"
);
}