Skip to content

Commit 6d3a021

Browse files
bluetoothbotclaude
andcommitted
test(gap): make fuzz test deterministic and cover all parse entry points
Replace ad-hoc random fuzz with a seeded random.Random so any crash is reproducible from the CI log alone, drop the per-iteration print() that flooded test output, and exercise every public parse entry point (not just _uncached_parse_advertisement_data) plus a targeted truncated-length case that overruns the buffer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c677de6 commit 6d3a021

1 file changed

Lines changed: 47 additions & 10 deletions

File tree

tests/test_gap_fuzzer.py

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,52 @@
1+
"""Deterministic fuzz coverage for the GAP parser.
2+
3+
Generates pseudo-random advertisement payloads with a fixed seed so any
4+
crash is reproducible from a CI log alone, and exercises every public
5+
parse entry point so failures aren't hidden behind a single code path.
6+
"""
7+
18
import random
29

10+
from bluetooth_data_tools import (
11+
parse_advertisement_data,
12+
parse_advertisement_data_bytes,
13+
parse_advertisement_data_tuple,
14+
)
315
from bluetooth_data_tools.gap import _uncached_parse_advertisement_data
416

17+
_FUZZ_SEED = 0xB1EC0FFEE
18+
_FUZZ_ITERATIONS = 1000
19+
20+
21+
def _random_chunks(rng: random.Random) -> tuple[bytes, ...]:
22+
return tuple(
23+
bytes(rng.randint(0, 255) for _ in range(rng.randint(1, 31)))
24+
for _ in range(rng.randint(1, 3))
25+
)
26+
27+
28+
def test_gap_fuzzer_random_bytes_do_not_crash() -> None:
29+
rng = random.Random(_FUZZ_SEED)
30+
for _ in range(_FUZZ_ITERATIONS):
31+
chunks = _random_chunks(rng)
32+
joined = b"".join(chunks)
33+
# Every public entry point must tolerate arbitrary bytes without raising.
34+
_uncached_parse_advertisement_data(joined)
35+
parse_advertisement_data_bytes(joined)
36+
parse_advertisement_data_tuple(chunks)
37+
parse_advertisement_data(chunks)
38+
539

6-
def test_gap_fuzzer() -> None:
7-
"""Test random data does not crash."""
8-
for i in range(1000):
9-
adv = (
10-
bytes([random.randint(0, 255) for _ in range(random.randint(1, 31))]),
11-
bytes([random.randint(0, 255) for _ in range(random.randint(1, 31))]),
12-
bytes([random.randint(0, 255) for _ in range(random.randint(1, 31))]),
13-
)
14-
print([i, adv])
15-
_uncached_parse_advertisement_data(b"".join(adv))
40+
def test_gap_fuzzer_truncated_length_does_not_crash() -> None:
41+
"""An AD struct that claims more bytes than remain must be rejected, not read."""
42+
rng = random.Random(_FUZZ_SEED ^ 0x1)
43+
for _ in range(200):
44+
# Claim a length that overruns the buffer, then provide a smaller body.
45+
body = bytes(rng.randint(0, 255) for _ in range(rng.randint(0, 5)))
46+
# length byte = 100 (huge), random type, short body
47+
payload = bytes([100, rng.randint(1, 0xFF)]) + body
48+
adv = parse_advertisement_data((payload,))
49+
assert adv.local_name is None
50+
assert adv.manufacturer_data == {}
51+
assert adv.service_data == {}
52+
assert adv.service_uuids == []

0 commit comments

Comments
 (0)