Coverage for /pythoncovmergedfiles/medio/medio/src/turbohtml/tools/fuzz/xml_island_oracles.py: 37%
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 islands pin lexical node kinds and attachment."""
3from __future__ import annotations
5import re
6from typing import TYPE_CHECKING, Final
8from turbohtml import CData, Comment, Document, Element, Html, Node, Text, parse_xml
10if TYPE_CHECKING:
11 import random
12 from collections.abc import Callable
15def xml_island_check(
16 case: str,
17 serialize: Callable[[Node], str] | None = None,
18 read: Callable[[str], Document] = parse_xml,
19) -> str | None:
20 """Keep literal node kinds independent of round-trip agreement."""
21 if (match := _CASE.fullmatch(case)) is None:
22 raise UnsupportedXmlIslandCaseError(case)
23 source, expected = _document(*match.groups())
24 root: Final = read(source)
25 if _snapshot(root) != expected:
26 return "parsed XML islands differ from literals"
27 printed: Final = _serialize(root, serialize)
28 reparsed: Final = read(printed)
29 if _snapshot(reparsed) != expected:
30 return "serialized XML islands differ from literals"
31 return None if _serialize(reparsed, serialize) == printed else "XML island serialization changes on repeat"
34def _document(mode: str, comment: str, cdata: str, count: str) -> tuple[str, tuple[tuple[str, str, str], ...]]:
35 encoded_comment, decoded_comment = _COMMENTS[comment]
36 encoded_cdata, decoded_cdata = _CDATA[cdata]
37 group: Final = f"<!--{encoded_comment}--><![CDATA[{encoded_cdata}]]><&"
38 prefix: Final = "<!--lead &-->" if mode == "outside" else ""
39 suffix: Final = "<!--tail-->" if mode == "outside" else ""
40 return prefix + "<Root>" + group * int(count) + "</Root>" + suffix, (
41 *([("Comment", "lead &", "Document")] if mode == "outside" else []),
42 ("Element", "Root", "Document"),
43 *[
44 record
45 for _ in range(int(count))
46 for record in [
47 ("Comment", decoded_comment, "Root"),
48 ("CData", decoded_cdata, "Root"),
49 ("Text", "<&", "Root"),
50 ]
51 ],
52 *([("Comment", "tail", "Document")] if mode == "outside" else []),
53 )
56def _snapshot(root: Document) -> tuple[tuple[str, str, str], ...]:
57 records: Final[list[tuple[str, str, str]]] = []
58 for node in root.descendants:
59 parent: Final = node.parent
60 parent_tag: Final = parent.tag if isinstance(parent, Element) else "Document"
61 if isinstance(node, Element):
62 records.append(("Element", node.tag, parent_tag))
63 elif isinstance(node, (Comment, CData, Text)):
64 records.append((type(node).__name__, node.data, parent_tag))
65 else:
66 records.append(("other", type(node).__name__, parent_tag))
67 return tuple(records)
70def _serialize(root: Node, serialize: Callable[[Node], str] | None) -> str:
71 return root.serialize(Html(xml=True)) if serialize is None else serialize(root)
74def xml_island_generate(rng: random.Random) -> str:
75 """Bound lexical groups before parsing."""
76 return f"{rng.choice(_MODES)}:{rng.choice(tuple(_COMMENTS))}:{rng.choice(tuple(_CDATA))}:{rng.randint(1, 3)}"
79def xml_island_seeds() -> list[str]:
80 """Cover the finite productions without sampling gaps."""
81 return [
82 f"{mode}:{comment}:{cdata}:{count}"
83 for mode in _MODES
84 for comment in _COMMENTS
85 for cdata in _CDATA
86 for count in range(1, 4)
87 ]
90def xml_island_controls() -> dict[str, bool]:
91 """Reject content-preserving kind loss and changed comment attachment."""
92 return {
93 "CDATA converted to Text": xml_island_check(
94 "inside:plain:plain:1", lambda _node: "<Root><!--note-->Value<&</Root>"
95 )
96 is not None,
97 "comment moved into Root": xml_island_check(
98 "outside:plain:plain:1",
99 lambda _node: "<!--lead &--><Root><!--note--><![CDATA[Value]]><&<!--tail--></Root>",
100 )
101 is not None,
102 "CDATA reference decoded": xml_island_check(
103 "inside:plain:markup:1", lambda node: node.serialize(Html(xml=True)).replace("&]]>", "&]]>")
104 )
105 is not None,
106 }
109class UnsupportedXmlIslandCaseError(ValueError):
110 """Separate malformed programs from failures of generated XML."""
113_CASE: Final = re.compile(r"(inside|outside):(plain|markup|lines):(plain|markup|lines):([1-3])")
114_MODES: Final = ("inside", "outside")
115_COMMENTS: Final = {"plain": ("note", "note"), "markup": ("<x> &", "<x> &"), "lines": ("a\r\nb\rc", "a\nb\nc")}
116_CDATA: Final = {"plain": ("Value", "Value"), "markup": ("<x>&", "<x>&"), "lines": ("a\r\nb\rc", "a\nb\nc")}
118__all__ = [
119 "UnsupportedXmlIslandCaseError",
120 "xml_island_check",
121 "xml_island_controls",
122 "xml_island_generate",
123 "xml_island_seeds",
124]