forked from genewildish/Mainline
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.
196 lines
6.7 KiB
Python
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
|