Fold Tier C surface into existing fuzz targets; closes #312

Extends the five existing targets to cover the remaining gaps from #312
without new files:

- property_types     Line/Circle/Arc setters, Scene.children collection ops
                     (index/count/find/insert/slice/pop), module functions
                     find/find_all/bresenham/lock. Benchmark triplet excluded
                     (end_benchmark writes a file per call).
- grid_entity        grid.at / [x,y] / entities_in_radius / center_camera /
                     hovered_cell, and GridPoint named-layer __getattr__/
                     __setattr__.
- pathfinding_behavior  Grid.find_path + full AStarPath (peek/__len__/__bool__/
                     iteration) that path_from didn't reach.
- fov                ColorLayer perspective (apply/update/clear_perspective)
                     and draw_fov.
- maps_procgen       ColorLayer/TileLayer apply_threshold/apply_ranges/
                     apply_gradient from HeightMap sources.

The full instrumented campaign surfaced five new bugs, filed as #321 (HIGH
ColorLayer.draw_fov bad-free), #322 (WangSet.terrain_enum error-pending
abort), #323/#324/#325 (float->int UB in pitch_shift/hsl_shift/Vector). Per
decision, this issue delivers fuzz coverage only; the bugs are tracked
separately.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
This commit is contained in:
John McCardle 2026-06-21 16:45:03 -04:00
commit 246ed886db
6 changed files with 466 additions and 7 deletions

View file

@ -48,7 +48,7 @@ The libFuzzer+ASan harness (#283) has nine work tranches merged: build plumbing
- `fuzz_maps_procgen` -- HeightMap/DiscreteMap interfaces (W7) - `fuzz_maps_procgen` -- HeightMap/DiscreteMap interfaces (W7)
- `fuzz_pathfinding_behavior` -- Dijkstra + turn manager (W9, fixed #311) - `fuzz_pathfinding_behavior` -- Dijkstra + turn manager (W9, fixed #311)
The active tier1 queue is empty. The last three findings (#309 Caption float→uint, #310 FOV enum, #311 DijkstraMap OOB) all landed on master in mid-April. Coverage extension to remaining public API surface is tracked under #312. Coverage extension (#312) added four more: `fuzz_audio_dsp` (SoundBuffer DSP), `fuzz_import_parsers` (Tiled/LDtk file parsers), `fuzz_texture_factory` (byte ingestion), `fuzz_shader_bindings` (uniform-binding lifetime), plus Tier C surface folded into the existing targets. That run found five new bugs: #321 (HIGH -- ColorLayer.draw_fov bad-free), #322 (WangSet.terrain_enum error-pending abort), #323/#324/#325 (float→int UB in pitch_shift/hsl_shift/Vector).
### Recently Shipped (April 2026) ### Recently Shipped (April 2026)
- **#294** -- `entity.perspective_map` replaces flat `vector<UIGridPointState>` with a 3-state DiscreteMap (UNKNOWN/DISCOVERED/VISIBLE). Per-entity FOV memory is now serializable, swappable, and structurally enforces visible-as-subset-of-discovered. - **#294** -- `entity.perspective_map` replaces flat `vector<UIGridPointState>` with a 3-state DiscreteMap (UNKNOWN/DISCOVERED/VISIBLE). Per-entity FOV memory is now serializable, swappable, and structurally enforces visible-as-subset-of-discovered.
@ -57,6 +57,7 @@ The active tier1 queue is empty. The last three findings (#309 Caption float→u
- **Phase 5.3** -- documentation regenerated; `tools/generate_stubs_v2.py` rewritten as introspection-based so it can no longer drift from the C++ source. - **Phase 5.3** -- documentation regenerated; `tools/generate_stubs_v2.py` rewritten as introspection-based so it can no longer drift from the C++ source.
### Recently Shipped (June 2026) ### Recently Shipped (June 2026)
- **#312** -- Fuzz coverage extended to the remaining public API surface. Four new libFuzzer targets (`fuzz_audio_dsp`, `fuzz_import_parsers`, `fuzz_texture_factory`, `fuzz_shader_bindings`) cover the Tier A/B gaps (external file parsers, audio DSP math, raw-byte texture ingestion, shader uniform-binding lifetime); Tier C surface (Line/Circle/Arc, `Scene.children` collections, `find`/`find_all`/`bresenham`/`lock`, grid spatial queries, GridPoint dynamic attrs, `Grid.find_path`+AStarPath, ColorLayer perspective/`draw_fov`, layer `apply_*`) folded into the five existing targets. Each new target is signature-validated against the live API and seeded from real fixtures. The campaign immediately found **five new bugs** -- filed #321-#325 (no fixes this round; targets only). A pre-existing infra fix rode along: `tools/build_debug_libs.sh` flag-quoting bug that broke instrumented debug-lib rebuilds. The benchmark triplet is deliberately excluded from fuzzing (`end_benchmark()` writes a file per call).
- **#320** -- `Caption` constructor positional signature now matches its frozen docstring. The docstring advertised `Caption(pos, font, text, ...)` (parallel to `Sprite`/`Entity`, whose 2nd positional is the resource), but the implementation laid its two positional slots out as `(pos, text)` with `font` keyword-only, so `Caption((x,y), None, "text")` raised `TypeError`. Fixed `UICaption::init` to `(pos, font, text)` positional-or-keyword. Audited zero live callers of the old `(pos, text)` 2-positional form. Also added the matching read-only `Caption.font` getter (the class docstring listed `font` as an attribute but no getter existed; it now reflects the supplied or engine-default font). Also rewrote two stale unit tests (`test_animation_raii`, `test_animation_property_locking`) that called the removed `mcrfpy.Animation(...)` constructor to use `drawable.animate(...)` -- preserving the suite's only `conflict_mode` (#120) coverage and the weak-target RAII checks. Suite now 297/297. - **#320** -- `Caption` constructor positional signature now matches its frozen docstring. The docstring advertised `Caption(pos, font, text, ...)` (parallel to `Sprite`/`Entity`, whose 2nd positional is the resource), but the implementation laid its two positional slots out as `(pos, text)` with `font` keyword-only, so `Caption((x,y), None, "text")` raised `TypeError`. Fixed `UICaption::init` to `(pos, font, text)` positional-or-keyword. Audited zero live callers of the old `(pos, text)` 2-positional form. Also added the matching read-only `Caption.font` getter (the class docstring listed `font` as an attribute but no getter existed; it now reflects the supplied or engine-default font). Also rewrote two stale unit tests (`test_animation_raii`, `test_animation_property_locking`) that called the removed `mcrfpy.Animation(...)` constructor to use `drawable.animate(...)` -- preserving the suite's only `conflict_mode` (#120) coverage and the weak-target RAII checks. Suite now 297/297.
- **#317 / #318 / #319** -- The three code-level bugs surfaced by the #314 docstring-accuracy verify pass, fixed together. #317: `automation.scroll()` dropped the x of its position argument (the scroll delta now has its own `injectMouseEvent` parameter, so the real x/y is forwarded). #318: `GridView.texture` always returned `None` (a TODO stub) -- it now returns a `Texture` wrapper (and since `mcrfpy.Grid`/`mcrfpy.GridView` are one type post-#252, both names benefit). #319: `Entity.visible_entities(radius=None)` raised `TypeError` (the `i` format code rejects `None`) -- radius is now parsed as an object so `None`/omitted/`-1` mean "grid default". Regression tests for each; api-surface snapshot re-baselined and docs/stubs regenerated. - **#317 / #318 / #319** -- The three code-level bugs surfaced by the #314 docstring-accuracy verify pass, fixed together. #317: `automation.scroll()` dropped the x of its position argument (the scroll delta now has its own `injectMouseEvent` parameter, so the real x/y is forwarded). #318: `GridView.texture` always returned `None` (a TODO stub) -- it now returns a `Texture` wrapper (and since `mcrfpy.Grid`/`mcrfpy.GridView` are one type post-#252, both names benefit). #319: `Entity.visible_entities(radius=None)` raised `TypeError` (the `i` format code rejects `None`) -- radius is now parsed as an object so `None`/omitted/`-1` mean "grid default". Regression tests for each; api-surface snapshot re-baselined and docs/stubs regenerated.
- **#316** -- Sparse (windowed) perspective writeback in `UIEntity::updateVisibility`. The demote+promote passes are now clipped to an AABB sized to `fov_radius` (with a `prev_fov` window cache so a moving entity leaves no trailing "ghost vision"), replacing two full-`W*H` walks per entity. The Phase 5.2 benchmark's flat ~25-36 ms/entity writeback overhead on a 1000x1000 grid collapses to single-digit microseconds (384x-6577x on the cheap algorithms; lost in timing noise on the rest). Adversarial verify caught a regression the happy-path test missed -- externally-assigned maps (the documented `from_bytes` load/resume path) need a one-shot full demote (`perspective_full_demote_pending`) since `prev_fov` only bounds engine-promoted cells; fixed and locked with a 7-section regression test. - **#316** -- Sparse (windowed) perspective writeback in `UIEntity::updateVisibility`. The demote+promote passes are now clipped to an AABB sized to `fov_radius` (with a `prev_fov` window cache so a moving entity leaves no trailing "ghost vision"), replacing two full-`W*H` walks per entity. The Phase 5.2 benchmark's flat ~25-36 ms/entity writeback overhead on a 1000x1000 grid collapses to single-digit microseconds (384x-6577x on the cheap algorithms; lost in timing noise on the rest). Adversarial verify caught a regression the happy-path test missed -- externally-assigned maps (the documented `from_bytes` load/resume path) need a one-shot full demote (`perspective_full_demote_pending`) since `prev_fov` only bounds engine-promoted cells; fixed and locked with a 7-section regression test.
@ -64,7 +65,7 @@ The active tier1 queue is empty. The last three findings (#309 Caption float→u
- **#314** -- API audit follow-through complete. (1) Snapshot lock: a public API-surface regression test (`tests/unit/api_surface_snapshot_test.py`) enshrines the frozen contract. (2) **F15**: all 289 raw docstring slots across the 20 frozen binding files converted to `MCRF_*` macros (frozen surface 100% compliant), driven by two one-agent-per-file workflows with build/doc gates and an adversarial signature-accuracy verify pass. Property types now resolve to real types (not `Any`) and read-only flags are correct. (3) A strict frozen-docstring gate (`tools/check_frozen_docstrings.sh`, wired into `generate_all_docs.sh`) locks it against regression. Breaking-change findings (F1/F4/F6/F11/F13) closed earlier; F7/F8/F10 deferred as non-1.0. Code-level bugs surfaced by the verify pass filed as #317/#318/#319. - **#314** -- API audit follow-through complete. (1) Snapshot lock: a public API-surface regression test (`tests/unit/api_surface_snapshot_test.py`) enshrines the frozen contract. (2) **F15**: all 289 raw docstring slots across the 20 frozen binding files converted to `MCRF_*` macros (frozen surface 100% compliant), driven by two one-agent-per-file workflows with build/doc gates and an adversarial signature-accuracy verify pass. Property types now resolve to real types (not `Any`) and read-only flags are correct. (3) A strict frozen-docstring gate (`tools/check_frozen_docstrings.sh`, wired into `generate_all_docs.sh`) locks it against regression. Breaking-change findings (F1/F4/F6/F11/F13) closed earlier; F7/F8/F10 deferred as non-1.0. Code-level bugs surfaced by the verify pass filed as #317/#318/#319.
### Active Follow-Ups ### Active Follow-Ups
- **#312** Extend fuzz coverage to remaining public API surface - **#321** (HIGH) ColorLayer.draw_fov bad-free; **#322** WangSet.terrain_enum error-pending abort; **#323/#324/#325** float→int UB (pitch_shift/hsl_shift/Vector) -- all surfaced by the #312 fuzz run, filed but not yet fixed.
### Other Post-7DRL Priorities ### Other Post-7DRL Priorities
- Progress on the r/roguelikedev tutorial series (#167) - Progress on the r/roguelikedev tutorial series (#167)

View file

@ -59,6 +59,78 @@ def _make_grid(stream):
return grid, w, h return grid, w, h
def _color_or_none(stream):
"""Return a (possibly out-of-range) RGBA tuple, or None, for color args."""
if stream.bool():
return None
return (stream.int_in_range(-20, 300), stream.int_in_range(-20, 300),
stream.int_in_range(-20, 300), stream.int_in_range(-20, 300))
def _grid_with_color_layer(stream, name):
"""Build a small grid with one named ColorLayer. Returns (grid, layer, w, h)
or (None, None, 0, 0) if construction failed."""
w = stream.int_in_range(2, 16)
h = stream.int_in_range(2, 16)
try:
grid = mcrfpy.Grid(grid_size=(w, h))
layer = mcrfpy.ColorLayer(name=name, z_index=0)
grid.add_layer(layer)
except EXPECTED_EXCEPTIONS:
return None, None, 0, 0
return grid, layer, w, h
def _fuzz_perspective(stream):
"""ColorLayer.apply_perspective / update_perspective / clear_perspective."""
grid, layer, w, h = _grid_with_color_layer(stream, "persp")
if layer is None:
return
ent = None
try:
ent = mcrfpy.Entity(grid_pos=(stream.int_in_range(0, w - 1),
stream.int_in_range(0, h - 1)), grid=grid)
ent.sight_radius = stream.int_in_range(-2, 20)
except EXPECTED_EXCEPTIONS:
pass
# Sometimes pass a non-entity to hit the type-check path.
target = ent if (ent is not None and stream.bool()) else stream.pick_one((None, "bad", ent))
try:
layer.apply_perspective(target, _color_or_none(stream),
_color_or_none(stream), _color_or_none(stream))
except EXPECTED_EXCEPTIONS:
pass
try:
layer.update_perspective()
except EXPECTED_EXCEPTIONS:
pass
try:
layer.clear_perspective()
except EXPECTED_EXCEPTIONS:
pass
def _fuzz_draw_fov(stream):
"""ColorLayer.draw_fov(source, radius, fov, visible, discovered, unknown)."""
grid, layer, w, h = _grid_with_color_layer(stream, "fov")
if layer is None:
return
source = (stream.int_in_range(-3, w + 3), stream.int_in_range(-3, h + 3))
kw = {}
if stream.bool():
kw["radius"] = stream.int_in_range(-2, 40)
if stream.bool():
kw["fov"] = stream.pick_one(_FOV_MEMBERS)
if stream.bool():
kw["visible"] = _color_or_none(stream)
if stream.bool():
kw["discovered"] = _color_or_none(stream)
try:
layer.draw_fov(source, **kw)
except EXPECTED_EXCEPTIONS:
pass
def fuzz_one_input(data): def fuzz_one_input(data):
stream = ByteStream(data) stream = ByteStream(data)
try: try:
@ -68,7 +140,7 @@ def fuzz_one_input(data):
for _ in range(n_ops): for _ in range(n_ops):
if stream.remaining < 1: if stream.remaining < 1:
break break
op = stream.u8() % 16 op = stream.u8() % 18
try: try:
if op == 0: if op == 0:
# Replace the active grid (drop the old one). # Replace the active grid (drop the old one).
@ -246,7 +318,7 @@ def fuzz_one_input(data):
y = stream.int_in_range(0, max(0, h - 1)) y = stream.int_in_range(0, max(0, h - 1))
grid.compute_fov((x, y), radius=5, algorithm="basic") grid.compute_fov((x, y), radius=5, algorithm="basic")
else: # op == 15 elif op == 15:
# is_in_fov garbage args. # is_in_fov garbage args.
bad_choice = stream.u8() % 3 bad_choice = stream.u8() % 3
if bad_choice == 0: if bad_choice == 0:
@ -256,6 +328,16 @@ def fuzz_one_input(data):
else: else:
_ = grid.is_in_fov((1, 2, 3)) _ = grid.is_in_fov((1, 2, 3))
elif op == 16:
# Tier C (#312): ColorLayer perspective system. Build a
# self-contained grid + ColorLayer + Entity so the entity
# visibility-perspective path resolves to real objects.
_fuzz_perspective(stream)
else: # op == 17
# Tier C (#312): ColorLayer.draw_fov from a source cell.
_fuzz_draw_fov(stream)
except EXPECTED_EXCEPTIONS: except EXPECTED_EXCEPTIONS:
pass pass
except EXPECTED_EXCEPTIONS: except EXPECTED_EXCEPTIONS:

View file

@ -264,6 +264,93 @@ def _op_die(stream, grids, entities):
# the next op that touches it should hit defensive paths. # the next op that touches it should hit defensive paths.
def _op_grid_query(stream, grids, entities):
"""Tier C (#312): grid spatial-query surface not covered elsewhere --
at(), grid[x,y] subscript, entities_in_radius, center_camera, hovered_cell.
Coords intentionally stray out of bounds to exercise the guards.
"""
grid = _pick_grid(stream, grids)
if grid is None:
return
which = stream.u8() % 5
if which == 0:
x = stream.int_in_range(-3, MAX_GRID_DIM + 3)
y = stream.int_in_range(-3, MAX_GRID_DIM + 3)
gp = grid.at(x, y)
_ = gp.walkable
_ = gp.transparent
_ = gp.grid_pos
_ = gp.entities
elif which == 1:
x = stream.int_in_range(-3, MAX_GRID_DIM + 3)
y = stream.int_in_range(-3, MAX_GRID_DIM + 3)
_ = grid[x, y]
elif which == 2:
pos = (stream.int_in_range(-3, MAX_GRID_DIM + 3),
stream.int_in_range(-3, MAX_GRID_DIM + 3))
grid.entities_in_radius(pos, stream.float_in_range(-1.0, 24.0))
elif which == 3:
if stream.bool():
grid.center_camera((stream.float_in_range(-5.0, 40.0),
stream.float_in_range(-5.0, 40.0)))
else:
grid.center_camera()
else:
_ = grid.hovered_cell
def _op_gridpoint_attrs(stream, grids, entities):
"""Tier C (#312): GridPoint __getattr__/__setattr__ named-layer access.
Ensures the grid has a named ColorLayer + TileLayer so the dynamic
attribute path (UIGridPoint::getattro/setattro) resolves to real layers,
then exercises built-in props, valid layer writes, a bogus layer name, and
a wrong-typed tile write.
"""
grid = _pick_grid(stream, grids)
if grid is None:
return
try:
if len(grid.layers) == 0:
grid.add_layer(mcrfpy.ColorLayer(name="fuzzcolor", z_index=0))
grid.add_layer(mcrfpy.TileLayer(name="fuzztile", z_index=-1))
except EXPECTED_EXCEPTIONS:
pass
x = stream.int_in_range(0, MAX_GRID_DIM - 1)
y = stream.int_in_range(0, MAX_GRID_DIM - 1)
if (stream.u8() & 0x07) == 0:
x = stream.int_in_range(-3, MAX_GRID_DIM + 3)
y = stream.int_in_range(-3, MAX_GRID_DIM + 3)
try:
gp = grid.at(x, y)
except EXPECTED_EXCEPTIONS:
return
for setter in (("walkable", stream.bool()), ("transparent", stream.bool())):
try:
setattr(gp, setter[0], setter[1])
except EXPECTED_EXCEPTIONS:
pass
for name in ("fuzzcolor", "fuzztile", "nonexistent_layer"):
try:
_ = getattr(gp, name)
except EXPECTED_EXCEPTIONS:
pass
try:
gp.fuzzcolor = (stream.int_in_range(-20, 300),
stream.int_in_range(-20, 300),
stream.int_in_range(-20, 300))
except EXPECTED_EXCEPTIONS:
pass
try:
gp.fuzztile = stream.int_in_range(-5, 4096)
except EXPECTED_EXCEPTIONS:
pass
try:
gp.fuzztile = "not an int" # wrong type -> TypeError path
except EXPECTED_EXCEPTIONS:
pass
def _op_iterate_and_mutate(stream, grids, entities): def _op_iterate_and_mutate(stream, grids, entities):
"""Iterate grid.entities and mid-loop call die() or reassign grid. """Iterate grid.entities and mid-loop call die() or reassign grid.
@ -326,6 +413,8 @@ _OPS = [
_op_set_grid_none, # 11 _op_set_grid_none, # 11
_op_die, # 12 _op_die, # 12
_op_iterate_and_mutate, # 13 _op_iterate_and_mutate, # 13
_op_grid_query, # 14 (Tier C #312)
_op_gridpoint_attrs, # 15 (Tier C #312)
] ]

View file

@ -508,8 +508,64 @@ def _op_bsp_walk(stream, bsps):
pass pass
def _ordered_range(stream, lo=-3.0, hi=3.0):
a = stream.float_in_range(lo, hi)
b = stream.float_in_range(lo, hi)
return (a, b) if a <= b else (b, a)
def _color_tuple(stream):
return (stream.int_in_range(-20, 300), stream.int_in_range(-20, 300),
stream.int_in_range(-20, 300), stream.int_in_range(-20, 300))
def _op_layer_apply(stream, hms):
"""Tier C (#312): ColorLayer/TileLayer terrain application from a HeightMap
source -- apply_threshold / apply_gradient / apply_ranges. Half the time the
source heightmap matches the layer dims (reaches the real mapping logic),
otherwise a pooled, possibly mismatched map is used (must raise, not crash).
"""
w = stream.int_in_range(MIN_DIM, MAX_DIM)
h = stream.int_in_range(MIN_DIM, MAX_DIM)
try:
grid = mcrfpy.Grid(grid_size=(w, h))
clayer = mcrfpy.ColorLayer(name="terrain", z_index=0)
tlayer = mcrfpy.TileLayer(name="tiles", z_index=-1)
grid.add_layer(clayer)
grid.add_layer(tlayer)
except EXPECTED_EXCEPTIONS:
return
if stream.bool() or not hms:
src = mcrfpy.HeightMap(size=(w, h), fill=stream.float_in_range(-2.0, 2.0))
try:
src.add_voronoi(stream.int_in_range(1, 8), seed=stream.u32())
except EXPECTED_EXCEPTIONS:
pass
else:
src = hms[stream.int_in_range(0, len(hms) - 1)]
which = stream.u8() % 5
if which == 0:
clayer.apply_threshold(src, _ordered_range(stream), _color_tuple(stream))
elif which == 1:
clayer.apply_gradient(src, _ordered_range(stream),
_color_tuple(stream), _color_tuple(stream))
elif which == 2:
n = stream.int_in_range(1, 4)
ranges = [(_ordered_range(stream), _color_tuple(stream)) for _ in range(n)]
clayer.apply_ranges(src, ranges)
elif which == 3:
tlayer.apply_threshold(src, _ordered_range(stream), stream.int_in_range(-5, 4096))
else:
n = stream.int_in_range(1, 4)
ranges = [(_ordered_range(stream), stream.int_in_range(-5, 4096)) for _ in range(n)]
tlayer.apply_ranges(src, ranges)
def _dispatch(op, stream, hms, dms, nss, bsps): def _dispatch(op, stream, hms, dms, nss, bsps):
# 23 distinct operations covering all four surfaces plus conversions. # 24 distinct operations covering all four surfaces plus conversions, plus
# one Tier C (#312) layer-application op.
if op == 0: if op == 0:
_make_heightmap(stream, hms) _make_heightmap(stream, hms)
elif op == 1: elif op == 1:
@ -558,9 +614,11 @@ def _dispatch(op, stream, hms, dms, nss, bsps):
_op_bsp_split(stream, bsps) _op_bsp_split(stream, bsps)
elif op == 23: elif op == 23:
_op_bsp_walk(stream, bsps) _op_bsp_walk(stream, bsps)
elif op == 24:
_op_layer_apply(stream, hms)
_NUM_OPS = 24 _NUM_OPS = 25
def fuzz_one_input(data): def fuzz_one_input(data):

View file

@ -102,7 +102,7 @@ def _pick_entity(stream, entities):
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
NUM_OPS = 17 NUM_OPS = 18
def _dispatch(op, stream, state): def _dispatch(op, stream, state):
@ -255,6 +255,47 @@ def _dispatch(op, stream, state):
except EXPECTED_EXCEPTIONS: except EXPECTED_EXCEPTIONS:
break break
elif op == 17:
# Op 17 (Tier C #312): Grid.find_path -- the dedicated A* entry that
# path_from() doesn't reach -- then fully exercise the AStarPath object
# (peek / __len__ / __bool__ / iteration), not just walk()/properties.
start = _rand_coord(stream, w, h, oob_chance=True)
end = _rand_coord(stream, w, h, oob_chance=True)
kw = {}
if stream.bool():
kw["diagonal_cost"] = stream.float_in_range(-2.0, 10.0)
if stream.bool():
kw["collide"] = None if stream.bool() else stream.ascii_str(max_len=6)
path = grid.find_path(start, end, **kw)
if path is not None:
try:
_ = len(path)
_ = bool(path)
_ = path.peek()
except EXPECTED_EXCEPTIONS:
pass
mode = stream.u8() % 3
if mode == 0:
# Full iteration consumes the path.
count = 0
try:
for _step in path:
count += 1
if count > 256:
break
except EXPECTED_EXCEPTIONS:
pass
else:
for _ in range(stream.int_in_range(0, 6)):
try:
if mode == 1:
path.walk()
else:
path.peek()
path.walk()
except EXPECTED_EXCEPTIONS:
break
def _dispatch_step(stream, state): def _dispatch_step(stream, state):
"""Fire grid.step() with random n / turn_order filter. Separate op so """Fire grid.step() with random n / turn_order filter. Separate op so

View file

@ -79,6 +79,31 @@ TILELAYER_BOOL_PROPS = ("visible",)
COLORLAYER_INT_PROPS = ("z_index",) COLORLAYER_INT_PROPS = ("z_index",)
COLORLAYER_BOOL_PROPS = ("visible",) COLORLAYER_BOOL_PROPS = ("visible",)
# Tier C (#312): primitive shapes (Line/Circle/Arc) -- their setters were never
# touched by the property-type fuzzer. Verified against src/UILine.cpp,
# src/UICircle.cpp, src/UIArc.cpp.
LINE_FLOAT_PROPS = ("x", "y", "thickness", "opacity", "rotation")
LINE_INT_PROPS = ("z_index",)
LINE_BOOL_PROPS = ("visible",)
LINE_COLOR_PROPS = ("color",)
LINE_VECTOR_PROPS = ("start", "end", "pos")
LINE_STR_PROPS = ("name",)
CIRCLE_FLOAT_PROPS = ("x", "y", "radius", "outline", "opacity", "rotation")
CIRCLE_INT_PROPS = ("z_index",)
CIRCLE_BOOL_PROPS = ("visible",)
CIRCLE_COLOR_PROPS = ("fill_color", "outline_color")
CIRCLE_VECTOR_PROPS = ("center", "pos")
CIRCLE_STR_PROPS = ("name",)
ARC_FLOAT_PROPS = ("x", "y", "radius", "start_angle", "end_angle", "thickness",
"opacity", "rotation")
ARC_INT_PROPS = ("z_index",)
ARC_BOOL_PROPS = ("visible",)
ARC_COLOR_PROPS = ("color",)
ARC_VECTOR_PROPS = ("center", "pos")
ARC_STR_PROPS = ("name",)
def confused_value(stream): def confused_value(stream):
"""Return a deliberately type-confused value for property setters. """Return a deliberately type-confused value for property setters.
@ -563,6 +588,166 @@ def fuzz_hot_loop_reads(stream):
pass pass
# ----- Tier C (#312): shapes, Scene.children collections, module functions ---
def _make_line(stream):
return mcrfpy.Line(
start=(stream.float_in_range(-100, 600), stream.float_in_range(-100, 600)),
end=(stream.float_in_range(-100, 600), stream.float_in_range(-100, 600)),
thickness=stream.float_in_range(-2, 12))
def _make_circle(stream):
return mcrfpy.Circle(
center=(stream.float_in_range(-100, 600), stream.float_in_range(-100, 600)),
radius=stream.float_in_range(-5, 120))
def _make_arc(stream):
return mcrfpy.Arc(
center=(stream.float_in_range(-100, 600), stream.float_in_range(-100, 600)),
radius=stream.float_in_range(-5, 120),
start_angle=stream.float_in_range(-720, 720),
end_angle=stream.float_in_range(-720, 720))
# maker -> (correct-write groups, all-writable names for confused writes)
_SHAPES = (
(_make_line,
(("float", LINE_FLOAT_PROPS), ("int", LINE_INT_PROPS),
("bool", LINE_BOOL_PROPS), ("color", LINE_COLOR_PROPS),
("vector", LINE_VECTOR_PROPS), ("str", LINE_STR_PROPS)),
LINE_FLOAT_PROPS + LINE_INT_PROPS + LINE_BOOL_PROPS + LINE_COLOR_PROPS
+ LINE_VECTOR_PROPS + LINE_STR_PROPS),
(_make_circle,
(("float", CIRCLE_FLOAT_PROPS), ("int", CIRCLE_INT_PROPS),
("bool", CIRCLE_BOOL_PROPS), ("color", CIRCLE_COLOR_PROPS),
("vector", CIRCLE_VECTOR_PROPS), ("str", CIRCLE_STR_PROPS)),
CIRCLE_FLOAT_PROPS + CIRCLE_INT_PROPS + CIRCLE_BOOL_PROPS + CIRCLE_COLOR_PROPS
+ CIRCLE_VECTOR_PROPS + CIRCLE_STR_PROPS),
(_make_arc,
(("float", ARC_FLOAT_PROPS), ("int", ARC_INT_PROPS),
("bool", ARC_BOOL_PROPS), ("color", ARC_COLOR_PROPS),
("vector", ARC_VECTOR_PROPS), ("str", ARC_STR_PROPS)),
ARC_FLOAT_PROPS + ARC_INT_PROPS + ARC_BOOL_PROPS + ARC_COLOR_PROPS
+ ARC_VECTOR_PROPS + ARC_STR_PROPS),
)
def fuzz_shapes(stream):
"""Build a Line/Circle/Arc and hammer its setters (correct + confused)."""
maker, groups, all_writable = _SHAPES[stream.u8() % len(_SHAPES)]
try:
shape = maker(stream)
except EXPECTED_EXCEPTIONS:
return
_write_correct(stream, shape, groups)
read_names = tuple(name for _t, names in groups for name in names)
_read_all(shape, read_names + ("bounds", "global_position", "shader", "uniforms"))
for _ in range(stream.int_in_range(1, 6)):
_write_confused(stream, shape, all_writable)
def fuzz_scene_children(stream):
"""Tier C (#312): Scene.children collection ops -- append/insert/index/
count/pop/remove/slice/iterate, exercised outside the grid-entity scope."""
scene = mcrfpy.Scene(stream.ascii_str(6) or "fz")
col = scene.children
for _ in range(stream.int_in_range(0, 6)):
kind = stream.u8() % 5
try:
if kind == 0:
col.append(_make_frame(stream))
elif kind == 1:
col.append(_make_caption(stream))
elif kind == 2:
col.append(_make_sprite(stream))
elif kind == 3:
col.append(_make_circle(stream))
else:
idx = stream.int_in_range(0, max(0, len(col)))
col.insert(idx, _make_line(stream))
except EXPECTED_EXCEPTIONS:
pass
try:
_ = len(col)
except EXPECTED_EXCEPTIONS:
pass
n = 0
try:
n = len(col)
except EXPECTED_EXCEPTIONS:
pass
if n > 0:
first = None
try:
first = col[stream.int_in_range(0, n - 1)]
except EXPECTED_EXCEPTIONS:
pass
for meth in ("index", "count"):
try:
getattr(col, meth)(first)
except EXPECTED_EXCEPTIONS:
pass
try:
col.find(stream.ascii_str(6))
except EXPECTED_EXCEPTIONS:
pass
try:
col.remove(first)
except EXPECTED_EXCEPTIONS:
pass
try:
a = stream.int_in_range(0, max(0, n))
b = stream.int_in_range(a, max(a, n))
_ = col[a:b]
except EXPECTED_EXCEPTIONS:
pass
try:
for item in col:
_ = getattr(item, "x", None)
except EXPECTED_EXCEPTIONS:
pass
try:
col.pop()
except EXPECTED_EXCEPTIONS:
pass
def fuzz_module_functions(stream):
"""Tier C (#312): mcrfpy.find / find_all / bresenham / lock.
NOTE: the benchmark triplet (start_benchmark/log_benchmark/end_benchmark) is
intentionally NOT fuzzed here -- end_benchmark() writes a log file to disk on
every call (g_benchmarkLogger.end()), which over a fuzz campaign would spam
the filesystem and throttle iteration without exercising any memory-safety
path. It is harness instrumentation, not API surface worth fuzzing.
"""
try:
mcrfpy.find(stream.ascii_str(8))
except EXPECTED_EXCEPTIONS:
pass
try:
mcrfpy.find(stream.ascii_str(8), stream.ascii_str(6))
except EXPECTED_EXCEPTIONS:
pass
try:
mcrfpy.find_all(stream.ascii_str(8))
except EXPECTED_EXCEPTIONS:
pass
a = (stream.int_in_range(-80, 80), stream.int_in_range(-80, 80))
b = (stream.int_in_range(-80, 80), stream.int_in_range(-80, 80))
try:
mcrfpy.bresenham(a, b, stream.bool(), stream.bool())
except EXPECTED_EXCEPTIONS:
pass
try:
with mcrfpy.lock():
pass
except EXPECTED_EXCEPTIONS:
pass
# ----- Dispatch ----- # ----- Dispatch -----
OPS = ( OPS = (
@ -582,6 +767,9 @@ OPS = (
fuzz_vector, fuzz_vector,
fuzz_nested_reparent, fuzz_nested_reparent,
fuzz_hot_loop_reads, fuzz_hot_loop_reads,
fuzz_shapes, # Tier C (#312)
fuzz_scene_children, # Tier C (#312)
fuzz_module_functions, # Tier C (#312)
) )