Coverage for /pythoncovmergedfiles/medio/medio/src/turbohtml/tools/fuzz/html_grammar_oracles.py: 40%

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

48 statements  

1"""Finite sibling productions pin parsed multiplicity and decoded contents.""" 

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_sibling_check( 

17 case: str, 

18 serialize: Callable[[Node], str] = Node.serialize, 

19 read: Callable[[str], Element] = parse_fragment, 

20) -> str | None: 

21 """Compare generated literals before and after serialization.""" 

22 if (match := _CASE.fullmatch(case)) is None: 

23 raise UnsupportedHtmlSiblingCaseError(case) 

24 tag, attribute, quote, count, leaf = match.groups() 

25 encoded, decoded, encoded_attribute, decoded_attribute = _LEAVES[leaf] 

26 delimiter: Final = _QUOTES[quote] 

27 source: Final = f"<{tag} {attribute}={delimiter}{encoded_attribute}{delimiter}>{encoded}</{tag}>" * int(count) 

28 expected: Final = json.dumps([ 

29 record 

30 for _ in range(int(count)) 

31 for record in [ 

32 ("element", tag, ((attribute, decoded_attribute),), "div"), 

33 *([("text", decoded, tag)] if decoded else []), 

34 ] 

35 ]) 

36 root: Final = read(source) 

37 if _snapshot(root) != expected: 

38 return "parsed sibling tree differs from literals" 

39 printed: Final = "".join(serialize(node) for node in root.children) 

40 reparsed: Final = read(printed) 

41 if _snapshot(reparsed) != expected: 

42 return "serialized sibling tree differs from literals" 

43 return ( 

44 None 

45 if "".join(serialize(node) for node in reparsed.children) == printed 

46 else "sibling serialization changes on repeat" 

47 ) 

48 

49 

50def _snapshot(root: Element) -> str: 

51 records: Final[ 

52 list[tuple[str, str, str] | tuple[str, str, tuple[tuple[str, str | list[str] | None], ...], str]] 

53 ] = [] 

54 for node in root.descendants: 

55 parent: Element = cast("Element", node.parent) 

56 if isinstance(node, Element): 

57 records.append(("element", node.tag, tuple(sorted(node.attrs.items())), parent.tag)) 

58 elif isinstance(node, Text): 

59 records.append(("text", node.data, parent.tag)) 

60 else: 

61 records.append(("other", type(node).__name__, parent.tag)) 

62 return json.dumps(records) 

63 

64 

65def html_sibling_generate(rng: random.Random) -> str: 

66 """Bound siblings and choose terminal text without recursion.""" 

67 return ( 

68 f"{rng.choice(_TAGS)}:{rng.choice(_ATTRIBUTES)}:{rng.choice(tuple(_QUOTES))}:" 

69 f"{rng.randint(1, 4)}:{rng.choice(tuple(_LEAVES))}" 

70 ) 

71 

72 

73def html_sibling_seeds() -> list[str]: 

74 """Sweep structural, attribute and terminal productions.""" 

75 return [ 

76 f"{tag}:{attribute}:{quote}:{count}:{leaf}" 

77 for tag in _TAGS 

78 for attribute in _ATTRIBUTES 

79 for quote in _QUOTES 

80 for count in range(1, 5) 

81 for leaf in _LEAVES 

82 ] 

83 

84 

85def html_sibling_controls() -> dict[str, bool]: 

86 """Require multiplicity and literal attribute/text decoding.""" 

87 return { 

88 "dropped identical sibling": html_sibling_check( 

89 "span:title:bare:2:empty", lambda node: "" if node.previous_sibling is None else node.serialize() 

90 ) 

91 is not None, 

92 "changed attribute": html_sibling_check("span:title:bare:1:empty", lambda _node: '<span title="wrong"></span>') 

93 is not None, 

94 "encoded text": html_sibling_check( 

95 "span:title:bare:1:named", lambda node: node.serialize().replace("&amp;", "&amp;amp;") 

96 ) 

97 is not None, 

98 } 

99 

100 

101class UnsupportedHtmlSiblingCaseError(ValueError): 

102 """Separate malformed programs from failures of valid generated trees.""" 

103 

104 

105_CASE: Final = re.compile(r"(span|em|code):(title|data-x):(single|double|bare):([1-4]):(empty|plain|named|numeric)") 

106_TAGS: Final = ("span", "em", "code") 

107_ATTRIBUTES: Final = ("title", "data-x") 

108_QUOTES: Final = {"single": "'", "double": '"', "bare": ""} 

109_LEAVES: Final = { 

110 "empty": ("", "", "Token", "Token"), 

111 "plain": ("MiXeD", "MiXeD", "Token", "Token"), 

112 "named": ("&amp;&lt;&gt;", "&<>", "&quot;&apos;&amp;", "\"'&"), 

113 "numeric": ("&#65;&#x42;&#67;", "ABC", "&#x26;&#34;", '&"'), 

114} 

115 

116__all__ = [ 

117 "UnsupportedHtmlSiblingCaseError", 

118 "html_sibling_check", 

119 "html_sibling_controls", 

120 "html_sibling_generate", 

121 "html_sibling_seeds", 

122]