Coverage for /pythoncovmergedfiles/medio/medio/src/turbohtml/tools/fuzz/html_foreign_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

49 statements  

1"""Literal SVG trees prevent matching parse and reparse errors from hiding changes.""" 

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

17 case: str, 

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

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

20) -> str | None: 

21 """Keep expected namespaces independent of parsing.""" 

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

23 raise UnsupportedHtmlForeignCaseError(case) 

24 boundary, identifier, quote, leaf = match.groups() 

25 encoded, decoded = _LEAVES[leaf] 

26 sibling: Final = "y" if identifier == "x" else "x" 

27 delimiter: Final = _QUOTES[quote] 

28 source: Final = ( 

29 f"<svg><{boundary}><a id={delimiter}{identifier}{delimiter}>{encoded}</a></{boundary}>" 

30 f"<g id={delimiter}{sibling}{delimiter}></g></svg>" 

31 ) 

32 expected: Final = json.dumps([ 

33 ("element", "svg", "svg", (), "html", "div"), 

34 ("element", "svg", boundary, (), "svg", "svg"), 

35 ("element", "html", "a", (("id", identifier),), "svg", boundary), 

36 ("text", "", decoded, (), "html", "a"), 

37 ("element", "svg", "g", (("id", sibling),), "svg", "svg"), 

38 ]) 

39 root: Final = read(source) 

40 if _snapshot(root) != expected: 

41 return "parsed SVG integration tree differs from literals" 

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

43 reparsed: Final = read(printed) 

44 if _snapshot(reparsed) != expected: 

45 return "serialized SVG integration tree differs from literals" 

46 return ( 

47 None 

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

49 else "SVG integration serialization changes on repeat" 

50 ) 

51 

52 

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

54 records: Final[list[tuple[str, str, str, tuple[tuple[str, str | list[str] | None], ...], str, str]]] = [] 

55 for node in root.descendants: 

56 parent: Final = cast("Element", node.parent) 

57 if isinstance(node, Element): 

58 records.append(( 

59 "element", 

60 node.namespace.value, 

61 node.tag, 

62 tuple(sorted(node.attrs.items())), 

63 parent.namespace.value, 

64 parent.tag, 

65 )) 

66 elif isinstance(node, Text): 

67 records.append(("text", "", node.data, (), parent.namespace.value, parent.tag)) 

68 else: 

69 records.append(("other", "", type(node).__name__, (), parent.namespace.value, parent.tag)) 

70 return json.dumps(records) 

71 

72 

73def html_foreign_generate(rng: random.Random) -> str: 

74 """Finite productions cap tree depth and size.""" 

75 return ( 

76 f"{rng.choice(_BOUNDARIES)}:{rng.choice(_IDENTIFIERS)}:" 

77 f"{rng.choice(tuple(_QUOTES))}:{rng.choice(tuple(_LEAVES))}" 

78 ) 

79 

80 

81def html_foreign_seeds() -> list[str]: 

82 """Cover finite productions without relying on random draws.""" 

83 return [ 

84 f"{boundary}:{identifier}:{quote}:{leaf}" 

85 for boundary in _BOUNDARIES 

86 for identifier in _IDENTIFIERS 

87 for quote in _QUOTES 

88 for leaf in _LEAVES 

89 ] 

90 

91 

92def html_foreign_controls() -> dict[str, bool]: 

93 """Expose matching errors in namespace, attachment and content.""" 

94 return { 

95 "HTML child became SVG": html_foreign_check( 

96 "foreignObject:x:double:plain", lambda node: node.serialize().replace("foreignObject", "g") 

97 ) 

98 is not None, 

99 "SVG sibling moved inside integration point": html_foreign_check( 

100 "desc:x:double:plain", 

101 lambda _node: '<svg><desc><a id="x">MiXeD</a><g id="y"></g></desc></svg>', 

102 ) 

103 is not None, 

104 "changed identifier": html_foreign_check( 

105 "title:x:double:plain", lambda node: node.serialize().replace('id="x"', 'id="y"') 

106 ) 

107 is not None, 

108 "encoded text": html_foreign_check( 

109 "foreignObject:x:double:named", lambda node: node.serialize().replace("&amp;", "&amp;amp;") 

110 ) 

111 is not None, 

112 } 

113 

114 

115class UnsupportedHtmlForeignCaseError(ValueError): 

116 """Separate invalid programs from failures of generated trees.""" 

117 

118 

119_CASE: Final = re.compile(r"(foreignObject|desc|title):(x|y):(single|double):(plain|named|numeric)") 

120_BOUNDARIES: Final = ("foreignObject", "desc", "title") 

121_IDENTIFIERS: Final = ("x", "y") 

122_QUOTES: Final = {"single": "'", "double": '"'} 

123_LEAVES: Final = { 

124 "plain": ("MiXeD", "MiXeD"), 

125 "named": ("&amp;&lt;&gt;", "&<>"), 

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

127} 

128 

129__all__ = [ 

130 "UnsupportedHtmlForeignCaseError", 

131 "html_foreign_check", 

132 "html_foreign_controls", 

133 "html_foreign_generate", 

134 "html_foreign_seeds", 

135]