""" Tests for engine.effects module. """ from engine.effects import EffectChain, EffectConfig, EffectContext, EffectRegistry class MockEffect: name = "mock" config = EffectConfig(enabled=True, intensity=1.0) def __init__(self): self.processed = False self.last_ctx = None def process(self, buf, ctx): self.processed = True self.last_ctx = ctx return buf + ["processed"] def configure(self, config): self.config = config class TestEffectConfig: def test_defaults(self): cfg = EffectConfig() assert cfg.enabled is True assert cfg.intensity == 1.0 assert cfg.params == {} def test_custom_values(self): cfg = EffectConfig(enabled=False, intensity=0.5, params={"key": "value"}) assert cfg.enabled is False assert cfg.intensity == 0.5 assert cfg.params == {"key": "value"} class TestEffectContext: def test_defaults(self): ctx = EffectContext( terminal_width=80, terminal_height=24, scroll_cam=0, ticker_height=20, mic_excess=0.0, grad_offset=0.0, frame_number=0, has_message=False, ) assert ctx.terminal_width == 80 assert ctx.terminal_height == 24 assert ctx.ticker_height == 20 assert ctx.items == [] def test_with_items(self): items = [("Title", "Source", "12:00")] ctx = EffectContext( terminal_width=80, terminal_height=24, scroll_cam=0, ticker_height=20, mic_excess=0.0, grad_offset=0.0, frame_number=0, has_message=False, items=items, ) assert ctx.items == items class TestEffectRegistry: def test_init_empty(self): registry = EffectRegistry() assert len(registry.list_all()) == 0 def test_register(self): registry = EffectRegistry() effect = MockEffect() registry.register(effect) assert "mock" in registry.list_all() def test_get(self): registry = EffectRegistry() effect = MockEffect() registry.register(effect) retrieved = registry.get("mock") assert retrieved is effect def test_get_nonexistent(self): registry = EffectRegistry() assert registry.get("nonexistent") is None def test_enable(self): registry = EffectRegistry() effect = MockEffect() effect.config.enabled = False registry.register(effect) registry.enable("mock") assert effect.config.enabled is True def test_disable(self): registry = EffectRegistry() effect = MockEffect() effect.config.enabled = True registry.register(effect) registry.disable("mock") assert effect.config.enabled is False def test_list_enabled(self): registry = EffectRegistry() class EnabledEffect: name = "enabled_effect" config = EffectConfig(enabled=True, intensity=1.0) class DisabledEffect: name = "disabled_effect" config = EffectConfig(enabled=False, intensity=1.0) registry.register(EnabledEffect()) registry.register(DisabledEffect()) enabled = registry.list_enabled() assert len(enabled) == 1 assert enabled[0].name == "enabled_effect" def test_configure(self): registry = EffectRegistry() effect = MockEffect() registry.register(effect) new_config = EffectConfig(enabled=False, intensity=0.3) registry.configure("mock", new_config) assert effect.config.enabled is False assert effect.config.intensity == 0.3 def test_is_enabled(self): registry = EffectRegistry() effect = MockEffect() effect.config.enabled = True registry.register(effect) assert registry.is_enabled("mock") is True assert registry.is_enabled("nonexistent") is False class TestEffectChain: def test_init(self): registry = EffectRegistry() chain = EffectChain(registry) assert chain.get_order() == [] def test_set_order(self): registry = EffectRegistry() effect1 = MockEffect() effect1.name = "effect1" effect2 = MockEffect() effect2.name = "effect2" registry.register(effect1) registry.register(effect2) chain = EffectChain(registry) chain.set_order(["effect1", "effect2"]) assert chain.get_order() == ["effect1", "effect2"] def test_add_effect(self): registry = EffectRegistry() effect = MockEffect() effect.name = "test_effect" registry.register(effect) chain = EffectChain(registry) chain.add_effect("test_effect") assert "test_effect" in chain.get_order() def test_add_effect_invalid(self): registry = EffectRegistry() chain = EffectChain(registry) result = chain.add_effect("nonexistent") assert result is False def test_remove_effect(self): registry = EffectRegistry() effect = MockEffect() effect.name = "test_effect" registry.register(effect) chain = EffectChain(registry) chain.set_order(["test_effect"]) chain.remove_effect("test_effect") assert "test_effect" not in chain.get_order() def test_reorder(self): registry = EffectRegistry() effect1 = MockEffect() effect1.name = "effect1" effect2 = MockEffect() effect2.name = "effect2" effect3 = MockEffect() effect3.name = "effect3" registry.register(effect1) registry.register(effect2) registry.register(effect3) chain = EffectChain(registry) chain.set_order(["effect1", "effect2", "effect3"]) result = chain.reorder(["effect3", "effect1", "effect2"]) assert result is True assert chain.get_order() == ["effect3", "effect1", "effect2"] def test_reorder_invalid(self): registry = EffectRegistry() effect = MockEffect() effect.name = "effect1" registry.register(effect) chain = EffectChain(registry) result = chain.reorder(["effect1", "nonexistent"]) assert result is False def test_process_empty_chain(self): registry = EffectRegistry() chain = EffectChain(registry) buf = ["line1", "line2"] ctx = EffectContext( terminal_width=80, terminal_height=24, scroll_cam=0, ticker_height=20, mic_excess=0.0, grad_offset=0.0, frame_number=0, has_message=False, ) result = chain.process(buf, ctx) assert result == buf def test_process_with_effects(self): registry = EffectRegistry() effect = MockEffect() effect.name = "test_effect" registry.register(effect) chain = EffectChain(registry) chain.set_order(["test_effect"]) buf = ["line1", "line2"] ctx = EffectContext( terminal_width=80, terminal_height=24, scroll_cam=0, ticker_height=20, mic_excess=0.0, grad_offset=0.0, frame_number=0, has_message=False, ) result = chain.process(buf, ctx) assert result == ["line1", "line2", "processed"] assert effect.processed is True assert effect.last_ctx is ctx def test_process_disabled_effect(self): registry = EffectRegistry() effect = MockEffect() effect.name = "test_effect" effect.config.enabled = False registry.register(effect) chain = EffectChain(registry) chain.set_order(["test_effect"]) buf = ["line1"] ctx = EffectContext( terminal_width=80, terminal_height=24, scroll_cam=0, ticker_height=20, mic_excess=0.0, grad_offset=0.0, frame_number=0, has_message=False, ) result = chain.process(buf, ctx) assert result == ["line1"] assert effect.processed is False class TestEffectsExports: def test_all_exports_are_importable(self): """Verify all exports in __all__ can actually be imported.""" import engine.effects as effects_module for name in effects_module.__all__: getattr(effects_module, name) class TestPerformanceMonitor: def test_empty_stats(self): from engine.effects.performance import PerformanceMonitor monitor = PerformanceMonitor() stats = monitor.get_stats() assert "error" in stats def test_record_and_retrieve(self): from engine.effects.performance import PerformanceMonitor monitor = PerformanceMonitor() monitor.start_frame(1) monitor.record_effect("test_effect", 1.5, 100, 150) monitor.end_frame(1, 2.0) stats = monitor.get_stats() assert "error" not in stats assert stats["frame_count"] == 1 assert "test_effect" in stats["effects"] def test_multiple_frames(self): from engine.effects.performance import PerformanceMonitor monitor = PerformanceMonitor(max_frames=3) for i in range(5): monitor.start_frame(i) monitor.record_effect("effect1", 1.0, 100, 100) monitor.record_effect("effect2", 0.5, 100, 100) monitor.end_frame(i, 1.5) stats = monitor.get_stats() assert stats["frame_count"] == 3 assert "effect1" in stats["effects"] assert "effect2" in stats["effects"] def test_reset(self): from engine.effects.performance import PerformanceMonitor monitor = PerformanceMonitor() monitor.start_frame(1) monitor.record_effect("test", 1.0, 100, 100) monitor.end_frame(1, 1.0) monitor.reset() stats = monitor.get_stats() assert "error" in stats class TestEffectPipelinePerformance: def test_pipeline_stays_within_frame_budget(self): """Verify effect pipeline completes within frame budget (33ms for 30fps).""" from engine.effects import ( EffectChain, EffectConfig, EffectContext, EffectRegistry, ) class DummyEffect: name = "dummy" config = EffectConfig(enabled=True, intensity=1.0) def process(self, buf, ctx): return [line * 2 for line in buf] registry = EffectRegistry() registry.register(DummyEffect()) from engine.effects.performance import PerformanceMonitor monitor = PerformanceMonitor(max_frames=10) chain = EffectChain(registry, monitor) chain.set_order(["dummy"]) buf = ["x" * 80] * 20 for i in range(10): ctx = EffectContext( terminal_width=80, terminal_height=24, scroll_cam=0, ticker_height=20, mic_excess=0.0, grad_offset=0.0, frame_number=i, has_message=False, ) chain.process(buf, ctx) stats = monitor.get_stats() assert "error" not in stats assert stats["pipeline"]["max_ms"] < 33.0 def test_individual_effects_performance(self): """Verify individual effects don't exceed 10ms per frame.""" from engine.effects import ( EffectChain, EffectConfig, EffectContext, EffectRegistry, ) class SlowEffect: name = "slow" config = EffectConfig(enabled=True, intensity=1.0) def process(self, buf, ctx): result = [] for line in buf: result.append(line) result.append(line + line) return result registry = EffectRegistry() registry.register(SlowEffect()) from engine.effects.performance import PerformanceMonitor monitor = PerformanceMonitor(max_frames=5) chain = EffectChain(registry, monitor) chain.set_order(["slow"]) buf = ["x" * 80] * 10 for i in range(5): ctx = EffectContext( terminal_width=80, terminal_height=24, scroll_cam=0, ticker_height=20, mic_excess=0.0, grad_offset=0.0, frame_number=i, has_message=False, ) chain.process(buf, ctx) stats = monitor.get_stats() assert "error" not in stats assert stats["effects"]["slow"]["max_ms"] < 10.0