Add regression tests for Frame.children mutation and parent=None removal
frame_children_mutation_test: validates remove(), property mutation via stored refs, fill_color persistence, iteration mutation, pop(), and while-loop clearing — with visual screenshot verification. parent_none_removal_test: validates that setting .parent = None removes children from Frame.children and scene.children, Entity.grid = None removal, and Grid overlay children removal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b1902a3d8b
commit
6a0040d630
2 changed files with 348 additions and 0 deletions
176
tests/regression/frame_children_mutation_test.py
Normal file
176
tests/regression/frame_children_mutation_test.py
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
"""Test Frame.children mutation behaviors reported as potential bugs.
|
||||
|
||||
Three failure modes reported:
|
||||
1. remove() on child doesn't visually update
|
||||
2. Property mutation on stored references doesn't render
|
||||
3. Clearing children via while-loop doesn't work
|
||||
|
||||
This test validates the data model. Visual screenshots require
|
||||
a timer-based approach with mcrfpy.step() in headless mode.
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
PASS = True
|
||||
results = []
|
||||
|
||||
def check(name, condition):
|
||||
global PASS
|
||||
if not condition:
|
||||
PASS = False
|
||||
results.append(f" FAIL: {name}")
|
||||
else:
|
||||
results.append(f" pass: {name}")
|
||||
|
||||
# ============================================================
|
||||
# Test 1: remove() actually modifies the children collection
|
||||
# ============================================================
|
||||
|
||||
scene = mcrfpy.Scene("test")
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
box = mcrfpy.Frame(pos=(10,10), size=(200,200),
|
||||
fill_color=mcrfpy.Color(40,40,40))
|
||||
scene.children.append(box)
|
||||
|
||||
cap1 = mcrfpy.Caption(text="child1", pos=(5,5))
|
||||
cap2 = mcrfpy.Caption(text="child2", pos=(5,25))
|
||||
cap3 = mcrfpy.Caption(text="child3", pos=(5,45))
|
||||
box.children.append(cap1)
|
||||
box.children.append(cap2)
|
||||
box.children.append(cap3)
|
||||
|
||||
check("1a: initial children count is 3", len(box.children) == 3)
|
||||
|
||||
# Remove middle child
|
||||
box.children.remove(cap2)
|
||||
check("1b: children count after remove is 2", len(box.children) == 2)
|
||||
check("1c: first child is still cap1", box.children[0].text == "child1")
|
||||
check("1d: second child is now cap3", box.children[1].text == "child3")
|
||||
|
||||
# ============================================================
|
||||
# Test 2: Property mutation on stored references
|
||||
# ============================================================
|
||||
|
||||
stored_ref = box.children[0]
|
||||
check("2a: stored ref text matches", stored_ref.text == "child1")
|
||||
stored_ref.text = "MUTATED"
|
||||
check("2b: mutation via stored ref visible via children[0]",
|
||||
box.children[0].text == "MUTATED")
|
||||
|
||||
# Verify identity: stored ref should point to same C++ object
|
||||
check("2c: stored ref identity (same text after re-read)",
|
||||
stored_ref.text == box.children[0].text)
|
||||
|
||||
# ============================================================
|
||||
# Test 3: fill_color mutation on child persists
|
||||
# ============================================================
|
||||
|
||||
cap_direct = box.children[1]
|
||||
original_color = cap_direct.fill_color
|
||||
cap_direct.fill_color = mcrfpy.Color(255, 0, 0, 255)
|
||||
reread_color = box.children[1].fill_color
|
||||
check("3a: fill_color mutation persists (r)",
|
||||
reread_color.r == 255)
|
||||
check("3b: fill_color mutation persists (g)",
|
||||
reread_color.g == 0)
|
||||
|
||||
# ============================================================
|
||||
# Test 4: Mutation via iteration
|
||||
# ============================================================
|
||||
|
||||
for child in box.children:
|
||||
child.text = "ITER_" + child.text
|
||||
|
||||
check("4a: mutation via iteration on child 0",
|
||||
box.children[0].text == "ITER_MUTATED")
|
||||
check("4b: mutation via iteration on child 1",
|
||||
box.children[1].text == "ITER_child3")
|
||||
|
||||
# ============================================================
|
||||
# Test 5: pop() works
|
||||
# ============================================================
|
||||
|
||||
popped = box.children.pop()
|
||||
check("5a: pop() reduces count", len(box.children) == 1)
|
||||
check("5b: popped element has correct text", popped.text == "ITER_child3")
|
||||
|
||||
# ============================================================
|
||||
# Test 6: while-loop clearing
|
||||
# ============================================================
|
||||
|
||||
box.children.append(mcrfpy.Caption(text="a", pos=(0,0)))
|
||||
box.children.append(mcrfpy.Caption(text="b", pos=(0,0)))
|
||||
box.children.append(mcrfpy.Caption(text="c", pos=(0,0)))
|
||||
check("6a: re-added children", len(box.children) == 4)
|
||||
|
||||
while len(box.children):
|
||||
box.children.pop()
|
||||
check("6b: while-loop clearing empties children", len(box.children) == 0)
|
||||
|
||||
# ============================================================
|
||||
# Test 7: Visual rendering via screenshots
|
||||
# ============================================================
|
||||
|
||||
scene2 = mcrfpy.Scene("visual")
|
||||
parent = mcrfpy.Frame(pos=(10,10), size=(400,300),
|
||||
fill_color=mcrfpy.Color(40,40,40))
|
||||
scene2.children.append(parent)
|
||||
|
||||
red_box = mcrfpy.Frame(pos=(10,10), size=(80,80),
|
||||
fill_color=mcrfpy.Color(255,0,0))
|
||||
green_box = mcrfpy.Frame(pos=(100,10), size=(80,80),
|
||||
fill_color=mcrfpy.Color(0,255,0))
|
||||
blue_box = mcrfpy.Frame(pos=(190,10), size=(80,80),
|
||||
fill_color=mcrfpy.Color(0,0,255))
|
||||
|
||||
parent.children.append(red_box)
|
||||
parent.children.append(green_box)
|
||||
parent.children.append(blue_box)
|
||||
|
||||
mcrfpy.current_scene = scene2
|
||||
|
||||
# Render a frame, take screenshot with all 3
|
||||
mcrfpy.step(0.05)
|
||||
automation.screenshot("frame_children_7a_all3.png")
|
||||
|
||||
# Remove green box
|
||||
parent.children.remove(green_box)
|
||||
check("7a: remove green_box reduces count to 2", len(parent.children) == 2)
|
||||
|
||||
mcrfpy.step(0.05)
|
||||
automation.screenshot("frame_children_7b_no_green.png")
|
||||
|
||||
# Mutate red to yellow via stored reference
|
||||
red_box.fill_color = mcrfpy.Color(255, 255, 0)
|
||||
mcrfpy.step(0.05)
|
||||
automation.screenshot("frame_children_7c_yellow.png")
|
||||
|
||||
# Mutate via indexed access
|
||||
parent.children[0].fill_color = mcrfpy.Color(255, 0, 255) # magenta
|
||||
mcrfpy.step(0.05)
|
||||
automation.screenshot("frame_children_7d_magenta.png")
|
||||
|
||||
# Clear all
|
||||
while len(parent.children):
|
||||
parent.children.pop()
|
||||
check("7b: all children cleared", len(parent.children) == 0)
|
||||
|
||||
mcrfpy.step(0.05)
|
||||
automation.screenshot("frame_children_7e_cleared.png")
|
||||
|
||||
# ============================================================
|
||||
# Report
|
||||
# ============================================================
|
||||
|
||||
print("=" * 50)
|
||||
print("Frame.children mutation test results:")
|
||||
for r in results:
|
||||
print(r)
|
||||
print("=" * 50)
|
||||
if PASS:
|
||||
print("PASS")
|
||||
else:
|
||||
print("FAIL")
|
||||
sys.exit(0 if PASS else 1)
|
||||
172
tests/regression/parent_none_removal_test.py
Normal file
172
tests/regression/parent_none_removal_test.py
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
"""Test that setting .parent = None removes child from parent's children.
|
||||
|
||||
Verifies consistency between:
|
||||
- Frame.children.remove(child)
|
||||
- child.parent = None
|
||||
- Entity.grid = None
|
||||
"""
|
||||
import mcrfpy
|
||||
from mcrfpy import automation
|
||||
import sys
|
||||
|
||||
PASS = True
|
||||
results = []
|
||||
|
||||
def check(name, condition):
|
||||
global PASS
|
||||
if not condition:
|
||||
PASS = False
|
||||
results.append(f" FAIL: {name}")
|
||||
else:
|
||||
results.append(f" pass: {name}")
|
||||
|
||||
# ============================================================
|
||||
# Test 1: child.parent = None removes from Frame.children
|
||||
# ============================================================
|
||||
|
||||
scene = mcrfpy.Scene("test")
|
||||
mcrfpy.current_scene = scene
|
||||
|
||||
box = mcrfpy.Frame(pos=(10,10), size=(200,200),
|
||||
fill_color=mcrfpy.Color(40,40,40))
|
||||
scene.children.append(box)
|
||||
|
||||
cap1 = mcrfpy.Caption(text="child1", pos=(5,5))
|
||||
cap2 = mcrfpy.Caption(text="child2", pos=(5,25))
|
||||
cap3 = mcrfpy.Caption(text="child3", pos=(5,45))
|
||||
box.children.append(cap1)
|
||||
box.children.append(cap2)
|
||||
box.children.append(cap3)
|
||||
|
||||
check("1a: initial count is 3", len(box.children) == 3)
|
||||
check("1b: cap2 parent is box", cap2.parent is not None)
|
||||
|
||||
# Remove via parent = None
|
||||
cap2.parent = None
|
||||
check("1c: count after cap2.parent=None is 2", len(box.children) == 2)
|
||||
check("1d: cap2.parent is now None", cap2.parent is None)
|
||||
check("1e: remaining children are cap1,cap3",
|
||||
box.children[0].text == "child1" and box.children[1].text == "child3")
|
||||
|
||||
# ============================================================
|
||||
# Test 2: child.parent = None on Frame child (not Caption)
|
||||
# ============================================================
|
||||
|
||||
inner = mcrfpy.Frame(pos=(10,100), size=(50,50),
|
||||
fill_color=mcrfpy.Color(255,0,0))
|
||||
box.children.append(inner)
|
||||
check("2a: count after adding inner frame is 3", len(box.children) == 3)
|
||||
|
||||
inner.parent = None
|
||||
check("2b: count after inner.parent=None is 2", len(box.children) == 2)
|
||||
check("2c: inner.parent is None", inner.parent is None)
|
||||
|
||||
# ============================================================
|
||||
# Test 3: child.parent = None on scene-level element
|
||||
# ============================================================
|
||||
|
||||
top_frame = mcrfpy.Frame(pos=(300,10), size=(100,100),
|
||||
fill_color=mcrfpy.Color(0,255,0))
|
||||
scene.children.append(top_frame)
|
||||
initial_scene_count = len(scene.children)
|
||||
check("3a: top_frame added to scene", initial_scene_count >= 2)
|
||||
|
||||
top_frame.parent = None
|
||||
check("3b: scene count decreased", len(scene.children) == initial_scene_count - 1)
|
||||
check("3c: top_frame.parent is None", top_frame.parent is None)
|
||||
|
||||
# ============================================================
|
||||
# Test 4: while-loop clearing via parent = None
|
||||
# ============================================================
|
||||
|
||||
container = mcrfpy.Frame(pos=(10,10), size=(200,200))
|
||||
scene.children.append(container)
|
||||
|
||||
for i in range(5):
|
||||
container.children.append(
|
||||
mcrfpy.Caption(text=f"item{i}", pos=(0, i*20)))
|
||||
check("4a: container has 5 children", len(container.children) == 5)
|
||||
|
||||
# Clear by setting parent = None on each child
|
||||
while len(container.children):
|
||||
container.children[0].parent = None
|
||||
check("4b: container cleared via parent=None loop", len(container.children) == 0)
|
||||
|
||||
# ============================================================
|
||||
# Test 5: Entity.grid = None removes from grid
|
||||
# ============================================================
|
||||
|
||||
tex = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16)
|
||||
grid = mcrfpy.Grid(grid_size=(10,10), texture=tex,
|
||||
pos=(0,0), size=(200,200))
|
||||
scene.children.append(grid)
|
||||
|
||||
e1 = mcrfpy.Entity(grid_pos=(1,1), texture=tex, sprite_index=0)
|
||||
e2 = mcrfpy.Entity(grid_pos=(2,2), texture=tex, sprite_index=1)
|
||||
e3 = mcrfpy.Entity(grid_pos=(3,3), texture=tex, sprite_index=2)
|
||||
grid.entities.append(e1)
|
||||
grid.entities.append(e2)
|
||||
grid.entities.append(e3)
|
||||
|
||||
check("5a: grid has 3 entities", len(grid.entities) == 3)
|
||||
|
||||
e2.grid = None
|
||||
check("5b: grid has 2 entities after e2.grid=None", len(grid.entities) == 2)
|
||||
check("5c: e2.grid is None", e2.grid is None)
|
||||
|
||||
# ============================================================
|
||||
# Test 6: Grid.children (overlay drawables) - parent = None
|
||||
# ============================================================
|
||||
|
||||
overlay_cap = mcrfpy.Caption(text="overlay", pos=(5,5))
|
||||
grid.children.append(overlay_cap)
|
||||
check("6a: grid.children has 1 overlay", len(grid.children) == 1)
|
||||
|
||||
overlay_cap.parent = None
|
||||
check("6b: grid.children empty after parent=None", len(grid.children) == 0)
|
||||
|
||||
# ============================================================
|
||||
# Test 7: Visual verification with screenshots
|
||||
# ============================================================
|
||||
|
||||
scene2 = mcrfpy.Scene("visual")
|
||||
parent = mcrfpy.Frame(pos=(10,10), size=(400,300),
|
||||
fill_color=mcrfpy.Color(40,40,40))
|
||||
scene2.children.append(parent)
|
||||
|
||||
red = mcrfpy.Frame(pos=(10,10), size=(80,80),
|
||||
fill_color=mcrfpy.Color(255,0,0))
|
||||
green = mcrfpy.Frame(pos=(100,10), size=(80,80),
|
||||
fill_color=mcrfpy.Color(0,255,0))
|
||||
blue = mcrfpy.Frame(pos=(190,10), size=(80,80),
|
||||
fill_color=mcrfpy.Color(0,0,255))
|
||||
|
||||
parent.children.append(red)
|
||||
parent.children.append(green)
|
||||
parent.children.append(blue)
|
||||
|
||||
mcrfpy.current_scene = scene2
|
||||
mcrfpy.step(0.05)
|
||||
automation.screenshot("parent_none_7a_before.png")
|
||||
|
||||
# Remove green via parent = None
|
||||
green.parent = None
|
||||
mcrfpy.step(0.05)
|
||||
automation.screenshot("parent_none_7b_after.png")
|
||||
|
||||
check("7a: visual - green removed from children", len(parent.children) == 2)
|
||||
|
||||
# ============================================================
|
||||
# Report
|
||||
# ============================================================
|
||||
|
||||
print("=" * 50)
|
||||
print("parent=None removal test results:")
|
||||
for r in results:
|
||||
print(r)
|
||||
print("=" * 50)
|
||||
if PASS:
|
||||
print("PASS")
|
||||
else:
|
||||
print("FAIL")
|
||||
sys.exit(0 if PASS else 1)
|
||||
Loading…
Add table
Add a link
Reference in a new issue