From 6a0040d63023f4e714c773ca8cffeabd09f9dc84 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Wed, 1 Apr 2026 11:27:47 -0400 Subject: [PATCH] Add regression tests for Frame.children mutation and parent=None removal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../frame_children_mutation_test.py | 176 ++++++++++++++++++ tests/regression/parent_none_removal_test.py | 172 +++++++++++++++++ 2 files changed, 348 insertions(+) create mode 100644 tests/regression/frame_children_mutation_test.py create mode 100644 tests/regression/parent_none_removal_test.py diff --git a/tests/regression/frame_children_mutation_test.py b/tests/regression/frame_children_mutation_test.py new file mode 100644 index 0000000..b77de8d --- /dev/null +++ b/tests/regression/frame_children_mutation_test.py @@ -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) diff --git a/tests/regression/parent_none_removal_test.py b/tests/regression/parent_none_removal_test.py new file mode 100644 index 0000000..9a9a7b1 --- /dev/null +++ b/tests/regression/parent_none_removal_test.py @@ -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)