forked from genewildish/Mainline
test(verification): Add visual verification tests for message overlay
This commit is contained in:
234
tests/test_visual_verification.py
Normal file
234
tests/test_visual_verification.py
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
"""
|
||||||
|
Visual verification tests for message overlay and effect rendering.
|
||||||
|
|
||||||
|
These tests verify that the sideline pipeline produces visual output
|
||||||
|
that matches the expected behavior of upstream/main, even if the
|
||||||
|
buffer format differs due to architectural differences.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from engine.display import DisplayRegistry
|
||||||
|
from engine.pipeline import Pipeline, PipelineConfig, PipelineContext
|
||||||
|
from engine.pipeline.adapters import create_stage_from_display
|
||||||
|
from engine.pipeline.params import PipelineParams
|
||||||
|
from engine.pipeline.presets import get_preset
|
||||||
|
|
||||||
|
|
||||||
|
class TestMessageOverlayVisuals:
|
||||||
|
"""Test message overlay visual rendering."""
|
||||||
|
|
||||||
|
def test_message_overlay_produces_output(self):
|
||||||
|
"""Verify message overlay stage produces output when ntfy message is present."""
|
||||||
|
# This test verifies the message overlay stage is working
|
||||||
|
# It doesn't compare with upstream, just verifies functionality
|
||||||
|
|
||||||
|
from engine.pipeline.adapters.message_overlay import MessageOverlayStage
|
||||||
|
from engine.pipeline.adapters import MessageOverlayConfig
|
||||||
|
|
||||||
|
# Test the rendering function directly
|
||||||
|
stage = MessageOverlayStage(
|
||||||
|
config=MessageOverlayConfig(enabled=True, display_secs=30)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Test with a mock message
|
||||||
|
msg = ("Test Title", "Test Message Body", 0.0)
|
||||||
|
w, h = 80, 24
|
||||||
|
|
||||||
|
# Render overlay
|
||||||
|
overlay, _ = stage._render_message_overlay(msg, w, h, (None, None))
|
||||||
|
|
||||||
|
# Verify overlay has content
|
||||||
|
assert len(overlay) > 0, "Overlay should have content when message is present"
|
||||||
|
|
||||||
|
# Verify overlay contains expected content
|
||||||
|
overlay_text = "".join(overlay)
|
||||||
|
# Note: Message body is rendered as block characters, not text
|
||||||
|
# The title appears in the metadata line
|
||||||
|
assert "Test Title" in overlay_text, "Overlay should contain message title"
|
||||||
|
assert "ntfy" in overlay_text, "Overlay should contain ntfy metadata"
|
||||||
|
assert "\033[" in overlay_text, "Overlay should contain ANSI codes"
|
||||||
|
|
||||||
|
def test_message_overlay_appears_in_correct_position(self):
|
||||||
|
"""Verify message overlay appears in centered position."""
|
||||||
|
# This test verifies the message overlay positioning logic
|
||||||
|
# It checks that the overlay coordinates are calculated correctly
|
||||||
|
|
||||||
|
from engine.pipeline.adapters.message_overlay import MessageOverlayStage
|
||||||
|
from engine.pipeline.adapters import MessageOverlayConfig
|
||||||
|
|
||||||
|
stage = MessageOverlayStage(config=MessageOverlayConfig())
|
||||||
|
|
||||||
|
# Test positioning calculation
|
||||||
|
msg = ("Test Title", "Test Body", 0.0)
|
||||||
|
w, h = 80, 24
|
||||||
|
|
||||||
|
# Render overlay
|
||||||
|
overlay, _ = stage._render_message_overlay(msg, w, h, (None, None))
|
||||||
|
|
||||||
|
# Verify overlay has content
|
||||||
|
assert len(overlay) > 0, "Overlay should have content"
|
||||||
|
|
||||||
|
# Verify overlay contains cursor positioning codes
|
||||||
|
overlay_text = "".join(overlay)
|
||||||
|
assert "\033[" in overlay_text, "Overlay should contain ANSI codes"
|
||||||
|
assert "H" in overlay_text, "Overlay should contain cursor positioning"
|
||||||
|
|
||||||
|
# Verify panel is centered (check first line's position)
|
||||||
|
# Panel height is len(msg_rows) + 2 (content + meta + border)
|
||||||
|
# panel_top = max(0, (h - panel_h) // 2)
|
||||||
|
# First content line should be at panel_top + 1
|
||||||
|
first_line = overlay[0]
|
||||||
|
assert "\033[" in first_line, "First line should have cursor positioning"
|
||||||
|
assert ";1H" in first_line, "First line should position at column 1"
|
||||||
|
|
||||||
|
def test_theme_system_integration(self):
|
||||||
|
"""Verify theme system is integrated with message overlay."""
|
||||||
|
from engine import config as engine_config
|
||||||
|
from engine.themes import THEME_REGISTRY
|
||||||
|
|
||||||
|
# Verify theme registry has expected themes
|
||||||
|
assert "green" in THEME_REGISTRY, "Green theme should exist"
|
||||||
|
assert "orange" in THEME_REGISTRY, "Orange theme should exist"
|
||||||
|
assert "purple" in THEME_REGISTRY, "Purple theme should exist"
|
||||||
|
|
||||||
|
# Verify active theme is set
|
||||||
|
assert engine_config.ACTIVE_THEME is not None, "Active theme should be set"
|
||||||
|
assert engine_config.ACTIVE_THEME.name in THEME_REGISTRY, (
|
||||||
|
"Active theme should be in registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Verify theme has gradient colors
|
||||||
|
assert len(engine_config.ACTIVE_THEME.main_gradient) == 12, (
|
||||||
|
"Main gradient should have 12 colors"
|
||||||
|
)
|
||||||
|
assert len(engine_config.ACTIVE_THEME.message_gradient) == 12, (
|
||||||
|
"Message gradient should have 12 colors"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPipelineExecutionOrder:
|
||||||
|
"""Test pipeline execution order for visual consistency."""
|
||||||
|
|
||||||
|
def test_message_overlay_after_camera(self):
|
||||||
|
"""Verify message overlay is applied after camera transformation."""
|
||||||
|
from engine.pipeline import Pipeline, PipelineConfig, PipelineContext
|
||||||
|
from engine.pipeline.adapters import (
|
||||||
|
create_stage_from_display,
|
||||||
|
MessageOverlayStage,
|
||||||
|
MessageOverlayConfig,
|
||||||
|
)
|
||||||
|
from engine.display import DisplayRegistry
|
||||||
|
|
||||||
|
# Create pipeline
|
||||||
|
config = PipelineConfig(
|
||||||
|
source="empty",
|
||||||
|
display="null",
|
||||||
|
camera="feed",
|
||||||
|
effects=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx = PipelineContext()
|
||||||
|
pipeline = Pipeline(config=config, context=ctx)
|
||||||
|
|
||||||
|
# Add stages
|
||||||
|
from engine.data_sources.sources import EmptyDataSource
|
||||||
|
from engine.pipeline.adapters import DataSourceStage
|
||||||
|
|
||||||
|
pipeline.add_stage(
|
||||||
|
"source",
|
||||||
|
DataSourceStage(EmptyDataSource(width=80, height=24), name="empty"),
|
||||||
|
)
|
||||||
|
pipeline.add_stage(
|
||||||
|
"message_overlay", MessageOverlayStage(config=MessageOverlayConfig())
|
||||||
|
)
|
||||||
|
pipeline.add_stage(
|
||||||
|
"display", create_stage_from_display(DisplayRegistry.create("null"), "null")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Build and check order
|
||||||
|
pipeline.build()
|
||||||
|
execution_order = pipeline.execution_order
|
||||||
|
|
||||||
|
# Verify message_overlay comes after camera stages
|
||||||
|
camera_idx = next(
|
||||||
|
(i for i, name in enumerate(execution_order) if "camera" in name), -1
|
||||||
|
)
|
||||||
|
msg_idx = next(
|
||||||
|
(i for i, name in enumerate(execution_order) if "message_overlay" in name),
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
if camera_idx >= 0 and msg_idx >= 0:
|
||||||
|
assert msg_idx > camera_idx, "Message overlay should be after camera stage"
|
||||||
|
|
||||||
|
|
||||||
|
class TestCapturedOutputAnalysis:
|
||||||
|
"""Test analysis of captured output files."""
|
||||||
|
|
||||||
|
def test_captured_files_exist(self):
|
||||||
|
"""Verify captured output files exist."""
|
||||||
|
sideline_path = Path("output/sideline_demo.json")
|
||||||
|
upstream_path = Path("output/upstream_demo.json")
|
||||||
|
|
||||||
|
assert sideline_path.exists(), "Sideline capture file should exist"
|
||||||
|
assert upstream_path.exists(), "Upstream capture file should exist"
|
||||||
|
|
||||||
|
def test_captured_files_valid(self):
|
||||||
|
"""Verify captured output files are valid JSON."""
|
||||||
|
sideline_path = Path("output/sideline_demo.json")
|
||||||
|
upstream_path = Path("output/upstream_demo.json")
|
||||||
|
|
||||||
|
with open(sideline_path) as f:
|
||||||
|
sideline = json.load(f)
|
||||||
|
with open(upstream_path) as f:
|
||||||
|
upstream = json.load(f)
|
||||||
|
|
||||||
|
# Verify structure
|
||||||
|
assert "frames" in sideline, "Sideline should have frames"
|
||||||
|
assert "frames" in upstream, "Upstream should have frames"
|
||||||
|
assert len(sideline["frames"]) > 0, "Sideline should have at least one frame"
|
||||||
|
assert len(upstream["frames"]) > 0, "Upstream should have at least one frame"
|
||||||
|
|
||||||
|
def test_sideline_buffer_format(self):
|
||||||
|
"""Verify sideline buffer format is plain text."""
|
||||||
|
sideline_path = Path("output/sideline_demo.json")
|
||||||
|
|
||||||
|
with open(sideline_path) as f:
|
||||||
|
sideline = json.load(f)
|
||||||
|
|
||||||
|
# Check first frame
|
||||||
|
frame0 = sideline["frames"][0]["buffer"]
|
||||||
|
|
||||||
|
# Sideline should have plain text lines (no cursor positioning)
|
||||||
|
# Check first few lines
|
||||||
|
for i, line in enumerate(frame0[:5]):
|
||||||
|
# Should not start with cursor positioning
|
||||||
|
if line.strip():
|
||||||
|
assert not line.startswith("\033["), (
|
||||||
|
f"Line {i} should not start with cursor positioning"
|
||||||
|
)
|
||||||
|
# Should have actual content
|
||||||
|
assert len(line.strip()) > 0, f"Line {i} should have content"
|
||||||
|
|
||||||
|
def test_upstream_buffer_format(self):
|
||||||
|
"""Verify upstream buffer format includes cursor positioning."""
|
||||||
|
upstream_path = Path("output/upstream_demo.json")
|
||||||
|
|
||||||
|
with open(upstream_path) as f:
|
||||||
|
upstream = json.load(f)
|
||||||
|
|
||||||
|
# Check first frame
|
||||||
|
frame0 = upstream["frames"][0]["buffer"]
|
||||||
|
|
||||||
|
# Upstream should have cursor positioning codes
|
||||||
|
overlay_text = "".join(frame0[:10])
|
||||||
|
assert "\033[" in overlay_text, "Upstream buffer should contain ANSI codes"
|
||||||
|
assert "H" in overlay_text, "Upstream buffer should contain cursor positioning"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
pytest.main([__file__, "-v"])
|
||||||
Reference in New Issue
Block a user