diff --git a/tests/regression/issue_265_gridpointstate_dangle_test.py b/tests/regression/issue_265_gridpointstate_dangle_test.py new file mode 100644 index 0000000..a997b4d --- /dev/null +++ b/tests/regression/issue_265_gridpointstate_dangle_test.py @@ -0,0 +1,71 @@ +"""Regression test: GridPointState references must not dangle after grid resize. + +Issue #265: GridPointState objects held Python references to internal vectors +that could be invalidated when the underlying grid data was reallocated. + +Fix: GridPointState now copies data or holds safe references that survive +grid modifications. +""" +import mcrfpy +import sys + +PASS = True + +def check(name, condition): + global PASS + if not condition: + print(f"FAIL: {name}") + PASS = False + else: + print(f" ok: {name}") + +# Test 1: Access GridPointState after entity transfer +grid = mcrfpy.Grid(grid_size=(10, 10)) +entity = mcrfpy.Entity(grid_pos=(5, 5), grid=grid) +entity.update_visibility() + +state = entity.at(3, 3) +check("GridPointState accessible", state is not None) + +# Transfer entity to a different grid (invalidates old gridstate) +grid2 = mcrfpy.Grid(grid_size=(20, 20)) +entity.grid = grid2 +entity.update_visibility() + +# Old state reference should not crash +# (In the buggy version, accessing state after transfer would read freed memory) +try: + # Access the new gridstate + new_state = entity.at(15, 15) + check("GridPointState valid after transfer", new_state is not None) +except Exception as e: + check(f"GridPointState valid after transfer (exception: {e})", False) + +# Test 2: Multiple entities with GridPointState references +entities = [] +for i in range(5): + e = mcrfpy.Entity(grid_pos=(i, i), grid=grid) + entities.append(e) + +for e in entities: + e.update_visibility() + +states = [e.at(0, 0) for e in entities] +check("Multiple GridPointState refs created", len(states) == 5) + +# Remove all entities (should not crash) +for e in entities: + e.grid = None + +check("Entities removed safely", len(grid.entities) == 0) + +# Test 3: GridPoint reference stability +gp = grid.at(5, 5) +check("GridPoint accessible", gp is not None) +check("GridPoint walkable", gp.walkable == True or gp.walkable == False) + +if PASS: + print("PASS") + sys.exit(0) +else: + sys.exit(1) diff --git a/tests/regression/issue_267_275_refcount_test.py b/tests/regression/issue_267_275_refcount_test.py new file mode 100644 index 0000000..9ea0500 --- /dev/null +++ b/tests/regression/issue_267_275_refcount_test.py @@ -0,0 +1,98 @@ +"""Regression test: Python object reference count leaks. + +Issues #267, #275: Accessing certain properties (scene.children, entity +collections) would leak references, causing memory growth over time. + +Fix: Proper Py_DECREF in collection accessors and property getters. + +Note: sys.gettotalrefcount() is only available in debug Python builds. +This test uses a weaker proxy: create many objects, verify they're +collectible, and check that repeated property access doesn't accumulate +uncollectable objects. +""" +import mcrfpy +import sys +import gc + +PASS = True + +def check(name, condition): + global PASS + if not condition: + print(f"FAIL: {name}") + PASS = False + else: + print(f" ok: {name}") + +# Test 1: scene.children access doesn't leak +scene = mcrfpy.Scene("refcount_test") +mcrfpy.current_scene = scene + +for i in range(100): + children = scene.children + _ = len(children) + +gc.collect() +check("scene.children access loop completed", True) + +# Test 2: grid.entities access doesn't leak +grid = mcrfpy.Grid(grid_size=(10, 10)) +scene.children.append(grid) +for i in range(5): + mcrfpy.Entity(grid_pos=(i % 10, 0), grid=grid) + +for i in range(100): + entities = grid.entities + _ = len(entities) + +gc.collect() +check("grid.entities access loop completed", True) + +# Test 3: Repeated entity property access +entity = mcrfpy.Entity(grid_pos=(5, 5), grid=grid) +for i in range(100): + _ = entity.grid_pos + _ = entity.sprite_index + +gc.collect() +check("entity property access loop completed", True) + +# Test 4: Collection iteration doesn't leak +for i in range(50): + for child in scene.children: + pass + for ent in grid.entities: + pass + +gc.collect() +check("collection iteration loop completed", True) + +# Test 5: Creating and destroying objects in a loop +for i in range(50): + temp_scene = mcrfpy.Scene(f"temp_{i}") + f = mcrfpy.Frame(pos=(0, 0), size=(10, 10)) + temp_scene.children.append(f) + del f + +gc.collect() +check("create/destroy cycle completed", True) + +# Test 6: If pydebug is available, do actual refcount check +if hasattr(sys, 'gettotalrefcount'): + gc.collect() + before = sys.gettotalrefcount() + for i in range(1000): + _ = scene.children + _ = grid.entities + gc.collect() + after = sys.gettotalrefcount() + growth = after - before + check(f"refcount growth <= 50 (got {growth})", growth <= 50) +else: + print(" skip: sys.gettotalrefcount not available (not a debug build)") + +if PASS: + print("PASS") + sys.exit(0) +else: + sys.exit(1)