95 lines
3.0 KiB
Python
95 lines
3.0 KiB
Python
"""Fuzz tests for engine syntax — 10k random inputs.
|
|
|
|
Goals:
|
|
1. Tokenizer never crashes (any input is parseable).
|
|
2. Tokenizer is lossless: "".join(t.raw for t in tokens) == input.
|
|
3. Resolver never crashes on random input (lenient mode).
|
|
|
|
Fixed seed (0) for reproducibility — fuzz failures must reproduce.
|
|
Each iter uses fresh ctx for the resolver (RNG drift across iterations
|
|
would make output non-deterministic, breaking the fuzz contract).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
import string
|
|
from dataclasses import dataclass, field
|
|
|
|
import pytest
|
|
|
|
from engine.syntax import resolve_text, tokenize_text
|
|
from engine.syntax.types import SurfaceKind
|
|
|
|
|
|
@dataclass
|
|
class _FuzzCtx:
|
|
rng: random.Random = field(default_factory=lambda: random.Random(42))
|
|
max_ref_depth: int = 8
|
|
strict: bool = False
|
|
surface: SurfaceKind = "wildcard"
|
|
developer_mode: bool = False
|
|
warnings: list = field(default_factory=list)
|
|
_vars: dict[str, str] = field(default_factory=dict)
|
|
_modules: dict[str, dict] = field(default_factory=dict)
|
|
|
|
def get_var(self, name: str) -> str | None:
|
|
return self._vars.get(name)
|
|
|
|
def get_module(self, uuid: str) -> dict | None:
|
|
return self._modules.get(uuid)
|
|
|
|
|
|
def _wc(uuid: str, var_binding: str) -> dict:
|
|
return {
|
|
"type": "wildcard",
|
|
"var_binding": var_binding,
|
|
"options": [{"value": "leaf", "weight": 1}],
|
|
}
|
|
|
|
|
|
def _random_input(rng: random.Random, max_len: int = 200) -> str:
|
|
"""Generate input mixing literal text + valid + malformed syntax fragments."""
|
|
alphabet = string.ascii_letters + string.digits + " ${}@|*#$, \n\t"
|
|
return "".join(rng.choices(alphabet, k=rng.randint(0, max_len)))
|
|
|
|
|
|
@pytest.mark.fuzz
|
|
def test_tokenize_does_not_crash():
|
|
rng = random.Random(0) # deterministic seed → reproducible failures
|
|
for i in range(10_000):
|
|
s = _random_input(rng)
|
|
try:
|
|
tokens = tokenize_text(s)
|
|
except Exception as e:
|
|
pytest.fail(f"tokenize_text crashed on {s!r} (iter {i}): {e!r}")
|
|
# Tokenizer is lossless: concatenated raws == input
|
|
joined = "".join(t.raw for t in tokens)
|
|
assert joined == s, (
|
|
f"lossless invariant broken on {s!r} (iter {i}): got {joined!r}"
|
|
)
|
|
|
|
|
|
@pytest.mark.fuzz
|
|
def test_resolve_does_not_crash():
|
|
"""Resolver in lenient mode must never raise on random input.
|
|
|
|
Each iter gets fresh ctx so RNG state doesn't drift across iterations.
|
|
"""
|
|
from engine.syntax.types import SyntaxError as EngineSyntaxError
|
|
|
|
rng = random.Random(0)
|
|
catalog = {f"u{i:08x}": _wc(f"u{i:08x}", f"v{i}") for i in range(8)}
|
|
for i in range(10_000):
|
|
s = _random_input(rng)
|
|
ctx = _FuzzCtx(
|
|
rng=random.Random(42), # FRESH per iter
|
|
strict=False,
|
|
_modules=catalog,
|
|
)
|
|
try:
|
|
resolve_text(s, ctx)
|
|
except EngineSyntaxError:
|
|
pass # known engine errors are fine
|
|
except Exception as e:
|
|
pytest.fail(f"resolve_text crashed on {s!r} (iter {i}): {e!r}")
|