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

51 statements  

1"""Literal row groups expose matching parse and reparse insertion errors.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import re 

7from typing import TYPE_CHECKING, Final, cast 

8 

9from turbohtml import Element, Node, Text, parse_fragment 

10 

11if TYPE_CHECKING: 

12 import random 

13 from collections.abc import Callable 

14 

15 

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 ) 

59 

60 

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) 

73 

74 

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))}" 

78 

79 

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 ] 

89 

90 

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;", "&amp;amp;") 

107 ) 

108 is not None, 

109 } 

110 

111 

112class UnsupportedHtmlTableCaseError(ValueError): 

113 """Separate invalid programs from failures of generated tables.""" 

114 

115 

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": ("&amp;&lt;&gt;", "&<>"), 

124 "numeric": ("&#65;&#x42;&#67;", "ABC"), 

125} 

126 

127__all__ = [ 

128 "UnsupportedHtmlTableCaseError", 

129 "html_table_check", 

130 "html_table_controls", 

131 "html_table_generate", 

132 "html_table_seeds", 

133]