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.
207 lines
6.6 KiB
Python
207 lines
6.6 KiB
Python
"""Integration test: TintEffect in the pipeline."""
|
|
|
|
import queue
|
|
|
|
from engine.data_sources.sources import ListDataSource, SourceItem
|
|
from engine.effects.plugins.tint import TintEffect
|
|
from engine.effects.types import EffectConfig
|
|
from engine.pipeline import Pipeline, PipelineConfig
|
|
from engine.pipeline.adapters import (
|
|
DataSourceStage,
|
|
DisplayStage,
|
|
EffectPluginStage,
|
|
SourceItemsToBufferStage,
|
|
)
|
|
from engine.pipeline.core import PipelineContext
|
|
from engine.pipeline.params import PipelineParams
|
|
|
|
|
|
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],
|
|
tint_config: EffectConfig | None = None,
|
|
width: int = 80,
|
|
height: int = 24,
|
|
) -> tuple[Pipeline, QueueDisplay, PipelineContext]:
|
|
"""Build pipeline: source -> render -> tint effect -> display."""
|
|
display = QueueDisplay()
|
|
|
|
ctx = PipelineContext()
|
|
params = PipelineParams()
|
|
params.viewport_width = width
|
|
params.viewport_height = height
|
|
params.frame_number = 0
|
|
ctx.params = params
|
|
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 (simple)
|
|
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
|
|
|
|
# Tint effect
|
|
tint_effect = TintEffect()
|
|
if tint_config is not None:
|
|
tint_effect.configure(tint_config)
|
|
pipeline.add_stage("tint", EffectPluginStage(tint_effect, name="tint"))
|
|
|
|
# Display
|
|
pipeline.add_stage("display", DisplayStage(display, name="queue"))
|
|
|
|
pipeline.build()
|
|
pipeline.initialize()
|
|
|
|
return pipeline, display, ctx
|
|
|
|
|
|
class TestTintAcceptance:
|
|
"""Test TintEffect in a full pipeline."""
|
|
|
|
def test_tint_applies_default_color(self):
|
|
"""Default tint should apply ANSI color codes to output."""
|
|
items = [SourceItem(content="Hello World", source="test", timestamp="0")]
|
|
pipeline, display, ctx = _build_pipeline(items)
|
|
|
|
result = pipeline.execute(items)
|
|
|
|
assert result.success, f"Pipeline failed: {result.error}"
|
|
frame = display.frames.get(timeout=1)
|
|
|
|
text = "\n".join(frame)
|
|
assert "\033[" in text, f"Expected ANSI codes in frame: {frame}"
|
|
assert "Hello World" in text
|
|
|
|
def test_tint_applies_red_color(self):
|
|
"""Configured red tint should produce red ANSI code (196-197)."""
|
|
items = [SourceItem(content="Red Text", source="test", timestamp="0")]
|
|
config = EffectConfig(
|
|
enabled=True,
|
|
intensity=1.0,
|
|
params={"r": 255, "g": 0, "b": 0, "a": 0.8},
|
|
)
|
|
pipeline, display, ctx = _build_pipeline(items, tint_config=config)
|
|
|
|
result = pipeline.execute(items)
|
|
|
|
assert result.success
|
|
frame = display.frames.get(timeout=1)
|
|
line = frame[0]
|
|
|
|
# Should contain red ANSI code (196 or 197 in 256 color)
|
|
assert "\033[38;5;196m" in line or "\033[38;5;197m" in line, (
|
|
f"Missing red tint: {line}"
|
|
)
|
|
assert "Red Text" in line
|
|
|
|
def test_tint_disabled_does_nothing(self):
|
|
"""Disabled tint stage should pass through buffer unchanged."""
|
|
items = [SourceItem(content="Plain Text", source="test", timestamp="0")]
|
|
pipeline, display, ctx = _build_pipeline(items)
|
|
|
|
# Disable the tint stage
|
|
stage = pipeline.get_stage("tint")
|
|
stage.set_enabled(False)
|
|
|
|
result = pipeline.execute(items)
|
|
|
|
assert result.success
|
|
frame = display.frames.get(timeout=1)
|
|
text = "\n".join(frame)
|
|
|
|
# Should contain Plain Text with NO ANSI color codes
|
|
assert "Plain Text" in text
|
|
assert "\033[" not in text, f"Unexpected ANSI codes in frame: {frame}"
|
|
|
|
def test_tint_zero_transparency(self):
|
|
"""Alpha=0 should pass through buffer unchanged (no tint)."""
|
|
items = [SourceItem(content="Transparent", source="test", timestamp="0")]
|
|
config = EffectConfig(
|
|
enabled=True,
|
|
intensity=1.0,
|
|
params={"r": 255, "g": 128, "b": 64, "a": 0.0},
|
|
)
|
|
pipeline, display, ctx = _build_pipeline(items, tint_config=config)
|
|
|
|
result = pipeline.execute(items)
|
|
|
|
assert result.success
|
|
frame = display.frames.get(timeout=1)
|
|
text = "\n".join(frame)
|
|
|
|
assert "Transparent" in text
|
|
assert "\033[" not in text, f"Expected no ANSI codes with alpha=0: {frame}"
|
|
|
|
def test_tint_with_multiples_lines(self):
|
|
"""Tint should apply to all non-empty lines."""
|
|
items = [
|
|
SourceItem(content="Line1\nLine2\n\nLine4", source="test", timestamp="0")
|
|
]
|
|
config = EffectConfig(
|
|
enabled=True,
|
|
intensity=1.0,
|
|
params={"r": 0, "g": 255, "b": 0, "a": 0.7},
|
|
)
|
|
pipeline, display, ctx = _build_pipeline(items, tint_config=config)
|
|
|
|
result = pipeline.execute(items)
|
|
|
|
assert result.success
|
|
frame = display.frames.get(timeout=1)
|
|
|
|
# All non-empty lines should have green ANSI codes
|
|
green_codes = ["\033[38;5;", "m"]
|
|
for line in frame:
|
|
if line.strip():
|
|
assert green_codes[0] in line and green_codes[1] in line, (
|
|
f"Missing green tint: {line}"
|
|
)
|
|
else:
|
|
assert line == "", f"Empty lines should be exactly empty: {line}"
|
|
|
|
def test_tint_preserves_empty_lines(self):
|
|
"""Empty lines should remain empty (no ANSI codes)."""
|
|
items = [SourceItem(content="A\n\nB", source="test", timestamp="0")]
|
|
pipeline, display, ctx = _build_pipeline(items)
|
|
|
|
result = pipeline.execute(items)
|
|
|
|
assert result.success
|
|
frame = display.frames.get(timeout=1)
|
|
|
|
assert frame[0].strip() != ""
|
|
assert frame[1] == "" # Empty line unchanged
|
|
assert frame[2].strip() != ""
|