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:
parent
925699ef0b
commit
246ed886db
6 changed files with 466 additions and 7 deletions
|
|
@ -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<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.
|
||||
|
||||
### 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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue