136 lines
4.8 KiB
Python
136 lines
4.8 KiB
Python
|
|
"""
|
||
|
|
Regression test for issue #369.
|
||
|
|
|
||
|
|
Every path that hands a C++ drawable back to Python must return the *same* wrapper
|
||
|
|
object, or `child.parent is parent` silently answers False and Python subclasses
|
||
|
|
reached through those paths lose their identity.
|
||
|
|
|
||
|
|
Broken paths this locks down:
|
||
|
|
- UIDrawable::get_parent (.parent)
|
||
|
|
- find_in_collection (mcrfpy.find / find_all)
|
||
|
|
- UIEntityCollection concat/slice (entities + [...], entities[a:b])
|
||
|
|
"""
|
||
|
|
import mcrfpy
|
||
|
|
import sys
|
||
|
|
|
||
|
|
failures = []
|
||
|
|
|
||
|
|
|
||
|
|
def check(label, condition):
|
||
|
|
if condition:
|
||
|
|
print(f" PASS: {label}")
|
||
|
|
else:
|
||
|
|
print(f" FAIL: {label}")
|
||
|
|
failures.append(label)
|
||
|
|
|
||
|
|
|
||
|
|
scene = mcrfpy.Scene("issue369")
|
||
|
|
ui = scene.children
|
||
|
|
|
||
|
|
# ---------------------------------------------------------------- .parent identity
|
||
|
|
print("1. .parent returns a stable, identical wrapper")
|
||
|
|
parent = mcrfpy.Frame(pos=(0, 0), size=(100, 100), name="parent_frame")
|
||
|
|
kid = mcrfpy.Frame(pos=(1, 1), size=(10, 10), name="kid_frame")
|
||
|
|
ui.append(parent)
|
||
|
|
parent.children.append(kid)
|
||
|
|
|
||
|
|
check("kid.parent is parent", kid.parent is parent)
|
||
|
|
check("repeated reads are identical", kid.parent is kid.parent)
|
||
|
|
check("id() is stable across reads", id(kid.parent) == id(kid.parent))
|
||
|
|
|
||
|
|
# .parent must be usable as a dict key / set member
|
||
|
|
seen = {kid.parent: "found"}
|
||
|
|
check(".parent works as a dict key", seen.get(parent) == "found")
|
||
|
|
check(".parent works in a set", len({kid.parent, kid.parent, parent}) == 1)
|
||
|
|
|
||
|
|
# ------------------------------------------------------- subclass survives .parent
|
||
|
|
print("2. Python subclass identity survives .parent")
|
||
|
|
|
||
|
|
|
||
|
|
class MyFrame(mcrfpy.Frame):
|
||
|
|
def __init__(self, **kwargs):
|
||
|
|
super().__init__(**kwargs)
|
||
|
|
self.custom_state = "preserved"
|
||
|
|
|
||
|
|
|
||
|
|
sub_parent = MyFrame(pos=(0, 0), size=(200, 200), name="sub_parent")
|
||
|
|
sub_kid = mcrfpy.Caption(pos=(5, 5), text="inner", name="sub_kid")
|
||
|
|
ui.append(sub_parent)
|
||
|
|
sub_parent.children.append(sub_kid)
|
||
|
|
|
||
|
|
check("sub_kid.parent is sub_parent", sub_kid.parent is sub_parent)
|
||
|
|
check("subclass type preserved", isinstance(sub_kid.parent, MyFrame))
|
||
|
|
check("subclass attributes preserved",
|
||
|
|
getattr(sub_kid.parent, "custom_state", None) == "preserved")
|
||
|
|
|
||
|
|
# ------------------------------------------------- .parent covers all drawable types
|
||
|
|
print("3. .parent resolves non-Frame/Caption/Sprite/Grid parents (was None)")
|
||
|
|
# Line/Circle/Arc/Viewport3D had no switch arm and fell through to Py_RETURN_NONE.
|
||
|
|
# They aren't containers, so verify the *child-of-grid* case instead: a GridView
|
||
|
|
# parent must come back identical, and every type must at least round-trip via find().
|
||
|
|
grid = mcrfpy.Grid(grid_size=(10, 10), pos=(0, 0), size=(100, 100), name="the_grid")
|
||
|
|
ui.append(grid)
|
||
|
|
overlay = mcrfpy.Frame(pos=(0, 0), size=(10, 10), name="overlay_child")
|
||
|
|
grid.children.append(overlay)
|
||
|
|
check("overlay.parent is grid", overlay.parent is grid)
|
||
|
|
|
||
|
|
# ------------------------------------------------------------- find() identity
|
||
|
|
print("4. find() / find_all() return existing wrappers")
|
||
|
|
found = mcrfpy.find("parent_frame", "issue369")
|
||
|
|
check("find() returns the same object", found is parent)
|
||
|
|
|
||
|
|
found_sub = mcrfpy.find("sub_parent", "issue369")
|
||
|
|
check("find() preserves subclass", found_sub is sub_parent)
|
||
|
|
check("find() subclass attrs intact",
|
||
|
|
getattr(found_sub, "custom_state", None) == "preserved")
|
||
|
|
|
||
|
|
all_found = mcrfpy.find_all("*_frame", "issue369")
|
||
|
|
by_id = {id(o) for o in all_found}
|
||
|
|
check("find_all() returns live wrappers",
|
||
|
|
id(parent) in by_id and id(kid) in by_id)
|
||
|
|
|
||
|
|
# ---------------------------------------------------------- entity collection paths
|
||
|
|
print("5. EntityCollection slice / concat preserve entity identity")
|
||
|
|
|
||
|
|
|
||
|
|
class MyEntity(mcrfpy.Entity):
|
||
|
|
def __init__(self, **kwargs):
|
||
|
|
super().__init__(**kwargs)
|
||
|
|
self.tag = "mine"
|
||
|
|
|
||
|
|
|
||
|
|
e0 = MyEntity(grid_pos=(1, 1))
|
||
|
|
e1 = mcrfpy.Entity(grid_pos=(2, 2))
|
||
|
|
e2 = mcrfpy.Entity(grid_pos=(3, 3))
|
||
|
|
for e in (e0, e1, e2):
|
||
|
|
grid.entities.append(e)
|
||
|
|
|
||
|
|
check("entities[0] is e0", grid.entities[0] is e0)
|
||
|
|
|
||
|
|
sliced = grid.entities[0:2]
|
||
|
|
check("slice returns the same wrappers", sliced[0] is e0 and sliced[1] is e1)
|
||
|
|
check("slice preserves subclass", isinstance(sliced[0], MyEntity))
|
||
|
|
check("slice preserves subclass attrs", getattr(sliced[0], "tag", None) == "mine")
|
||
|
|
|
||
|
|
concat = grid.entities + []
|
||
|
|
check("concat returns the same wrappers", concat[0] is e0)
|
||
|
|
check("concat preserves subclass", isinstance(concat[0], MyEntity))
|
||
|
|
|
||
|
|
# The #266 identity ref must survive a duplicate-wrapper round trip: before the fix,
|
||
|
|
# the temporary wrapper's dealloc cleared UIEntity::pyobject on the shared C++ object.
|
||
|
|
del sliced, concat
|
||
|
|
import gc
|
||
|
|
gc.collect()
|
||
|
|
check("entity identity survives temp wrappers", grid.entities[0] is e0)
|
||
|
|
check("subclass attrs survive temp wrappers",
|
||
|
|
getattr(grid.entities[0], "tag", None) == "mine")
|
||
|
|
|
||
|
|
print()
|
||
|
|
if failures:
|
||
|
|
print(f"FAIL - {len(failures)} check(s) failed:")
|
||
|
|
for f in failures:
|
||
|
|
print(f" - {f}")
|
||
|
|
sys.exit(1)
|
||
|
|
|
||
|
|
print("PASS")
|
||
|
|
sys.exit(0)
|