Coverage for /pythoncovmergedfiles/medio/medio/src/turbohtml/tools/fuzz/xml_grammar_oracles.py: 38%

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

55 statements  

1"""Finite XML productions distinguish literal and referenced whitespace.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import re 

7from typing import TYPE_CHECKING, Final 

8 

9from turbohtml import Document, Element, Html, Node, Text, parse_xml 

10 

11if TYPE_CHECKING: 

12 import random 

13 from collections.abc import Callable 

14 

15 

16def xml_literal_check( 

17 case: str, 

18 serialize: Callable[[Node], str] | None = None, 

19 read: Callable[[str], Document] = parse_xml, 

20) -> str | None: 

21 """Pin XML semantics independently of a serializer or parser agreement.""" 

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

23 raise UnsupportedXmlLiteralCaseError(case) 

24 source, expected = _document(*match.groups()) 

25 root: Final = read(source) 

26 if _snapshot(root) != expected: 

27 return "parsed XML tree differs from literals" 

28 printed: Final = _serialize(root, serialize) 

29 reparsed: Final = read(printed) 

30 if _snapshot(reparsed) != expected: 

31 return "serialized XML tree differs from literals" 

32 return None if _serialize(reparsed, serialize) == printed else "XML serialization changes on repeat" 

33 

34 

35def _document(prefix: str, tag: str, quote: str, count: str, leaf: str) -> tuple[str, str]: 

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

37 delimiter: Final = _QUOTES[quote] 

38 declaration: Final = f"xmlns:{prefix}" 

39 qualified: Final = f"{prefix}:{tag}" 

40 child: Final = ( 

41 f"<{qualified} {prefix}:Key={delimiter}{encoded_attribute}{delimiter} Key={delimiter}Other{delimiter}>" 

42 f"{encoded}</{qualified}>" 

43 ) 

44 source: Final = f'<Root {declaration}="urn:{prefix}">{child * int(count)}</Root>' 

45 return source, json.dumps([ 

46 ("element", "Root", ((declaration, f"urn:{prefix}"),), "Document"), 

47 *[ 

48 record 

49 for _ in range(int(count)) 

50 for record in [ 

51 ("element", qualified, (("Key", "Other"), (f"{prefix}:Key", decoded_attribute)), "Root"), 

52 ("text", decoded, qualified), 

53 ] 

54 ], 

55 ]) 

56 

57 

58def _snapshot(root: Document) -> str: 

59 records: Final[ 

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

61 ] = [] 

62 for node in root.descendants: 

63 parent: Final = node.parent 

64 parent_tag: Final = parent.tag if isinstance(parent, Element) else "Document" 

65 if isinstance(node, Element): 

66 records.append(("element", node.tag, tuple(sorted(node.attrs.items())), parent_tag)) 

67 elif isinstance(node, Text): 

68 records.append(("text", node.data, parent_tag)) 

69 else: 

70 records.append(("other", type(node).__name__, parent_tag)) 

71 return json.dumps(records) 

72 

73 

74def _serialize(root: Node, serialize: Callable[[Node], str] | None) -> str: 

75 return root.serialize(Html(xml=True)) if serialize is None else serialize(root) 

76 

77 

78def xml_literal_generate(rng: random.Random) -> str: 

79 """Bound expansion before XML parsing.""" 

80 return ( 

81 f"{rng.choice(_PREFIXES)}:{rng.choice(_TAGS)}:{rng.choice(tuple(_QUOTES))}:" 

82 f"{rng.randint(1, 3)}:{rng.choice(tuple(_LEAVES))}" 

83 ) 

84 

85 

86def xml_literal_seeds() -> list[str]: 

87 """Cover the finite productions without random sampling gaps.""" 

88 return [ 

89 f"{prefix}:{tag}:{quote}:{count}:{leaf}" 

90 for prefix in _PREFIXES 

91 for tag in _TAGS 

92 for quote in _QUOTES 

93 for count in range(1, 4) 

94 for leaf in _LEAVES 

95 ] 

96 

97 

98def xml_literal_controls() -> dict[str, bool]: 

99 """Reject changes to names, bindings and referenced whitespace.""" 

100 return { 

101 "nested second child": xml_literal_check( 

102 "p:Leaf:double:2:plain", 

103 lambda _node: ( 

104 '<Root xmlns:p="urn:p"><p:Leaf p:Key="Value" Key="Other">MiXeD' 

105 '<p:Leaf p:Key="Value" Key="Other">MiXeD</p:Leaf></p:Leaf></Root>' 

106 ), 

107 ) 

108 is not None, 

109 "changed declaration": xml_literal_check( 

110 "p:Leaf:double:1:plain", lambda node: node.serialize(Html(xml=True)).replace("urn:p", "urn:wrong") 

111 ) 

112 is not None, 

113 "changed qualified name": xml_literal_check( 

114 "p:Leaf:double:1:plain", lambda node: node.serialize(Html(xml=True)).replace("p:Leaf", "p:leaf") 

115 ) 

116 is not None, 

117 "referenced whitespace folded": xml_literal_check( 

118 "p:Leaf:double:1:numeric", 

119 lambda _node: ( 

120 '<Root xmlns:p="urn:p"><p:Leaf p:Key="a b c d" Key="Other">a&#9;b&#10;c&#13;d</p:Leaf></Root>' 

121 ), 

122 ) 

123 is not None, 

124 } 

125 

126 

127class UnsupportedXmlLiteralCaseError(ValueError): 

128 """Separate malformed programs from failures of generated XML.""" 

129 

130 

131_CASE: Final = re.compile(r"(p|P):(Leaf|leaf):(single|double):([1-3]):(plain|escaped|literal|numeric)") 

132_PREFIXES: Final = ("p", "P") 

133_TAGS: Final = ("Leaf", "leaf") 

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

135_LEAVES: Final = { 

136 "plain": ("MiXeD", "MiXeD", "Value", "Value"), 

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

138 "literal": ("a\tb\r\nc\rd", "a\tb\nc\nd", "a\tb\r\nc\rd", "a b c d"), 

139 "numeric": ("a&#9;b&#10;c&#13;d", "a\tb\nc\rd", "a&#9;b&#10;c&#13;d", "a\tb\nc\rd"), 

140} 

141 

142__all__ = [ 

143 "UnsupportedXmlLiteralCaseError", 

144 "xml_literal_check", 

145 "xml_literal_controls", 

146 "xml_literal_generate", 

147 "xml_literal_seeds", 

148]