Files
Mainline/tests/test_tint_acceptance.py
David Gwilliam 238bac1bb2 feat: Complete pipeline hot-rebuild implementation with acceptance tests
- Implements pipeline hot-rebuild with state preservation (issue #43)
- Adds auto-injection of MVP stages for missing capabilities
- Adds radial camera mode for polar coordinate scanning
- Adds afterimage and motionblur effects using framebuffer history
- Adds comprehensive acceptance tests for camera modes and pipeline rebuild
- Updates presets.toml with new effect configurations

Related to: #35 (Pipeline Mutation API epic)
Closes: #43, #44, #45
2026-03-19 03:34:06 -07:00

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() != ""