|
| 1 | +"""Fuzz/property tests for generic-profile delta (mirrors gcf-go FuzzGeneric*). |
| 2 | +
|
| 3 | +Two properties: |
| 4 | + A. decode_generic_delta / decode_generic_full never crash on arbitrary or |
| 5 | + mutated input (they fail closed with a controlled error, or return). |
| 6 | + B. arbitrary string cell values survive the full-wire round-trip |
| 7 | + (quoting/escaping) with the pack root preserved. |
| 8 | +""" |
| 9 | +import random |
| 10 | + |
| 11 | +from gcf.generic_delta import ( |
| 12 | + GenericSet, |
| 13 | + decode_generic_delta, |
| 14 | + decode_generic_full, |
| 15 | + encode_generic_full, |
| 16 | + generic_pack_root, |
| 17 | +) |
| 18 | + |
| 19 | +# structural + delimiter + unicode chars, the ones that stress quoting/escaping |
| 20 | +ALPHABET = list("abcXYZ0129 .,-~^@#=|\t\n\r\"\\/éñ中🦞") |
| 21 | + |
| 22 | + |
| 23 | +def _rand_str(rng, maxlen=20): |
| 24 | + return "".join(rng.choice(ALPHABET) for _ in range(rng.randint(0, maxlen))) |
| 25 | + |
| 26 | + |
| 27 | +def test_fuzz_string_cell_roundtrip(): |
| 28 | + rng = random.Random(1234) |
| 29 | + for _ in range(20000): |
| 30 | + a, b = _rand_str(rng), _rand_str(rng) |
| 31 | + s = GenericSet(key="id", name="t", fields=["id", "a", "b"], rows=[ |
| 32 | + {"id": 1, "a": a, "b": b}, |
| 33 | + {"id": 2, "a": b, "b": a}, |
| 34 | + ]) |
| 35 | + got, _ = decode_generic_full(encode_generic_full(s, "")) |
| 36 | + assert generic_pack_root(got) == generic_pack_root(s), repr((a, b)) |
| 37 | + |
| 38 | + |
| 39 | +def test_fuzz_decode_never_crashes(): |
| 40 | + rng = random.Random(99) |
| 41 | + seeds = [ |
| 42 | + "GCF profile=generic delta=true base_root=a new_root=b key=id\n## added [1]{@id,x}\n1|2\n", |
| 43 | + "GCF profile=generic pack_root=r key=id\n## t [2]{@id,x}\n1|2\n3|4\n", |
| 44 | + "## removed [1]{@id}\n99\n", |
| 45 | + "", |
| 46 | + ] |
| 47 | + for _ in range(20000): |
| 48 | + if rng.random() < 0.5: |
| 49 | + data = _rand_str(rng, 80) |
| 50 | + else: |
| 51 | + chars = list(rng.choice(seeds)) |
| 52 | + for _ in range(rng.randint(0, 5)): |
| 53 | + if chars: |
| 54 | + chars[rng.randrange(len(chars))] = rng.choice(ALPHABET) |
| 55 | + data = "".join(chars) |
| 56 | + for fn in (decode_generic_delta, decode_generic_full): |
| 57 | + try: |
| 58 | + fn(data) # controlled error is fine; must terminate, must not crash |
| 59 | + except Exception: |
| 60 | + pass |
0 commit comments