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
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"""Literal SVG trees prevent matching parse and reparse errors from hiding changes."""
3from __future__ import annotations
5import json
6import re
7from typing import TYPE_CHECKING, Final, cast
9from turbohtml import Element, Node, Text, parse_fragment
11if TYPE_CHECKING:
12 import random
13 from collections.abc import Callable
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 )
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)
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 )
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 ]
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;")
110 )
111 is not None,
112 }
115class UnsupportedHtmlForeignCaseError(ValueError):
116 """Separate invalid programs from failures of generated trees."""
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": ("&<>", "&<>"),
126 "numeric": ("ABC", "ABC"),
127}
129__all__ = [
130 "UnsupportedHtmlForeignCaseError",
131 "html_foreign_check",
132 "html_foreign_controls",
133 "html_foreign_generate",
134 "html_foreign_seeds",
135]