Files
RFP_Infinity-Vis/tests/test_controller.py

182 lines
7.4 KiB
Python

from __future__ import annotations
from pathlib import Path
import unittest
from app.core.controller import InfinityMirrorController
from app.core.pattern_engine import PatternEngine
ROOT = Path(__file__).resolve().parents[1]
SAMPLE_MAPPING = ROOT / "sample_data" / "infinity_mirror_mapping_clean.xml"
class ControllerTests(unittest.TestCase):
def test_scene_state_returns_snapshot_instead_of_live_mutable_state(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
controller.set_foh_mode(True)
controller.set_pattern("sparkle")
observed_events: list[str] = []
controller.state_changed.connect(lambda: observed_events.append("state"))
snapshot = controller.scene_state("next")
snapshot.pattern_id = "solid"
snapshot.params.primary_color = "#FFFFFF"
self.assertEqual(controller.scene_state("live").pattern_id, "solid")
self.assertEqual(controller.scene_state("next").pattern_id, "sparkle")
self.assertEqual(controller.scene_state("next").params.primary_color, "#4D7CFF")
self.assertEqual(observed_events, [])
finally:
controller.timer.stop()
def test_params_property_returns_snapshot_copy(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
observed_events: list[str] = []
controller.state_changed.connect(lambda: observed_events.append("state"))
params = controller.params
params.strobe_duty_cycle = 0.12
params.flip_horizontal = True
self.assertEqual(controller.params.strobe_duty_cycle, 0.5)
self.assertFalse(controller.params.flip_horizontal)
self.assertEqual(observed_events, [])
finally:
controller.timer.stop()
def test_numeric_parameter_values_are_coerced_and_sanitized(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
controller.set_parameter("strobe_duty_cycle", "0.99")
self.assertEqual(controller.params.strobe_duty_cycle, 0.98)
finally:
controller.timer.stop()
def test_invalid_numeric_parameter_value_is_ignored(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
initial = controller.params.strobe_duty_cycle
controller.set_parameter("strobe_duty_cycle", "invalid")
self.assertEqual(controller.params.strobe_duty_cycle, initial)
finally:
controller.timer.stop()
def test_parameter_updates_preserve_existing_scan_flags(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
controller.set_parameter("flip_horizontal", True)
controller.set_parameter("flip_vertical", True)
controller.set_parameter("band_thickness", 1.6)
controller.set_parameter("strobe_duty_cycle", 0.4)
params = controller.params
self.assertTrue(params.flip_horizontal)
self.assertTrue(params.flip_vertical)
self.assertEqual(params.band_thickness, 1.6)
self.assertEqual(params.strobe_duty_cycle, 0.4)
finally:
controller.timer.stop()
def test_single_preview_mode_keeps_next_scene_synced_with_live(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
controller.set_pattern("sparkle")
controller.set_tempo_bpm(132.0)
self.assertEqual(controller.scene_state("live").pattern_id, "sparkle")
self.assertEqual(controller.scene_state("next").pattern_id, "sparkle")
self.assertEqual(controller.tempo_bpm, 132.0)
finally:
controller.timer.stop()
def test_foh_mode_edits_next_scene_without_touching_live(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
controller.set_foh_mode(True)
controller.set_pattern("sparkle")
controller.set_tempo_bpm(140.0)
self.assertEqual(controller.scene_state("live").pattern_id, "solid")
self.assertEqual(controller.scene_state("next").pattern_id, "sparkle")
self.assertEqual(controller.pattern_id, "sparkle")
self.assertAlmostEqual(controller.tempo_bpm, 140.0)
finally:
controller.timer.stop()
def test_go_promotes_next_scene_to_live(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
controller.set_foh_mode(True)
controller.set_pattern("sparkle")
controller.set_tempo_bpm(126.0)
controller.go_scene()
self.assertEqual(controller.scene_state("live").pattern_id, "sparkle")
self.assertAlmostEqual(controller.tempo_bpm, 126.0)
self.assertEqual(controller.scene_state("next").pattern_id, "sparkle")
finally:
controller.timer.stop()
def test_fade_go_commits_queued_scene_after_transition(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
controller.load_mapping(ROOT / "sample_data" / "infinity_mirror_mapping_clean.xml")
controller.set_foh_mode(True)
controller.set_pattern("sparkle")
controller.set_tempo_bpm(144.0)
controller.render_once(timestamp=10.0)
controller.fade_go(0.5, timestamp=10.0)
self.assertTrue(controller.transition_active)
self.assertEqual(controller.scene_state("live").pattern_id, "solid")
self.assertEqual(controller.scene_state("next").pattern_id, "sparkle")
self.assertIsNotNone(controller.preview_frame_for("next"))
controller.render_once(timestamp=10.6)
self.assertFalse(controller.transition_active)
self.assertEqual(controller.scene_state("live").pattern_id, "sparkle")
self.assertEqual(controller.current_frame.pattern_id, "sparkle")
finally:
controller.timer.stop()
def test_legacy_speed_parameter_maps_to_global_tempo(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
controller.set_parameter("speed", 2.0)
self.assertEqual(controller.tempo_bpm, 120.0)
finally:
controller.timer.stop()
def test_tempo_change_preserves_current_visual_phase(self) -> None:
controller = InfinityMirrorController(ROOT)
try:
controller.load_mapping(SAMPLE_MAPPING)
controller.set_pattern("center_pulse")
controller.set_parameter("color_mode", "mono")
controller.set_parameter("primary_color", "#FFFFFF")
controller.set_parameter("secondary_color", "#000000")
def active_tiles(timestamp: float) -> set[str]:
frame = controller._render_scene_frame(PatternEngine(), controller.scene_state("live"), timestamp)
return {
tile_id
for tile_id, tile in frame.tiles.items()
if max(tile.fill_color.to_8bit_tuple()) >= 240
}
before = active_tiles(10.0)
controller.set_tempo_bpm(240.0, timestamp=10.0)
after = active_tiles(10.0)
self.assertEqual(before, after)
advanced = active_tiles(10.26)
self.assertNotEqual(after, advanced)
finally:
controller.timer.stop()
if __name__ == "__main__":
unittest.main()