Coverage for /pythoncovmergedfiles/medio/medio/src/turbohtml/tools/fuzz/iterator_oracles.py: 35%
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"""Retained iterator cursors must follow edits without replaying stale nodes."""
3from __future__ import annotations
5import json
6import re
7from typing import TYPE_CHECKING, Final, cast
9from turbohtml import Element, Node, NodeFilter, NodeIterator
11if TYPE_CHECKING:
12 import random
13 from collections.abc import Callable
16def iterator_sequence_check(case: str, run: Callable[[str, str], str] | None = None) -> str | None:
17 """Pin cursor adjustment and reinsertion against literal traversal traces."""
18 program, separator, name = case.partition(":")
19 if not separator or program not in _PROGRAMS or re.fullmatch(r"(?:[ab]|div|g[a-z0-9]{0,15})", name) is None:
20 raise UnsupportedIteratorCaseError(case)
21 return None if (run or _run)(program, name) == _expected(program, name) else "iterator sequence differs from model"
24def _run(program: str, name: str) -> str:
25 registers: Final = (Element(name), Element("a"), Element("b"), Element("c"), Element("d"))
26 root: Final = registers[0]
27 registers[1].append(registers[2])
28 for child in (registers[1], *registers[3:]):
29 root.append(child)
30 iterator: Final = NodeIterator(root, what_to_show=NodeFilter.SHOW_ELEMENT)
31 forward, back, removed_index = _PROGRAMS[program]
32 visited: Final = [_identity(iterator.next_node(), registers) for _ in range(forward)]
33 rewind: Final = [_identity(iterator.previous_node(), registers) for _ in range(back)]
34 removed: Final = registers[removed_index]
35 removed.extract()
36 adjusted: Final = (_identity(iterator.reference_node, registers), iterator.pointer_before_reference_node)
37 next_node: Final = _identity(iterator.next_node(), registers)
38 previous_node: Final = _identity(iterator.previous_node(), registers)
39 detached: Final = (removed.tag, removed.parent is None)
40 root.append(removed)
41 remaining: Final = [_identity(node, registers) for node in iterator]
42 return json.dumps((visited, rewind, adjusted, next_node, previous_node, detached, remaining, root.html))
45def _identity(node: Node | None, registers: tuple[Element, ...]) -> int | None:
46 return None if node is None else registers.index(cast("Element", node))
49def _expected(program: str, name: str) -> str:
50 traces: Final = {
51 "after": ([0, 1, 2, 3], [], (2, False), 4, 4, ("c", True), [4, 3], "<a><b></b></a><d></d><c></c>"),
52 "before": ([0, 1, 2, 3], [3], (4, True), 4, 4, ("c", True), [4, 3], "<a><b></b></a><d></d><c></c>"),
53 "last": ([0, 1, 2, 3, 4], [4], (3, False), None, 3, ("d", True), [3, 4], "<a><b></b></a><c></c><d></d>"),
54 "ancestor": ([0, 1, 2], [], (0, False), 3, 3, ("a", True), [3, 4, 1, 2], "<c></c><d></d><a><b></b></a>"),
55 }
56 *trace, children = traces[program]
57 return json.dumps((*trace, f"<{name}>{children}</{name}>"))
60def iterator_sequence_generate(rng: random.Random) -> str:
61 """Keep five wrappers and one removal per generated program."""
62 return f"{rng.choice(tuple(_PROGRAMS))}:g{rng.randrange(10000)}"
65def iterator_sequence_seeds() -> list[str]:
66 """Cover cursor directions and ancestor removal before generation."""
67 return [f"{program}:g{index}" for program in _PROGRAMS for index in range(30)]
70def iterator_sequence_controls() -> dict[str, bool]:
71 """Require exact reference states and traversal order before fuzzing."""
72 return {
73 "missing extraction": iterator_sequence_check("after:a", lambda _program, _name: "[]") is not None,
74 "stale reference": iterator_sequence_check(
75 "after:a",
76 lambda _program, _name: json.dumps((
77 [0, 1, 2, 3],
78 [],
79 (3, False),
80 4,
81 4,
82 ("c", True),
83 [4, 3],
84 "<a><a><b></b></a><d></d><c></c></a>",
85 )),
86 )
87 is not None,
88 "wrong traversal order": iterator_sequence_check(
89 "before:a",
90 lambda _program, _name: json.dumps((
91 [0, 1, 2, 3],
92 [3],
93 (4, True),
94 4,
95 4,
96 ("c", True),
97 [3, 4],
98 "<a><a><b></b></a><d></d><c></c></a>",
99 )),
100 )
101 is not None,
102 }
105class UnsupportedIteratorCaseError(ValueError):
106 """Separate malformed programs from failures during valid traversal."""
109_PROGRAMS: Final = {"after": (4, 0, 3), "before": (4, 1, 3), "last": (5, 1, 4), "ancestor": (3, 0, 1)}
111__all__ = [
112 "UnsupportedIteratorCaseError",
113 "iterator_sequence_check",
114 "iterator_sequence_controls",
115 "iterator_sequence_generate",
116 "iterator_sequence_seeds",
117]