Skip to main content

max / pter

3.7 KB · 125 lines History Blame Raw
1 use proptest::prelude::*;
2
3 // Strategy: generate arbitrary HTML-like strings
4 fn html_fragment() -> impl Strategy<Value = String> {
5 let tags = prop::sample::select(vec![
6 "p",
7 "div",
8 "span",
9 "strong",
10 "em",
11 "a",
12 "h1",
13 "h2",
14 "h3",
15 "ul",
16 "ol",
17 "li",
18 "blockquote",
19 "pre",
20 "code",
21 "br",
22 "hr",
23 "img",
24 "table",
25 "tr",
26 "td",
27 "th",
28 "b",
29 "i",
30 "del",
31 "sup",
32 "sub",
33 ]);
34
35 let text = "[a-zA-Z0-9 .,!?]{0,100}";
36
37 prop::collection::vec(
38 prop_oneof![
39 // Plain text
40 text.prop_map(|s| s),
41 // Opening + closing tag with text
42 (tags.clone(), text).prop_map(|(tag, content)| { format!("<{tag}>{content}</{tag}>") }),
43 // Self-closing tag
44 tags.clone().prop_map(|tag| format!("<{tag}/>")),
45 // Nested tags
46 (tags.clone(), tags.clone(), text).prop_map(|(outer, inner, content)| {
47 format!("<{outer}><{inner}>{content}</{inner}></{outer}>")
48 }),
49 ],
50 1..10,
51 )
52 .prop_map(|parts| parts.join(""))
53 }
54
55 proptest! {
56 #[test]
57 fn never_panics(html in html_fragment()) {
58 let _ = pter::convert(&html);
59 }
60
61 #[test]
62 fn never_panics_on_arbitrary_bytes(s in "\\PC{0,500}") {
63 let _ = pter::convert(&s);
64 }
65
66 #[test]
67 fn output_contains_no_html_tags(html in html_fragment()) {
68 let md = pter::convert(&html);
69 // Output should never contain raw HTML tags
70 // (except inside code blocks, which we skip checking)
71 let without_code_blocks: String = md
72 .split("```")
73 .enumerate()
74 .filter(|(i, _)| i % 2 == 0) // only outside code blocks
75 .map(|(_, s)| s)
76 .collect();
77
78 // No <script>, <style>, <div>, etc. should leak through
79 assert!(!without_code_blocks.contains("<script"), "leaked <script> in: {md}");
80 assert!(!without_code_blocks.contains("<style"), "leaked <style> in: {md}");
81 assert!(!without_code_blocks.contains("<head"), "leaked <head> in: {md}");
82 }
83
84 #[test]
85 fn output_is_valid_utf8(html in html_fragment()) {
86 let md = pter::convert(&html);
87 // String type guarantees UTF-8, but verify no replacement chars snuck in
88 // from bad entity decoding
89 assert!(!md.contains('\u{FFFD}'), "replacement char in: {md}");
90 }
91
92 #[test]
93 fn no_trailing_whitespace_on_lines(html in html_fragment()) {
94 let md = pter::convert(&html);
95 for (i, line) in md.lines().enumerate() {
96 assert!(
97 line == line.trim_end(),
98 "trailing whitespace on line {i}: '{line}'"
99 );
100 }
101 }
102
103 #[test]
104 fn empty_input_returns_empty(s in "\\s{0,20}") {
105 let html = format!("<html><body>{s}</body></html>");
106 let md = pter::convert(&html);
107 // Whitespace-only input should produce empty or whitespace-only output
108 assert!(md.trim().is_empty() || !s.trim().is_empty());
109 }
110 }
111
112 // Drawn far past the default because the shape that broke this property took
113 // 44,991 draws to appear: a `<pre>` whose block markup has nothing to say, so
114 // only its separators reach the fence. 256 draws had never once produced it.
115 // The case lives in tests/edge_cases.rs; this is what would find the next one.
116 proptest! {
117 #![proptest_config(ProptestConfig::with_cases(100_000))]
118
119 #[test]
120 fn no_excessive_blank_lines(html in html_fragment()) {
121 let md = pter::convert(&html);
122 assert!(!md.contains("\n\n\n"), "triple newline in output: {md}");
123 }
124 }
125