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
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"""Finite XML productions distinguish literal and referenced whitespace."""
3from __future__ import annotations
5import json
6import re
7from typing import TYPE_CHECKING, Final
9from turbohtml import Document, Element, Html, Node, Text, parse_xml
11if TYPE_CHECKING:
12 import random
13 from collections.abc import Callable
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"
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 ])
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)
74def _serialize(root: Node, serialize: Callable[[Node], str] | None) -> str:
75 return root.serialize(Html(xml=True)) if serialize is None else serialize(root)
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 )
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 ]
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	b c d</p:Leaf></Root>'
121 ),
122 )
123 is not None,
124 }
127class UnsupportedXmlLiteralCaseError(ValueError):
128 """Separate malformed programs from failures of generated XML."""
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": ("&<>", "&<>", ""'&<", "\"'&<"),
138 "literal": ("a\tb\r\nc\rd", "a\tb\nc\nd", "a\tb\r\nc\rd", "a b c d"),
139 "numeric": ("a	b c d", "a\tb\nc\rd", "a	b c d", "a\tb\nc\rd"),
140}
142__all__ = [
143 "UnsupportedXmlLiteralCaseError",
144 "xml_literal_check",
145 "xml_literal_controls",
146 "xml_literal_generate",
147 "xml_literal_seeds",
148]