Coverage for /pythoncovmergedfiles/medio/medio/src/turbohtml/tools/fuzz/html_grammar_oracles.py: 40%
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 sibling productions pin parsed multiplicity and decoded contents."""
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_sibling_check(
17 case: str,
18 serialize: Callable[[Node], str] = Node.serialize,
19 read: Callable[[str], Element] = parse_fragment,
20) -> str | None:
21 """Compare generated literals before and after serialization."""
22 if (match := _CASE.fullmatch(case)) is None:
23 raise UnsupportedHtmlSiblingCaseError(case)
24 tag, attribute, quote, count, leaf = match.groups()
25 encoded, decoded, encoded_attribute, decoded_attribute = _LEAVES[leaf]
26 delimiter: Final = _QUOTES[quote]
27 source: Final = f"<{tag} {attribute}={delimiter}{encoded_attribute}{delimiter}>{encoded}</{tag}>" * int(count)
28 expected: Final = json.dumps([
29 record
30 for _ in range(int(count))
31 for record in [
32 ("element", tag, ((attribute, decoded_attribute),), "div"),
33 *([("text", decoded, tag)] if decoded else []),
34 ]
35 ])
36 root: Final = read(source)
37 if _snapshot(root) != expected:
38 return "parsed sibling tree differs from literals"
39 printed: Final = "".join(serialize(node) for node in root.children)
40 reparsed: Final = read(printed)
41 if _snapshot(reparsed) != expected:
42 return "serialized sibling tree differs from literals"
43 return (
44 None
45 if "".join(serialize(node) for node in reparsed.children) == printed
46 else "sibling serialization changes on repeat"
47 )
50def _snapshot(root: Element) -> str:
51 records: Final[
52 list[tuple[str, str, str] | tuple[str, str, tuple[tuple[str, str | list[str] | None], ...], str]]
53 ] = []
54 for node in root.descendants:
55 parent: Element = cast("Element", node.parent)
56 if isinstance(node, Element):
57 records.append(("element", node.tag, tuple(sorted(node.attrs.items())), parent.tag))
58 elif isinstance(node, Text):
59 records.append(("text", node.data, parent.tag))
60 else:
61 records.append(("other", type(node).__name__, parent.tag))
62 return json.dumps(records)
65def html_sibling_generate(rng: random.Random) -> str:
66 """Bound siblings and choose terminal text without recursion."""
67 return (
68 f"{rng.choice(_TAGS)}:{rng.choice(_ATTRIBUTES)}:{rng.choice(tuple(_QUOTES))}:"
69 f"{rng.randint(1, 4)}:{rng.choice(tuple(_LEAVES))}"
70 )
73def html_sibling_seeds() -> list[str]:
74 """Sweep structural, attribute and terminal productions."""
75 return [
76 f"{tag}:{attribute}:{quote}:{count}:{leaf}"
77 for tag in _TAGS
78 for attribute in _ATTRIBUTES
79 for quote in _QUOTES
80 for count in range(1, 5)
81 for leaf in _LEAVES
82 ]
85def html_sibling_controls() -> dict[str, bool]:
86 """Require multiplicity and literal attribute/text decoding."""
87 return {
88 "dropped identical sibling": html_sibling_check(
89 "span:title:bare:2:empty", lambda node: "" if node.previous_sibling is None else node.serialize()
90 )
91 is not None,
92 "changed attribute": html_sibling_check("span:title:bare:1:empty", lambda _node: '<span title="wrong"></span>')
93 is not None,
94 "encoded text": html_sibling_check(
95 "span:title:bare:1:named", lambda node: node.serialize().replace("&", "&amp;")
96 )
97 is not None,
98 }
101class UnsupportedHtmlSiblingCaseError(ValueError):
102 """Separate malformed programs from failures of valid generated trees."""
105_CASE: Final = re.compile(r"(span|em|code):(title|data-x):(single|double|bare):([1-4]):(empty|plain|named|numeric)")
106_TAGS: Final = ("span", "em", "code")
107_ATTRIBUTES: Final = ("title", "data-x")
108_QUOTES: Final = {"single": "'", "double": '"', "bare": ""}
109_LEAVES: Final = {
110 "empty": ("", "", "Token", "Token"),
111 "plain": ("MiXeD", "MiXeD", "Token", "Token"),
112 "named": ("&<>", "&<>", ""'&", "\"'&"),
113 "numeric": ("ABC", "ABC", "&"", '&"'),
114}
116__all__ = [
117 "UnsupportedHtmlSiblingCaseError",
118 "html_sibling_check",
119 "html_sibling_controls",
120 "html_sibling_generate",
121 "html_sibling_seeds",
122]