Files
sideline/tests/test_framebuffer_acceptance.py
David Gwilliam ef98add0c5 feat(integration): Complete feature rewrite with pipeline architecture, effects system, and display improvements
Major changes:
- Pipeline architecture with capability-based dependency resolution
- Effects plugin system with performance monitoring
- Display abstraction with multiple backends (terminal, null, websocket)
- Camera system for viewport scrolling
- Sensor framework for real-time input
- Command-and-control system via ntfy
- WebSocket display backend for browser clients
- Comprehensive test suite and documentation

Issue #48: ADR for preset scripting language included

This commit consolidates 110 individual commits into a single
feature integration that can be reviewed and tested before
further refinement.
2026-03-20 04:41:44 -07:00

196 lines
6.7 KiB
Python

"""Integration test: FrameBufferStage in the pipeline."""
import queue
from engine.data_sources.sources import ListDataSource, SourceItem
from engine.effects.types import EffectConfig
from engine.pipeline import Pipeline, PipelineConfig
from engine.pipeline.adapters import (
DataSourceStage,
DisplayStage,
SourceItemsToBufferStage,
)
from engine.pipeline.core import PipelineContext
from engine.pipeline.stages.framebuffer import FrameBufferStage
class QueueDisplay:
"""Stub display that captures every frame into a queue."""
def __init__(self):
self.frames: queue.Queue[list[str]] = queue.Queue()
self.width = 80
self.height = 24
self._init_called = False
def init(self, width: int, height: int, reuse: bool = False) -> None:
self.width = width
self.height = height
self._init_called = True
def show(self, buffer: list[str], border: bool = False) -> None:
self.frames.put(list(buffer))
def clear(self) -> None:
pass
def cleanup(self) -> None:
pass
def get_dimensions(self) -> tuple[int, int]:
return (self.width, self.height)
def _build_pipeline(
items: list[SourceItem],
history_depth: int = 5,
width: int = 80,
height: int = 24,
) -> tuple[Pipeline, QueueDisplay, PipelineContext]:
"""Build pipeline: source -> render -> framebuffer -> display."""
display = QueueDisplay()
ctx = PipelineContext()
ctx.set("items", items)
pipeline = Pipeline(
config=PipelineConfig(enable_metrics=True),
context=ctx,
)
# Source
source = ListDataSource(items, name="test-source")
pipeline.add_stage("source", DataSourceStage(source, name="test-source"))
# Render
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
# Framebuffer
framebuffer = FrameBufferStage(name="default", history_depth=history_depth)
pipeline.add_stage("framebuffer", framebuffer)
# Display
pipeline.add_stage("display", DisplayStage(display, name="queue"))
pipeline.build()
pipeline.initialize()
return pipeline, display, ctx
class TestFrameBufferAcceptance:
"""Test FrameBufferStage in a full pipeline."""
def test_framebuffer_populates_history(self):
"""After several frames, framebuffer should have history stored."""
items = [
SourceItem(content="Frame\nBuffer\nTest", source="test", timestamp="0")
]
pipeline, display, ctx = _build_pipeline(items, history_depth=5)
# Run 3 frames
for i in range(3):
result = pipeline.execute([])
assert result.success, f"Pipeline failed at frame {i}: {result.error}"
# Check framebuffer history in context
history = ctx.get("framebuffer.default.history")
assert history is not None, "Framebuffer history not found in context"
assert len(history) == 3, f"Expected 3 history frames, got {len(history)}"
def test_framebuffer_respects_depth(self):
"""Framebuffer should not exceed configured history depth."""
items = [SourceItem(content="Depth\nTest", source="test", timestamp="0")]
pipeline, display, ctx = _build_pipeline(items, history_depth=3)
# Run 5 frames
for i in range(5):
result = pipeline.execute([])
assert result.success
history = ctx.get("framebuffer.default.history")
assert history is not None
assert len(history) == 3, f"Expected depth 3, got {len(history)}"
def test_framebuffer_current_intensity(self):
"""Framebuffer should compute current intensity map."""
items = [SourceItem(content="Intensity\nMap", source="test", timestamp="0")]
pipeline, display, ctx = _build_pipeline(items, history_depth=5)
# Run at least one frame
result = pipeline.execute([])
assert result.success
intensity = ctx.get("framebuffer.default.current_intensity")
assert intensity is not None, "No intensity map in context"
# Intensity should be a list of one value per line? Actually it's a 2D array or list?
# Let's just check it's non-empty
assert len(intensity) > 0, "Intensity map is empty"
def test_framebuffer_get_frame(self):
"""Should be able to retrieve specific frames from history."""
items = [SourceItem(content="Retrieve\nFrame", source="test", timestamp="0")]
pipeline, display, ctx = _build_pipeline(items, history_depth=5)
# Run 2 frames
for i in range(2):
result = pipeline.execute([])
assert result.success
# Retrieve frame 0 (most recent)
recent = pipeline.get_stage("framebuffer").get_frame(0, ctx)
assert recent is not None, "Cannot retrieve recent frame"
assert len(recent) > 0, "Recent frame is empty"
# Retrieve frame 1 (previous)
previous = pipeline.get_stage("framebuffer").get_frame(1, ctx)
assert previous is not None, "Cannot retrieve previous frame"
def test_framebuffer_with_motionblur_effect(self):
"""MotionBlurEffect should work when depending on framebuffer."""
from engine.effects.plugins.motionblur import MotionBlurEffect
from engine.pipeline.adapters import EffectPluginStage
items = [SourceItem(content="Motion\nBlur", source="test", timestamp="0")]
display = QueueDisplay()
ctx = PipelineContext()
ctx.set("items", items)
pipeline = Pipeline(
config=PipelineConfig(enable_metrics=True),
context=ctx,
)
source = ListDataSource(items, name="test")
pipeline.add_stage("source", DataSourceStage(source, name="test"))
pipeline.add_stage("render", SourceItemsToBufferStage(name="render"))
framebuffer = FrameBufferStage(name="default", history_depth=3)
pipeline.add_stage("framebuffer", framebuffer)
motionblur = MotionBlurEffect()
motionblur.configure(EffectConfig(enabled=True, intensity=0.5))
pipeline.add_stage(
"motionblur",
EffectPluginStage(
motionblur,
name="motionblur",
dependencies={"framebuffer.history.default"},
),
)
pipeline.add_stage("display", DisplayStage(display, name="queue"))
pipeline.build()
pipeline.initialize()
# Run a few frames
for i in range(5):
result = pipeline.execute([])
assert result.success, f"Motion blur pipeline failed at frame {i}"
# Check that history exists
history = ctx.get("framebuffer.default.history")
assert history is not None
assert len(history) > 0