Add regression tests for vector NULL safety and uniform owner validity, closes #287

Covers #268 (sfVector2f_to_PyObject NULL propagation) and #272
(UniformCollection weak_ptr validity check). Combined with existing
tests for #258-#278, this completes regression coverage for the
full memory safety audit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2026-04-10 02:57:32 -04:00
commit 2b0267430d
2 changed files with 159 additions and 0 deletions

View file

@ -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)

View file

@ -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<void> 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)