From 246ed886dbc14c8a3ff8159f3728e06c37fadce4 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sun, 21 Jun 2026 16:45:03 -0400 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv --- ROADMAP.md | 5 +- tests/fuzz/fuzz_fov.py | 86 ++++++++++- tests/fuzz/fuzz_grid_entity.py | 89 +++++++++++ tests/fuzz/fuzz_maps_procgen.py | 62 +++++++- tests/fuzz/fuzz_pathfinding_behavior.py | 43 +++++- tests/fuzz/fuzz_property_types.py | 188 ++++++++++++++++++++++++ 6 files changed, 466 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index af6b058..9dd8dbb 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -48,7 +48,7 @@ The libFuzzer+ASan harness (#283) has nine work tranches merged: build plumbing - `fuzz_maps_procgen` -- HeightMap/DiscreteMap interfaces (W7) - `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) - **#294** -- `entity.perspective_map` replaces flat `vector` 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. ### 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. - **#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. @@ -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. ### 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 - Progress on the r/roguelikedev tutorial series (#167) diff --git a/tests/fuzz/fuzz_fov.py b/tests/fuzz/fuzz_fov.py index a2b62a8..3f95f37 100644 --- a/tests/fuzz/fuzz_fov.py +++ b/tests/fuzz/fuzz_fov.py @@ -59,6 +59,78 @@ def _make_grid(stream): 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): stream = ByteStream(data) try: @@ -68,7 +140,7 @@ def fuzz_one_input(data): for _ in range(n_ops): if stream.remaining < 1: break - op = stream.u8() % 16 + op = stream.u8() % 18 try: if op == 0: # 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)) grid.compute_fov((x, y), radius=5, algorithm="basic") - else: # op == 15 + elif op == 15: # is_in_fov garbage args. bad_choice = stream.u8() % 3 if bad_choice == 0: @@ -256,6 +328,16 @@ def fuzz_one_input(data): else: _ = 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: pass except EXPECTED_EXCEPTIONS: diff --git a/tests/fuzz/fuzz_grid_entity.py b/tests/fuzz/fuzz_grid_entity.py index 404ed52..09f9e0d 100644 --- a/tests/fuzz/fuzz_grid_entity.py +++ b/tests/fuzz/fuzz_grid_entity.py @@ -264,6 +264,93 @@ def _op_die(stream, grids, entities): # 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): """Iterate grid.entities and mid-loop call die() or reassign grid. @@ -326,6 +413,8 @@ _OPS = [ _op_set_grid_none, # 11 _op_die, # 12 _op_iterate_and_mutate, # 13 + _op_grid_query, # 14 (Tier C #312) + _op_gridpoint_attrs, # 15 (Tier C #312) ] diff --git a/tests/fuzz/fuzz_maps_procgen.py b/tests/fuzz/fuzz_maps_procgen.py index 86437ae..4a4df11 100644 --- a/tests/fuzz/fuzz_maps_procgen.py +++ b/tests/fuzz/fuzz_maps_procgen.py @@ -508,8 +508,64 @@ def _op_bsp_walk(stream, bsps): 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): - # 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: _make_heightmap(stream, hms) elif op == 1: @@ -558,9 +614,11 @@ def _dispatch(op, stream, hms, dms, nss, bsps): _op_bsp_split(stream, bsps) elif op == 23: _op_bsp_walk(stream, bsps) + elif op == 24: + _op_layer_apply(stream, hms) -_NUM_OPS = 24 +_NUM_OPS = 25 def fuzz_one_input(data): diff --git a/tests/fuzz/fuzz_pathfinding_behavior.py b/tests/fuzz/fuzz_pathfinding_behavior.py index 491617c..72615ed 100644 --- a/tests/fuzz/fuzz_pathfinding_behavior.py +++ b/tests/fuzz/fuzz_pathfinding_behavior.py @@ -102,7 +102,7 @@ def _pick_entity(stream, entities): # --------------------------------------------------------------------------- -NUM_OPS = 17 +NUM_OPS = 18 def _dispatch(op, stream, state): @@ -255,6 +255,47 @@ def _dispatch(op, stream, state): except EXPECTED_EXCEPTIONS: 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): """Fire grid.step() with random n / turn_order filter. Separate op so diff --git a/tests/fuzz/fuzz_property_types.py b/tests/fuzz/fuzz_property_types.py index 3b2e0b6..25895af 100644 --- a/tests/fuzz/fuzz_property_types.py +++ b/tests/fuzz/fuzz_property_types.py @@ -79,6 +79,31 @@ TILELAYER_BOOL_PROPS = ("visible",) COLORLAYER_INT_PROPS = ("z_index",) 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): """Return a deliberately type-confused value for property setters. @@ -563,6 +588,166 @@ def fuzz_hot_loop_reads(stream): 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 ----- OPS = ( @@ -582,6 +767,9 @@ OPS = ( fuzz_vector, fuzz_nested_reparent, fuzz_hot_loop_reads, + fuzz_shapes, # Tier C (#312) + fuzz_scene_children, # Tier C (#312) + fuzz_module_functions, # Tier C (#312) )