feat(tests): improve coverage to 56%, add benchmark regression tests

- Add EffectPlugin ABC with @abstractmethod decorators for interface enforcement
- Add runtime interface checking in discover_plugins() with issubclass()
- Add EffectContext factory with sensible defaults
- Standardize Display __init__ (remove redundant init in TerminalDisplay)
- Document effect behavior when ticker_height=0
- Evaluate legacy effects: document coexistence, no deprecation needed
- Research plugin patterns (VST, Python entry points)
- Fix pysixel dependency (removed broken dependency)

Test coverage improvements:
- Add DisplayRegistry tests
- Add MultiDisplay tests
- Add SixelDisplay tests
- Add controller._get_display tests
- Add effects controller command handling tests
- Add benchmark regression tests (@pytest.mark.benchmark)
- Add pytest marker for benchmark tests in pyproject.toml

Documentation updates:
- Update AGENTS.md with 56% coverage stats and effect plugin docs
- Update README.md with Sixel display mode and benchmark commands
- Add new modules to architecture section
This commit is contained in:
2026-03-15 23:26:10 -07:00
parent dcd31469a5
commit 9e4d54a82e
17 changed files with 768 additions and 45 deletions

100
tests/test_benchmark.py Normal file
View File

@@ -0,0 +1,100 @@
"""
Tests for engine.benchmark module - performance regression tests.
"""
from unittest.mock import patch
import pytest
from engine.display import NullDisplay
class TestBenchmarkNullDisplay:
"""Performance tests for NullDisplay - regression tests."""
@pytest.mark.benchmark
def test_null_display_minimum_fps(self):
"""NullDisplay should meet minimum performance threshold."""
import time
display = NullDisplay()
display.init(80, 24)
buffer = ["x" * 80 for _ in range(24)]
iterations = 1000
start = time.perf_counter()
for _ in range(iterations):
display.show(buffer)
elapsed = time.perf_counter() - start
fps = iterations / elapsed
min_fps = 20000
assert fps >= min_fps, f"NullDisplay FPS {fps:.0f} below minimum {min_fps}"
@pytest.mark.benchmark
def test_effects_minimum_throughput(self):
"""Effects should meet minimum processing throughput."""
import time
from effects_plugins import discover_plugins
from engine.effects import EffectContext, get_registry
discover_plugins()
registry = get_registry()
effect = registry.get("noise")
assert effect is not None, "Noise effect should be registered"
buffer = ["x" * 80 for _ in range(24)]
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,
)
iterations = 500
start = time.perf_counter()
for _ in range(iterations):
effect.process(buffer, ctx)
elapsed = time.perf_counter() - start
fps = iterations / elapsed
min_fps = 10000
assert fps >= min_fps, (
f"Effect processing FPS {fps:.0f} below minimum {min_fps}"
)
class TestBenchmarkWebSocketDisplay:
"""Performance tests for WebSocketDisplay."""
@pytest.mark.benchmark
def test_websocket_display_minimum_fps(self):
"""WebSocketDisplay should meet minimum performance threshold."""
import time
with patch("engine.display.backends.websocket.websockets", None):
from engine.display import WebSocketDisplay
display = WebSocketDisplay()
display.init(80, 24)
buffer = ["x" * 80 for _ in range(24)]
iterations = 500
start = time.perf_counter()
for _ in range(iterations):
display.show(buffer)
elapsed = time.perf_counter() - start
fps = iterations / elapsed
min_fps = 10000
assert fps >= min_fps, (
f"WebSocketDisplay FPS {fps:.0f} below minimum {min_fps}"
)

View File

@@ -5,7 +5,75 @@ Tests for engine.controller module.
from unittest.mock import MagicMock, patch
from engine import config
from engine.controller import StreamController
from engine.controller import StreamController, _get_display
class TestGetDisplay:
"""Tests for _get_display function."""
@patch("engine.controller.WebSocketDisplay")
@patch("engine.controller.TerminalDisplay")
def test_get_display_terminal(self, mock_terminal, mock_ws):
"""returns TerminalDisplay for display=terminal."""
mock_terminal.return_value = MagicMock()
mock_ws.return_value = MagicMock()
cfg = config.Config(display="terminal")
display = _get_display(cfg)
mock_terminal.assert_called()
assert isinstance(display, MagicMock)
@patch("engine.controller.WebSocketDisplay")
@patch("engine.controller.TerminalDisplay")
def test_get_display_websocket(self, mock_terminal, mock_ws):
"""returns WebSocketDisplay for display=websocket."""
mock_ws_instance = MagicMock()
mock_ws.return_value = mock_ws_instance
mock_terminal.return_value = MagicMock()
cfg = config.Config(display="websocket")
_get_display(cfg)
mock_ws.assert_called()
mock_ws_instance.start_server.assert_called()
mock_ws_instance.start_http_server.assert_called()
@patch("engine.controller.SixelDisplay")
def test_get_display_sixel(self, mock_sixel):
"""returns SixelDisplay for display=sixel."""
mock_sixel.return_value = MagicMock()
cfg = config.Config(display="sixel")
_get_display(cfg)
mock_sixel.assert_called()
def test_get_display_unknown_returns_null(self):
"""returns NullDisplay for unknown display mode."""
cfg = config.Config(display="unknown")
display = _get_display(cfg)
from engine.display import NullDisplay
assert isinstance(display, NullDisplay)
@patch("engine.controller.WebSocketDisplay")
@patch("engine.controller.TerminalDisplay")
@patch("engine.controller.MultiDisplay")
def test_get_display_both(self, mock_multi, mock_terminal, mock_ws):
"""returns MultiDisplay for display=both."""
mock_terminal_instance = MagicMock()
mock_ws_instance = MagicMock()
mock_terminal.return_value = mock_terminal_instance
mock_ws.return_value = mock_ws_instance
cfg = config.Config(display="both")
_get_display(cfg)
mock_multi.assert_called()
call_args = mock_multi.call_args[0][0]
assert mock_terminal_instance in call_args
assert mock_ws_instance in call_args
class TestStreamController:
@@ -68,6 +136,24 @@ class TestStreamController:
assert mic_ok is False
assert ntfy_ok is True
@patch("engine.controller.MicMonitor")
def test_initialize_sources_cc_subscribed(self, mock_mic):
"""initialize_sources subscribes C&C handler."""
mock_mic_instance = MagicMock()
mock_mic_instance.available = False
mock_mic_instance.start.return_value = False
mock_mic.return_value = mock_mic_instance
with patch("engine.controller.NtfyPoller") as mock_ntfy:
mock_ntfy_instance = MagicMock()
mock_ntfy_instance.start.return_value = True
mock_ntfy.return_value = mock_ntfy_instance
controller = StreamController()
controller.initialize_sources()
mock_ntfy_instance.subscribe.assert_called()
class TestStreamControllerCleanup:
"""Tests for StreamController cleanup."""

View File

@@ -2,7 +2,10 @@
Tests for engine.display module.
"""
from engine.display import NullDisplay, TerminalDisplay
from unittest.mock import MagicMock
from engine.display import DisplayRegistry, NullDisplay, TerminalDisplay
from engine.display.backends.multi import MultiDisplay
class TestDisplayProtocol:
@@ -25,6 +28,66 @@ class TestDisplayProtocol:
assert hasattr(display, "cleanup")
class TestDisplayRegistry:
"""Tests for DisplayRegistry class."""
def setup_method(self):
"""Reset registry before each test."""
DisplayRegistry._backends = {}
DisplayRegistry._initialized = False
def test_register_adds_backend(self):
"""register adds a backend to the registry."""
DisplayRegistry.register("test", TerminalDisplay)
assert DisplayRegistry.get("test") == TerminalDisplay
def test_register_case_insensitive(self):
"""register is case insensitive."""
DisplayRegistry.register("TEST", TerminalDisplay)
assert DisplayRegistry.get("test") == TerminalDisplay
def test_get_returns_none_for_unknown(self):
"""get returns None for unknown backend."""
assert DisplayRegistry.get("unknown") is None
def test_list_backends_returns_all(self):
"""list_backends returns all registered backends."""
DisplayRegistry.register("a", TerminalDisplay)
DisplayRegistry.register("b", NullDisplay)
backends = DisplayRegistry.list_backends()
assert "a" in backends
assert "b" in backends
def test_create_returns_instance(self):
"""create returns a display instance."""
DisplayRegistry.register("test", NullDisplay)
display = DisplayRegistry.create("test")
assert isinstance(display, NullDisplay)
def test_create_returns_none_for_unknown(self):
"""create returns None for unknown backend."""
display = DisplayRegistry.create("unknown")
assert display is None
def test_initialize_registers_defaults(self):
"""initialize registers default backends."""
DisplayRegistry.initialize()
assert DisplayRegistry.get("terminal") == TerminalDisplay
assert DisplayRegistry.get("null") == NullDisplay
from engine.display.backends.sixel import SixelDisplay
from engine.display.backends.websocket import WebSocketDisplay
assert DisplayRegistry.get("websocket") == WebSocketDisplay
assert DisplayRegistry.get("sixel") == SixelDisplay
def test_initialize_idempotent(self):
"""initialize can be called multiple times safely."""
DisplayRegistry.initialize()
DisplayRegistry._backends["custom"] = TerminalDisplay
DisplayRegistry.initialize()
assert "custom" in DisplayRegistry.list_backends()
class TestTerminalDisplay:
"""Tests for TerminalDisplay class."""
@@ -77,3 +140,62 @@ class TestNullDisplay:
"""cleanup does nothing."""
display = NullDisplay()
display.cleanup()
class TestMultiDisplay:
"""Tests for MultiDisplay class."""
def test_init_stores_dimensions(self):
"""init stores dimensions and forwards to displays."""
mock_display1 = MagicMock()
mock_display2 = MagicMock()
multi = MultiDisplay([mock_display1, mock_display2])
multi.init(120, 40)
assert multi.width == 120
assert multi.height == 40
mock_display1.init.assert_called_once_with(120, 40)
mock_display2.init.assert_called_once_with(120, 40)
def test_show_forwards_to_all_displays(self):
"""show forwards buffer to all displays."""
mock_display1 = MagicMock()
mock_display2 = MagicMock()
multi = MultiDisplay([mock_display1, mock_display2])
buffer = ["line1", "line2"]
multi.show(buffer)
mock_display1.show.assert_called_once_with(buffer)
mock_display2.show.assert_called_once_with(buffer)
def test_clear_forwards_to_all_displays(self):
"""clear forwards to all displays."""
mock_display1 = MagicMock()
mock_display2 = MagicMock()
multi = MultiDisplay([mock_display1, mock_display2])
multi.clear()
mock_display1.clear.assert_called_once()
mock_display2.clear.assert_called_once()
def test_cleanup_forwards_to_all_displays(self):
"""cleanup forwards to all displays."""
mock_display1 = MagicMock()
mock_display2 = MagicMock()
multi = MultiDisplay([mock_display1, mock_display2])
multi.cleanup()
mock_display1.cleanup.assert_called_once()
mock_display2.cleanup.assert_called_once()
def test_empty_displays_list(self):
"""handles empty displays list gracefully."""
multi = MultiDisplay([])
multi.init(80, 24)
multi.show(["test"])
multi.clear()
multi.cleanup()

View File

@@ -5,8 +5,10 @@ Tests for engine.effects.controller module.
from unittest.mock import MagicMock, patch
from engine.effects.controller import (
_format_stats,
handle_effects_command,
set_effect_chain_ref,
show_effects_menu,
)
@@ -92,6 +94,29 @@ class TestHandleEffectsCommand:
assert "Reordered pipeline" in result
mock_chain_instance.reorder.assert_called_once_with(["noise", "fade"])
def test_reorder_failure(self):
"""reorder returns error on failure."""
with patch("engine.effects.controller.get_registry") as mock_registry:
mock_registry.return_value.list_all.return_value = {}
with patch("engine.effects.controller._get_effect_chain") as mock_chain:
mock_chain_instance = MagicMock()
mock_chain_instance.reorder.return_value = False
mock_chain.return_value = mock_chain_instance
result = handle_effects_command("/effects reorder bad")
assert "Failed to reorder" in result
def test_unknown_effect(self):
"""unknown effect returns error."""
with patch("engine.effects.controller.get_registry") as mock_registry:
mock_registry.return_value.list_all.return_value = {}
result = handle_effects_command("/effects unknown on")
assert "Unknown effect" in result
def test_unknown_command(self):
"""unknown command returns error."""
result = handle_effects_command("/unknown")
@@ -102,6 +127,105 @@ class TestHandleEffectsCommand:
result = handle_effects_command("not a command")
assert "Unknown command" in result
def test_invalid_intensity_value(self):
"""invalid intensity value returns error."""
with patch("engine.effects.controller.get_registry") as mock_registry:
mock_plugin = MagicMock()
mock_registry.return_value.get.return_value = mock_plugin
mock_registry.return_value.list_all.return_value = {"noise": mock_plugin}
result = handle_effects_command("/effects noise intensity bad")
assert "Invalid intensity" in result
def test_missing_action(self):
"""missing action returns usage."""
with patch("engine.effects.controller.get_registry") as mock_registry:
mock_plugin = MagicMock()
mock_registry.return_value.get.return_value = mock_plugin
mock_registry.return_value.list_all.return_value = {"noise": mock_plugin}
result = handle_effects_command("/effects noise")
assert "Usage" in result
def test_stats_command(self):
"""stats command returns formatted stats."""
with patch("engine.effects.controller.get_monitor") as mock_monitor:
mock_monitor.return_value.get_stats.return_value = {
"frame_count": 100,
"pipeline": {"avg_ms": 1.5, "min_ms": 1.0, "max_ms": 2.0},
"effects": {},
}
result = handle_effects_command("/effects stats")
assert "Performance Stats" in result
def test_list_only_effects(self):
"""list command works with just /effects."""
with patch("engine.effects.controller.get_registry") as mock_registry:
mock_plugin = MagicMock()
mock_plugin.config.enabled = False
mock_plugin.config.intensity = 0.5
mock_registry.return_value.list_all.return_value = {"noise": mock_plugin}
with patch("engine.effects.controller._get_effect_chain") as mock_chain:
mock_chain.return_value = None
result = handle_effects_command("/effects")
assert "noise: OFF" in result
class TestShowEffectsMenu:
"""Tests for show_effects_menu function."""
def test_returns_formatted_menu(self):
"""returns formatted effects menu."""
with patch("engine.effects.controller.get_registry") as mock_registry:
mock_plugin = MagicMock()
mock_plugin.config.enabled = True
mock_plugin.config.intensity = 0.75
mock_registry.return_value.list_all.return_value = {"noise": mock_plugin}
with patch("engine.effects.controller._get_effect_chain") as mock_chain:
mock_chain_instance = MagicMock()
mock_chain_instance.get_order.return_value = ["noise"]
mock_chain.return_value = mock_chain_instance
result = show_effects_menu()
assert "EFFECTS MENU" in result
assert "noise" in result
class TestFormatStats:
"""Tests for _format_stats function."""
def test_returns_error_when_no_monitor(self):
"""returns error when monitor unavailable."""
with patch("engine.effects.controller.get_monitor") as mock_monitor:
mock_monitor.return_value.get_stats.return_value = {"error": "No data"}
result = _format_stats()
assert "No data" in result
def test_formats_pipeline_stats(self):
"""formats pipeline stats correctly."""
with patch("engine.effects.controller.get_monitor") as mock_monitor:
mock_monitor.return_value.get_stats.return_value = {
"frame_count": 50,
"pipeline": {"avg_ms": 2.5, "min_ms": 2.0, "max_ms": 3.0},
"effects": {"noise": {"avg_ms": 0.5, "min_ms": 0.4, "max_ms": 0.6}},
}
result = _format_stats()
assert "Pipeline" in result
assert "noise" in result
class TestSetEffectChainRef:
"""Tests for set_effect_chain_ref function."""

123
tests/test_sixel.py Normal file
View File

@@ -0,0 +1,123 @@
"""
Tests for engine.display.backends.sixel module.
"""
from unittest.mock import MagicMock, patch
class TestSixelDisplay:
"""Tests for SixelDisplay class."""
def test_init_stores_dimensions(self):
"""init stores dimensions."""
from engine.display.backends.sixel import SixelDisplay
display = SixelDisplay()
display.init(80, 24)
assert display.width == 80
assert display.height == 24
def test_init_custom_cell_size(self):
"""init accepts custom cell size."""
from engine.display.backends.sixel import SixelDisplay
display = SixelDisplay(cell_width=12, cell_height=18)
assert display.cell_width == 12
assert display.cell_height == 18
def test_show_handles_empty_buffer(self):
"""show handles empty buffer gracefully."""
from engine.display.backends.sixel import SixelDisplay
display = SixelDisplay()
display.init(80, 24)
with patch("engine.display.backends.sixel._encode_sixel") as mock_encode:
mock_encode.return_value = ""
display.show([])
def test_show_handles_pil_import_error(self):
"""show gracefully handles missing PIL."""
from engine.display.backends.sixel import SixelDisplay
display = SixelDisplay()
display.init(80, 24)
with patch.dict("sys.modules", {"PIL": None}):
display.show(["test line"])
def test_clear_sends_escape_sequence(self):
"""clear sends clear screen escape sequence."""
from engine.display.backends.sixel import SixelDisplay
display = SixelDisplay()
with patch("sys.stdout") as mock_stdout:
display.clear()
mock_stdout.buffer.write.assert_called()
def test_cleanup_does_nothing(self):
"""cleanup does nothing."""
from engine.display.backends.sixel import SixelDisplay
display = SixelDisplay()
display.cleanup()
class TestSixelAnsiParsing:
"""Tests for ANSI parsing in SixelDisplay."""
def test_parse_empty_string(self):
"""handles empty string."""
from engine.display.backends.sixel import _parse_ansi
result = _parse_ansi("")
assert len(result) > 0
def test_parse_plain_text(self):
"""parses plain text without ANSI codes."""
from engine.display.backends.sixel import _parse_ansi
result = _parse_ansi("hello world")
assert len(result) == 1
text, fg, bg, bold = result[0]
assert text == "hello world"
def test_parse_with_color_codes(self):
"""parses ANSI color codes."""
from engine.display.backends.sixel import _parse_ansi
result = _parse_ansi("\033[31mred\033[0m")
assert len(result) == 2
def test_parse_with_bold(self):
"""parses bold codes."""
from engine.display.backends.sixel import _parse_ansi
result = _parse_ansi("\033[1mbold\033[0m")
assert len(result) == 2
def test_parse_256_color(self):
"""parses 256 color codes."""
from engine.display.backends.sixel import _parse_ansi
result = _parse_ansi("\033[38;5;196mred\033[0m")
assert len(result) == 2
class TestSixelEncoding:
"""Tests for Sixel encoding."""
def test_encode_empty_image(self):
"""handles empty image."""
from engine.display.backends.sixel import _encode_sixel
with patch("PIL.Image.Image") as mock_image:
mock_img_instance = MagicMock()
mock_img_instance.convert.return_value = mock_img_instance
mock_img_instance.size = (0, 0)
mock_img_instance.load.return_value = {}
mock_image.return_value = mock_img_instance
result = _encode_sixel(mock_img_instance)
assert result == ""