Coverage for /pythoncovmergedfiles/medio/medio/src/turbohtml/tools/fuzz/html_table_oracles.py: 39%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""Literal row groups expose matching parse and reparse insertion errors."""
3from __future__ import annotations
5import json
6import re
7from typing import TYPE_CHECKING, Final, cast
9from turbohtml import Element, Node, Text, parse_fragment
11if TYPE_CHECKING:
12 import random
13 from collections.abc import Callable
16def html_table_check(
17 case: str,
18 serialize: Callable[[Node], str] = Node.serialize,
19 read: Callable[[str], Element] = parse_fragment,
20) -> str | None:
21 """Expect a tbody independently of whether source tags spell it out."""
22 if (match := _CASE.fullmatch(case)) is None:
23 raise UnsupportedHtmlTableCaseError(case)
24 cell, group, count, leaf = match.groups()
25 encoded, decoded = _LEAVES[leaf]
26 row_count: Final = int(count)
27 rows: Final = "".join(
28 f'<tr id="{identifier}"><{cell} id="x">{encoded}</{cell}><{cell} id="y">{encoded}</{cell}></tr>'
29 for identifier in _ROW_IDS[:row_count]
30 )
31 source: Final = '<table id="z">' + (f"<tbody>{rows}</tbody>" if group == "explicit" else rows) + "</table>"
32 expected: Final = json.dumps([
33 ("element", "html", "table", (("id", "z"),), 0),
34 ("element", "html", "tbody", (), 1),
35 *[
36 record
37 for identifier, position in zip(_ROW_IDS[:row_count], (3, 8)[:row_count], strict=True)
38 for record in [
39 ("element", "html", "tr", (("id", identifier),), 2),
40 ("element", "html", cell, (("id", "x"),), position),
41 ("text", "", decoded, (), position + 1),
42 ("element", "html", cell, (("id", "y"),), position),
43 ("text", "", decoded, (), position + 3),
44 ]
45 ],
46 ])
47 root: Final = read(source)
48 if _snapshot(root) != expected:
49 return "parsed table tree differs from literals"
50 printed: Final = "".join(serialize(node) for node in root.children)
51 reparsed: Final = read(printed)
52 if _snapshot(reparsed) != expected:
53 return "serialized table tree differs from literals"
54 return (
55 None
56 if "".join(serialize(node) for node in reparsed.children) == printed
57 else "table serialization changes on repeat"
58 )
61def _snapshot(root: Element) -> str:
62 nodes: Final = [root, *root.descendants]
63 records: Final[list[tuple[str, str, str, tuple[tuple[str, str | list[str] | None], ...], int]]] = []
64 for node in nodes[1:]:
65 parent: Final = nodes.index(cast("Element", node.parent))
66 if isinstance(node, Element):
67 records.append(("element", node.namespace.value, node.tag, tuple(sorted(node.attrs.items())), parent))
68 elif isinstance(node, Text):
69 records.append(("text", "", node.data, (), parent))
70 else:
71 records.append(("other", "", type(node).__name__, (), parent))
72 return json.dumps(records)
75def html_table_generate(rng: random.Random) -> str:
76 """Finite rows and terminal cells avoid recursive expansion."""
77 return f"{rng.choice(_CELLS)}:{rng.choice(_GROUPS)}:{rng.choice(_COUNTS)}:{rng.choice(tuple(_LEAVES))}"
80def html_table_seeds() -> list[str]:
81 """Cover row-group insertion without relying on random draws."""
82 return [
83 f"{cell}:{group}:{count}:{leaf}"
84 for cell in _CELLS
85 for group in _GROUPS
86 for count in _COUNTS
87 for leaf in _LEAVES
88 ]
91def html_table_controls() -> dict[str, bool]:
92 """Stable cell text does not prove correct grouping."""
93 return {
94 "changed row group": html_table_check(
95 "td:implied:1:plain", lambda node: node.serialize().replace("tbody", "thead")
96 )
97 is not None,
98 "dropped row": html_table_check(
99 "th:explicit:2:plain",
100 lambda _node: (
101 '<table id="z"><tbody><tr id="x"><th id="x">MiXeD</th><th id="y">MiXeD</th></tr></tbody></table>'
102 ),
103 )
104 is not None,
105 "encoded text": html_table_check(
106 "td:implied:2:named", lambda node: node.serialize().replace("&", "&amp;")
107 )
108 is not None,
109 }
112class UnsupportedHtmlTableCaseError(ValueError):
113 """Separate invalid programs from failures of generated tables."""
116_CASE: Final = re.compile(r"(td|th):(explicit|implied):([12]):(plain|named|numeric)")
117_CELLS: Final = ("td", "th")
118_GROUPS: Final = ("explicit", "implied")
119_COUNTS: Final = (1, 2)
120_ROW_IDS: Final = ("x", "y")
121_LEAVES: Final = {
122 "plain": ("MiXeD", "MiXeD"),
123 "named": ("&<>", "&<>"),
124 "numeric": ("ABC", "ABC"),
125}
127__all__ = [
128 "UnsupportedHtmlTableCaseError",
129 "html_table_check",
130 "html_table_controls",
131 "html_table_generate",
132 "html_table_seeds",
133]