Split monolithic scroll.py into focused modules: - viewport.py: terminal size (tw/th), ANSI positioning helpers - frame.py: FrameTimer class, scroll step calculation - layers.py: message overlay, ticker zone, firehose rendering - scroll.py: simplified orchestrator, imports from new modules Add stream controller and event types for future event-driven architecture: - controller.py: StreamController for source initialization and stream lifecycle - events.py: EventType enum and event dataclasses (HeadlineEvent, FrameTickEvent, etc.) Added tests for new modules: - test_viewport.py: 8 tests for viewport utilities - test_frame.py: 10 tests for frame timing - test_layers.py: 13 tests for layer compositing - test_events.py: 11 tests for event types - test_controller.py: 6 tests for stream controller This enables: - Testable chunks with clear responsibilities - Reusable viewport utilities across modules - Better separation of concerns in render pipeline - Foundation for future event-driven architecture Also includes Phase 1 documentation updates in code comments.
86 lines
2.8 KiB
Python
86 lines
2.8 KiB
Python
"""
|
|
Tests for engine.controller module.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from engine import config
|
|
from engine.controller import StreamController
|
|
|
|
|
|
class TestStreamController:
|
|
"""Tests for StreamController class."""
|
|
|
|
def test_init_default_config(self):
|
|
"""StreamController initializes with default config."""
|
|
controller = StreamController()
|
|
assert controller.config is not None
|
|
assert isinstance(controller.config, config.Config)
|
|
|
|
def test_init_custom_config(self):
|
|
"""StreamController accepts custom config."""
|
|
custom_config = config.Config(headline_limit=500)
|
|
controller = StreamController(config=custom_config)
|
|
assert controller.config.headline_limit == 500
|
|
|
|
def test_init_sources_none_by_default(self):
|
|
"""Sources are None until initialized."""
|
|
controller = StreamController()
|
|
assert controller.mic is None
|
|
assert controller.ntfy is None
|
|
|
|
@patch("engine.controller.MicMonitor")
|
|
@patch("engine.controller.NtfyPoller")
|
|
def test_initialize_sources(self, mock_ntfy, mock_mic):
|
|
"""initialize_sources creates mic and ntfy instances."""
|
|
mock_mic_instance = MagicMock()
|
|
mock_mic_instance.available = True
|
|
mock_mic_instance.start.return_value = True
|
|
mock_mic.return_value = mock_mic_instance
|
|
|
|
mock_ntfy_instance = MagicMock()
|
|
mock_ntfy_instance.start.return_value = True
|
|
mock_ntfy.return_value = mock_ntfy_instance
|
|
|
|
controller = StreamController()
|
|
mic_ok, ntfy_ok = controller.initialize_sources()
|
|
|
|
assert mic_ok is True
|
|
assert ntfy_ok is True
|
|
assert controller.mic is not None
|
|
assert controller.ntfy is not None
|
|
|
|
@patch("engine.controller.MicMonitor")
|
|
@patch("engine.controller.NtfyPoller")
|
|
def test_initialize_sources_mic_unavailable(self, mock_ntfy, mock_mic):
|
|
"""initialize_sources handles unavailable mic."""
|
|
mock_mic_instance = MagicMock()
|
|
mock_mic_instance.available = False
|
|
mock_mic.return_value = mock_mic_instance
|
|
|
|
mock_ntfy_instance = MagicMock()
|
|
mock_ntfy_instance.start.return_value = True
|
|
mock_ntfy.return_value = mock_ntfy_instance
|
|
|
|
controller = StreamController()
|
|
mic_ok, ntfy_ok = controller.initialize_sources()
|
|
|
|
assert mic_ok is False
|
|
assert ntfy_ok is True
|
|
|
|
|
|
class TestStreamControllerCleanup:
|
|
"""Tests for StreamController cleanup."""
|
|
|
|
@patch("engine.controller.MicMonitor")
|
|
def test_cleanup_stops_mic(self, mock_mic):
|
|
"""cleanup stops the microphone if running."""
|
|
mock_mic_instance = MagicMock()
|
|
mock_mic.return_value = mock_mic_instance
|
|
|
|
controller = StreamController()
|
|
controller.mic = mock_mic_instance
|
|
controller.cleanup()
|
|
|
|
mock_mic_instance.stop.assert_called_once()
|