diff --git a/tests/regression/issue_268_vector_null_test.py b/tests/regression/issue_268_vector_null_test.py new file mode 100644 index 0000000..c97783a --- /dev/null +++ b/tests/regression/issue_268_vector_null_test.py @@ -0,0 +1,69 @@ +"""Regression test: sfVector2f_to_PyObject must handle allocation failure. + +Issue #268: sfVector2f_to_PyObject in UIEntity.cpp could return NULL when +tp_alloc fails, but callers didn't check. Under normal conditions this +can't be triggered, but this test exercises all Vector-returning entity +properties to ensure the code path works correctly and doesn't crash. + +Fix: Callers now propagate NULL returns as Python exceptions. +""" +import mcrfpy +import sys + +def test_entity_vector_properties(): + """All Vector-returning properties must return valid Vector objects.""" + grid = mcrfpy.Grid(grid_size=(20, 20)) + entity = mcrfpy.Entity(grid_pos=(5, 5), grid=grid) + + pos = entity.pos + assert pos is not None, "entity.pos returned None" + assert hasattr(pos, 'x') and hasattr(pos, 'y'), "pos missing x/y" + + draw_pos = entity.draw_pos + assert draw_pos is not None, "entity.draw_pos returned None" + + cell_pos = entity.cell_pos + assert cell_pos is not None, "entity.cell_pos returned None" + assert cell_pos.x == 5 and cell_pos.y == 5, f"cell_pos wrong: {cell_pos.x}, {cell_pos.y}" + + offset = entity.sprite_offset + assert offset is not None, "entity.sprite_offset returned None" + assert offset.x == 0.0 and offset.y == 0.0, "Default sprite_offset should be (0,0)" + + print(" PASS: entity vector properties") + +def test_entity_vector_setters(): + """Vector property setters must accept tuples and Vectors.""" + grid = mcrfpy.Grid(grid_size=(20, 20)) + entity = mcrfpy.Entity(grid_pos=(3, 3), grid=grid) + + entity.sprite_offset = (10.0, -5.0) + assert entity.sprite_offset.x == 10.0, f"sprite_offset.x wrong: {entity.sprite_offset.x}" + assert entity.sprite_offset.y == -5.0, f"sprite_offset.y wrong: {entity.sprite_offset.y}" + + entity.draw_pos = mcrfpy.Vector(7.5, 8.5) + assert entity.draw_pos.x == 7.5, f"draw_pos.x wrong: {entity.draw_pos.x}" + + print(" PASS: entity vector setters") + +def test_entity_no_grid_vector(): + """Entities without a grid should handle vector properties gracefully.""" + entity = mcrfpy.Entity() + + draw_pos = entity.draw_pos + assert draw_pos is not None, "draw_pos should work without grid" + + cell_pos = entity.cell_pos + assert cell_pos is not None, "cell_pos should work without grid" + + offset = entity.sprite_offset + assert offset is not None, "sprite_offset should work without grid" + + print(" PASS: entity no-grid vector properties") + +print("Testing issue #268: sfVector2f_to_PyObject NULL safety...") +test_entity_vector_properties() +test_entity_vector_setters() +test_entity_no_grid_vector() +print("All #268 tests passed.") +sys.exit(0) diff --git a/tests/regression/issue_272_uniform_owner_test.py b/tests/regression/issue_272_uniform_owner_test.py new file mode 100644 index 0000000..a331ed4 --- /dev/null +++ b/tests/regression/issue_272_uniform_owner_test.py @@ -0,0 +1,90 @@ +"""Regression test: UniformCollection must check owner validity. + +Issue #272: PyUniformCollectionObject stored a raw pointer to UniformCollection +but only checked non-NULL. If the owning UIDrawable was destroyed, the raw +pointer dangled, causing use-after-free. + +Fix: Added weak_ptr owner field. All accessors now check owner.lock() +before accessing the collection, raising RuntimeError if the owner is gone. +""" +import mcrfpy +import gc +import sys + +def test_uniforms_basic_access(): + """Uniform collection should work when owner is alive.""" + frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + u = frame.uniforms + assert len(u) == 0, f"Expected empty uniforms, got {len(u)}" + + u['brightness'] = 1.0 + assert len(u) == 1, f"Expected 1 uniform, got {len(u)}" + assert 'brightness' in u, "brightness should be in uniforms" + + del u['brightness'] + assert len(u) == 0, f"Expected 0 after delete, got {len(u)}" + + print(" PASS: basic uniform access") + +def test_uniforms_owner_destroyed(): + """Accessing uniforms after owner destruction must fail gracefully.""" + frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + u = frame.uniforms + u['test'] = 1.0 + + del frame + gc.collect() + + # len() and __contains__ return 0/False gracefully + assert len(u) == 0, f"len should be 0 after owner destroyed, got {len(u)}" + assert 'test' not in u, "'test' should not be found after owner destroyed" + + # Subscript access and assignment raise RuntimeError + errors = 0 + try: + _ = u['test'] + except RuntimeError: + errors += 1 + + try: + u['new'] = 2.0 + except RuntimeError: + errors += 1 + + assert errors == 2, f"Expected 2 RuntimeErrors from subscript ops, got {errors}" + print(" PASS: uniforms after owner destroyed") + +def test_uniforms_owner_in_scene(): + """Uniforms should work while owner is in a scene's children.""" + scene = mcrfpy.Scene("uniform_test") + frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + scene.children.append(frame) + u = frame.uniforms + u['value'] = 0.5 + assert len(u) == 1, "Uniform should be accessible while in scene" + print(" PASS: uniforms with scene-owned frame") + +def test_uniforms_multiple_values(): + """Multiple uniform types should work correctly.""" + frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100)) + u = frame.uniforms + + u['f'] = 1.0 + u['v2'] = (1.0, 2.0) + u['v3'] = (1.0, 2.0, 3.0) + u['v4'] = (1.0, 2.0, 3.0, 4.0) + + assert len(u) == 4, f"Expected 4 uniforms, got {len(u)}" + + keys = list(u.keys()) + assert len(keys) == 4, f"Expected 4 keys, got {len(keys)}" + + print(" PASS: multiple uniform types") + +print("Testing issue #272: UniformCollection owner validity...") +test_uniforms_basic_access() +test_uniforms_owner_destroyed() +test_uniforms_owner_in_scene() +test_uniforms_multiple_values() +print("All #272 tests passed.") +sys.exit(0)