Rewrite stale Animation-ctor unit tests to drawable.animate()

test_animation_raii and test_animation_property_locking called the
mcrfpy.Animation(...) constructor, which was removed from the module export
during the API freeze. The Animation type still exists (returned by
drawable.animate()) and every behavior these tests check is intact:
  - hasValidTarget()/complete()/stop() on the returned handle (weak-target RAII)
  - conflict_mode 'replace'/'queue'/'error' + invalid-mode ValueError (#120)

Ported both to drawable.animate(prop, target, seconds, easing, conflict_mode=).
This file is the suite's only conflict_mode coverage, so it was rewritten rather
than deleted. Durations converted ms -> s. Suite now 297/297.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KnywUddaFRhkxo5kijxJnv
This commit is contained in:
John McCardle 2026-06-21 12:11:37 -04:00
commit 5eecb2b2b0
3 changed files with 45 additions and 62 deletions

View file

@ -57,6 +57,7 @@ The active tier1 queue is empty. The last three findings (#309 Caption float→u
- **Phase 5.3** -- documentation regenerated; `tools/generate_stubs_v2.py` rewritten as introspection-based so it can no longer drift from the C++ source.
### Recently Shipped (June 2026)
- **#320** -- `Caption` constructor positional signature now matches its frozen docstring. The docstring advertised `Caption(pos, font, text, ...)` (parallel to `Sprite`/`Entity`, whose 2nd positional is the resource), but the implementation laid its two positional slots out as `(pos, text)` with `font` keyword-only, so `Caption((x,y), None, "text")` raised `TypeError`. Fixed `UICaption::init` to `(pos, font, text)` positional-or-keyword. Audited zero live callers of the old `(pos, text)` 2-positional form. Also rewrote two stale unit tests (`test_animation_raii`, `test_animation_property_locking`) that called the removed `mcrfpy.Animation(...)` constructor to use `drawable.animate(...)` -- preserving the suite's only `conflict_mode` (#120) coverage and the weak-target RAII checks. Suite now 297/297.
- **#317 / #318 / #319** -- The three code-level bugs surfaced by the #314 docstring-accuracy verify pass, fixed together. #317: `automation.scroll()` dropped the x of its position argument (the scroll delta now has its own `injectMouseEvent` parameter, so the real x/y is forwarded). #318: `GridView.texture` always returned `None` (a TODO stub) -- it now returns a `Texture` wrapper (and since `mcrfpy.Grid`/`mcrfpy.GridView` are one type post-#252, both names benefit). #319: `Entity.visible_entities(radius=None)` raised `TypeError` (the `i` format code rejects `None`) -- radius is now parsed as an object so `None`/omitted/`-1` mean "grid default". Regression tests for each; api-surface snapshot re-baselined and docs/stubs regenerated.
- **#316** -- Sparse (windowed) perspective writeback in `UIEntity::updateVisibility`. The demote+promote passes are now clipped to an AABB sized to `fov_radius` (with a `prev_fov` window cache so a moving entity leaves no trailing "ghost vision"), replacing two full-`W*H` walks per entity. The Phase 5.2 benchmark's flat ~25-36 ms/entity writeback overhead on a 1000x1000 grid collapses to single-digit microseconds (384x-6577x on the cheap algorithms; lost in timing noise on the rest). Adversarial verify caught a regression the happy-path test missed -- externally-assigned maps (the documented `from_bytes` load/resume path) need a one-shot full demote (`perspective_full_demote_pending`) since `prev_fov` only bounds engine-promoted cells; fixed and locked with a 7-section regression test.
- **#313** -- `UIEntity::grid` migrated from `shared_ptr<UIGrid>` to `shared_ptr<GridData>` (post-#252 refactor cleanup), adding a new public `entity.texture` read/write property. Merged to master.

View file

@ -2,6 +2,13 @@
"""
Test Animation Property Locking (#120)
Verifies that multiple animations on the same property are handled correctly.
API note: the standalone ``mcrfpy.Animation(...)`` constructor was removed during
the API freeze. Animations are now created via ``drawable.animate(...)``, which
creates+starts the animation and returns the Animation handle. The conflict_mode
semantics ('replace'/'queue'/'error') are unchanged -- they are now a keyword
argument on ``animate()`` instead of on ``Animation.start()``. This file is the
only coverage of conflict_mode in the suite.
"""
import mcrfpy
@ -34,17 +41,11 @@ def test_1_replace_mode_default():
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
# Start first animation
anim1 = mcrfpy.Animation("x", 500.0, 2.0, "linear")
anim1.start(frame) # Default is replace mode
# Start first animation (default conflict_mode is 'replace')
frame.animate("x", 500.0, 2.0, "linear")
# Immediately start second animation on same property
anim2 = mcrfpy.Animation("x", 200.0, 1.0, "linear")
anim2.start(frame) # Should replace anim1
# anim1 should have been completed (jumped to final value)
# and anim2 should now be active
# The frame should be at x=500 (anim1's final value) then animating to 200
# Immediately start second animation on same property -> replaces first
frame.animate("x", 200.0, 1.0, "linear")
# If we got here without error, replace worked
test_result("Replace mode (default)", True)
@ -59,11 +60,8 @@ def test_2_replace_mode_explicit():
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim1 = mcrfpy.Animation("x", 500.0, 2.0, "linear")
anim1.start(frame, conflict_mode="replace")
anim2 = mcrfpy.Animation("x", 200.0, 1.0, "linear")
anim2.start(frame, conflict_mode="replace")
frame.animate("x", 500.0, 2.0, "linear", conflict_mode="replace")
frame.animate("x", 200.0, 1.0, "linear", conflict_mode="replace")
test_result("Replace mode (explicit)", True)
except Exception as e:
@ -78,15 +76,12 @@ def test_3_queue_mode():
ui.append(frame)
# Start first animation (short duration for test)
anim1 = mcrfpy.Animation("y", 300.0, 0.5, "linear")
anim1.start(frame)
frame.animate("y", 300.0, 0.5, "linear")
# Queue second animation
anim2 = mcrfpy.Animation("y", 100.0, 0.5, "linear")
anim2.start(frame, conflict_mode="queue")
# Queue second animation -> starts after the first completes
frame.animate("y", 100.0, 0.5, "linear", conflict_mode="queue")
# Both should be accepted without error
# anim2 will start after anim1 completes
test_result("Queue mode", True)
except Exception as e:
test_result("Queue mode", False, str(e))
@ -99,13 +94,11 @@ def test_4_error_mode():
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim1 = mcrfpy.Animation("w", 200.0, 2.0, "linear")
anim1.start(frame)
frame.animate("w", 200.0, 2.0, "linear")
# Try to start second animation with error mode
anim2 = mcrfpy.Animation("w", 300.0, 1.0, "linear")
try:
anim2.start(frame, conflict_mode="error")
frame.animate("w", 300.0, 1.0, "linear", conflict_mode="error")
test_result("Error mode", False, "Expected RuntimeError but none was raised")
except RuntimeError as e:
# This is expected!
@ -124,9 +117,8 @@ def test_5_invalid_conflict_mode():
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("h", 200.0, 1.0, "linear")
try:
anim.start(frame, conflict_mode="invalid_mode")
frame.animate("h", 200.0, 1.0, "linear", conflict_mode="invalid_mode")
test_result("Invalid conflict_mode", False, "Expected ValueError but none raised")
except ValueError as e:
if "invalid" in str(e).lower():
@ -144,14 +136,10 @@ def test_6_different_properties_no_conflict():
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
# Animate different properties - should not conflict
anim_x = mcrfpy.Animation("x", 500.0, 1.0, "linear")
anim_y = mcrfpy.Animation("y", 500.0, 1.0, "linear")
anim_w = mcrfpy.Animation("w", 200.0, 1.0, "linear")
anim_x.start(frame, conflict_mode="error")
anim_y.start(frame, conflict_mode="error")
anim_w.start(frame, conflict_mode="error")
# Animate different properties with error mode - should not conflict
frame.animate("x", 500.0, 1.0, "linear", conflict_mode="error")
frame.animate("y", 500.0, 1.0, "linear", conflict_mode="error")
frame.animate("w", 200.0, 1.0, "linear", conflict_mode="error")
# All should succeed without error since they're different properties
test_result("Different properties no conflict", True)
@ -171,11 +159,8 @@ def test_7_different_targets_no_conflict():
ui.append(frame2)
# Same property, different targets - should not conflict
anim1 = mcrfpy.Animation("x", 500.0, 1.0, "linear")
anim2 = mcrfpy.Animation("x", 600.0, 1.0, "linear")
anim1.start(frame1, conflict_mode="error")
anim2.start(frame2, conflict_mode="error")
frame1.animate("x", 500.0, 1.0, "linear", conflict_mode="error")
frame2.animate("x", 600.0, 1.0, "linear", conflict_mode="error")
test_result("Different targets no conflict", True)
except RuntimeError as e:
@ -191,16 +176,13 @@ def test_8_replace_completes_old():
frame = mcrfpy.Frame(pos=(0, 0), size=(100, 100))
ui.append(frame)
# Start animation to move x to 500
anim1 = mcrfpy.Animation("x", 500.0, 10.0, "linear") # Long duration
anim1.start(frame)
# Start animation to move x to 500 (long duration)
frame.animate("x", 500.0, 10.0, "linear")
# Immediately replace - should complete anim1 (jump to 500)
anim2 = mcrfpy.Animation("x", 200.0, 1.0, "linear")
anim2.start(frame, conflict_mode="replace")
# Immediately replace - should complete the old animation (jump to 500)
frame.animate("x", 200.0, 1.0, "linear", conflict_mode="replace")
# Frame should now be at x=500 (anim1's final) and animating to 200
# Due to immediate completion, x should equal 500 right now
# Due to immediate completion of the replaced animation, x should be 500 now
if frame.x == 500.0:
test_result("Replace completes old animation", True)
else:

View file

@ -3,6 +3,12 @@
Test the RAII AnimationManager implementation.
This verifies that weak_ptr properly handles all crash scenarios.
Uses mcrfpy.step() for synchronous test execution.
API note: the standalone ``mcrfpy.Animation(...)`` constructor was removed during
the API freeze. Animations are created via ``drawable.animate(prop, target,
duration_seconds, easing)``, which returns the Animation handle. The handle still
exposes ``hasValidTarget()``, ``complete()``, and ``stop()`` -- the weak_ptr
target-lifetime safety this suite checks is unchanged. (Durations are in seconds.)
"""
import mcrfpy
@ -48,8 +54,7 @@ try:
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("x", 200.0, 1000, "linear")
anim.start(frame)
anim = frame.animate("x", 200.0, 1.0, "linear")
if hasattr(anim, 'hasValidTarget'):
valid = anim.hasValidTarget()
@ -59,13 +64,12 @@ try:
except Exception as e:
test_result("Basic animation", False, str(e))
# Test 2: Remove animated object - shared_ptr stays alive while Python ref exists
# Test 2: Remove animated object - target invalid once last shared_ptr drops
try:
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("x", 500.0, 2000, "easeInOut")
anim.start(frame)
anim = frame.animate("x", 500.0, 2.0, "easeInOut")
ui.remove(frame)
# Note: frame still holds a shared_ptr reference, so target is still valid
@ -85,8 +89,7 @@ try:
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("x", 500.0, 2000, "linear")
anim.start(frame)
anim = frame.animate("x", 500.0, 2.0, "linear")
if hasattr(anim, 'complete'):
anim.complete()
@ -96,14 +99,13 @@ try:
except Exception as e:
test_result("Animation complete method", False, str(e))
# Test 4: Multiple animations rapidly
# Test 4: Multiple animations rapidly (each replaces the prior on 'x')
try:
frame = mcrfpy.Frame(pos=(200, 200), size=(100, 100))
ui.append(frame)
for i in range(10):
anim = mcrfpy.Animation("x", 300.0 + i * 10, 1000, "linear")
anim.start(frame)
frame.animate("x", 300.0 + i * 10, 1.0, "linear")
test_result("Multiple animations rapidly", True)
except Exception as e:
@ -116,8 +118,7 @@ try:
for i in range(5):
frame = mcrfpy.Frame(pos=(50 * i, 100), size=(40, 40))
ui.append(frame)
anim = mcrfpy.Animation("y", 300.0, 2000, "easeOutBounce")
anim.start(frame)
frame.animate("y", 300.0, 2.0, "easeOutBounce")
test2.activate()
mcrfpy.step(0.1)
@ -132,8 +133,7 @@ except Exception as e:
try:
frame = mcrfpy.Frame(pos=(100, 100), size=(100, 100))
ui.append(frame)
anim = mcrfpy.Animation("w", 200.0, 1500, "easeInOutCubic")
anim.start(frame)
anim = frame.animate("w", 200.0, 1.5, "easeInOutCubic")
# Clear all UI except background - iterate in reverse
for i in range(len(ui) - 1, 0, -1):