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

48 statements  

1"""Retained iterator cursors must follow edits without replaying stale nodes.""" 

2 

3from __future__ import annotations 

4 

5import json 

6import re 

7from typing import TYPE_CHECKING, Final, cast 

8 

9from turbohtml import Element, Node, NodeFilter, NodeIterator 

10 

11if TYPE_CHECKING: 

12 import random 

13 from collections.abc import Callable 

14 

15 

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" 

22 

23 

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)) 

43 

44 

45def _identity(node: Node | None, registers: tuple[Element, ...]) -> int | None: 

46 return None if node is None else registers.index(cast("Element", node)) 

47 

48 

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}>")) 

58 

59 

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)}" 

63 

64 

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)] 

68 

69 

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 } 

103 

104 

105class UnsupportedIteratorCaseError(ValueError): 

106 """Separate malformed programs from failures during valid traversal.""" 

107 

108 

109_PROGRAMS: Final = {"after": (4, 0, 3), "before": (4, 1, 3), "last": (5, 1, 4), "ancestor": (3, 0, 1)} 

110 

111__all__ = [ 

112 "UnsupportedIteratorCaseError", 

113 "iterator_sequence_check", 

114 "iterator_sequence_controls", 

115 "iterator_sequence_generate", 

116 "iterator_sequence_seeds", 

117]