forked from genewildish/Mainline
feat(figment): complete pipeline integration with native effect plugin
- Add engine/effects/plugins/figment.py (native pipeline implementation) - Add engine/figment_render.py, engine/figment_trigger.py, engine/themes.py - Add 3 SVG assets in figments/ (Mexican/Aztec motif) - Add engine/display/backends/animation_report.py for debugging - Add engine/pipeline/adapters/frame_capture.py for frame capture - Add test-figment preset to presets.toml - Add cairosvg optional dependency to pyproject.toml - Update EffectPluginStage to support is_overlay attribute (for overlay effects) - Add comprehensive tests: test_figment_effect.py, test_figment_pipeline.py, test_figment_render.py - Remove obsolete test_ui_simple.py - Update TODO.md with test cleanup plan - Refactor test_adapters.py to use real components instead of mocks This completes the figment SVG overlay feature integration using the modern pipeline architecture, avoiding legacy effects_plugins. All tests pass (758 total).
This commit is contained in:
@@ -1,17 +1,16 @@
|
||||
"""
|
||||
Tests for engine/pipeline/adapters.py - Stage adapters for the pipeline.
|
||||
|
||||
Tests Stage adapters that bridge existing components to the Stage interface:
|
||||
- DataSourceStage: Wraps DataSource objects
|
||||
- DisplayStage: Wraps Display backends
|
||||
- PassthroughStage: Simple pass-through stage for pre-rendered data
|
||||
- SourceItemsToBufferStage: Converts SourceItem objects to text buffers
|
||||
- EffectPluginStage: Wraps effect plugins
|
||||
Tests Stage adapters that bridge existing components to the Stage interface.
|
||||
Focuses on behavior testing rather than mock interactions.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from engine.data_sources.sources import SourceItem
|
||||
from engine.display.backends.null import NullDisplay
|
||||
from engine.effects.plugins import discover_plugins
|
||||
from engine.effects.registry import get_registry
|
||||
from engine.pipeline.adapters import (
|
||||
DataSourceStage,
|
||||
DisplayStage,
|
||||
@@ -25,28 +24,14 @@ from engine.pipeline.core import PipelineContext
|
||||
class TestDataSourceStage:
|
||||
"""Test DataSourceStage adapter."""
|
||||
|
||||
def test_datasource_stage_name(self):
|
||||
"""DataSourceStage stores name correctly."""
|
||||
def test_datasource_stage_properties(self):
|
||||
"""DataSourceStage has correct name, category, and capabilities."""
|
||||
mock_source = MagicMock()
|
||||
stage = DataSourceStage(mock_source, name="headlines")
|
||||
|
||||
assert stage.name == "headlines"
|
||||
|
||||
def test_datasource_stage_category(self):
|
||||
"""DataSourceStage has 'source' category."""
|
||||
mock_source = MagicMock()
|
||||
stage = DataSourceStage(mock_source, name="headlines")
|
||||
assert stage.category == "source"
|
||||
|
||||
def test_datasource_stage_capabilities(self):
|
||||
"""DataSourceStage advertises source capability."""
|
||||
mock_source = MagicMock()
|
||||
stage = DataSourceStage(mock_source, name="headlines")
|
||||
assert "source.headlines" in stage.capabilities
|
||||
|
||||
def test_datasource_stage_dependencies(self):
|
||||
"""DataSourceStage has no dependencies."""
|
||||
mock_source = MagicMock()
|
||||
stage = DataSourceStage(mock_source, name="headlines")
|
||||
assert stage.dependencies == set()
|
||||
|
||||
def test_datasource_stage_process_calls_get_items(self):
|
||||
@@ -64,7 +49,7 @@ class TestDataSourceStage:
|
||||
assert result == mock_items
|
||||
mock_source.get_items.assert_called_once()
|
||||
|
||||
def test_datasource_stage_process_fallback_returns_data(self):
|
||||
def test_datasource_stage_process_fallback(self):
|
||||
"""DataSourceStage.process() returns data if no get_items method."""
|
||||
mock_source = MagicMock(spec=[]) # No get_items method
|
||||
stage = DataSourceStage(mock_source, name="headlines")
|
||||
@@ -76,124 +61,68 @@ class TestDataSourceStage:
|
||||
|
||||
|
||||
class TestDisplayStage:
|
||||
"""Test DisplayStage adapter."""
|
||||
"""Test DisplayStage adapter using NullDisplay for real behavior."""
|
||||
|
||||
def test_display_stage_properties(self):
|
||||
"""DisplayStage has correct name, category, and capabilities."""
|
||||
display = NullDisplay()
|
||||
stage = DisplayStage(display, name="terminal")
|
||||
|
||||
def test_display_stage_name(self):
|
||||
"""DisplayStage stores name correctly."""
|
||||
mock_display = MagicMock()
|
||||
stage = DisplayStage(mock_display, name="terminal")
|
||||
assert stage.name == "terminal"
|
||||
|
||||
def test_display_stage_category(self):
|
||||
"""DisplayStage has 'display' category."""
|
||||
mock_display = MagicMock()
|
||||
stage = DisplayStage(mock_display, name="terminal")
|
||||
assert stage.category == "display"
|
||||
|
||||
def test_display_stage_capabilities(self):
|
||||
"""DisplayStage advertises display capability."""
|
||||
mock_display = MagicMock()
|
||||
stage = DisplayStage(mock_display, name="terminal")
|
||||
assert "display.output" in stage.capabilities
|
||||
|
||||
def test_display_stage_dependencies(self):
|
||||
"""DisplayStage depends on render.output."""
|
||||
mock_display = MagicMock()
|
||||
stage = DisplayStage(mock_display, name="terminal")
|
||||
assert "render.output" in stage.dependencies
|
||||
|
||||
def test_display_stage_init(self):
|
||||
"""DisplayStage.init() calls display.init() with dimensions."""
|
||||
mock_display = MagicMock()
|
||||
mock_display.init.return_value = True
|
||||
stage = DisplayStage(mock_display, name="terminal")
|
||||
def test_display_stage_init_and_process(self):
|
||||
"""DisplayStage initializes display and processes buffer."""
|
||||
from engine.pipeline.params import PipelineParams
|
||||
|
||||
display = NullDisplay()
|
||||
stage = DisplayStage(display, name="terminal")
|
||||
|
||||
ctx = PipelineContext()
|
||||
ctx.params = MagicMock()
|
||||
ctx.params.viewport_width = 100
|
||||
ctx.params.viewport_height = 30
|
||||
ctx.params = PipelineParams()
|
||||
ctx.params.viewport_width = 80
|
||||
ctx.params.viewport_height = 24
|
||||
|
||||
# Initialize
|
||||
result = stage.init(ctx)
|
||||
|
||||
assert result is True
|
||||
mock_display.init.assert_called_once_with(100, 30, reuse=False)
|
||||
|
||||
def test_display_stage_init_uses_defaults(self):
|
||||
"""DisplayStage.init() uses defaults when params missing."""
|
||||
mock_display = MagicMock()
|
||||
mock_display.init.return_value = True
|
||||
stage = DisplayStage(mock_display, name="terminal")
|
||||
# Process buffer
|
||||
buffer = ["Line 1", "Line 2", "Line 3"]
|
||||
output = stage.process(buffer, ctx)
|
||||
assert output == buffer
|
||||
|
||||
ctx = PipelineContext()
|
||||
ctx.params = None
|
||||
# Verify display captured the buffer
|
||||
assert display._last_buffer == buffer
|
||||
|
||||
result = stage.init(ctx)
|
||||
|
||||
assert result is True
|
||||
mock_display.init.assert_called_once_with(80, 24, reuse=False)
|
||||
|
||||
def test_display_stage_process_calls_show(self):
|
||||
"""DisplayStage.process() calls display.show() with data."""
|
||||
mock_display = MagicMock()
|
||||
stage = DisplayStage(mock_display, name="terminal")
|
||||
|
||||
test_buffer = [[["A", "red"] for _ in range(80)] for _ in range(24)]
|
||||
ctx = PipelineContext()
|
||||
result = stage.process(test_buffer, ctx)
|
||||
|
||||
assert result == test_buffer
|
||||
mock_display.show.assert_called_once_with(test_buffer)
|
||||
|
||||
def test_display_stage_process_skips_none_data(self):
|
||||
def test_display_stage_skips_none_data(self):
|
||||
"""DisplayStage.process() skips show() if data is None."""
|
||||
mock_display = MagicMock()
|
||||
stage = DisplayStage(mock_display, name="terminal")
|
||||
display = NullDisplay()
|
||||
stage = DisplayStage(display, name="terminal")
|
||||
|
||||
ctx = PipelineContext()
|
||||
result = stage.process(None, ctx)
|
||||
|
||||
assert result is None
|
||||
mock_display.show.assert_not_called()
|
||||
|
||||
def test_display_stage_cleanup(self):
|
||||
"""DisplayStage.cleanup() calls display.cleanup()."""
|
||||
mock_display = MagicMock()
|
||||
stage = DisplayStage(mock_display, name="terminal")
|
||||
|
||||
stage.cleanup()
|
||||
|
||||
mock_display.cleanup.assert_called_once()
|
||||
assert display._last_buffer is None
|
||||
|
||||
|
||||
class TestPassthroughStage:
|
||||
"""Test PassthroughStage adapter."""
|
||||
|
||||
def test_passthrough_stage_name(self):
|
||||
"""PassthroughStage stores name correctly."""
|
||||
def test_passthrough_stage_properties(self):
|
||||
"""PassthroughStage has correct properties."""
|
||||
stage = PassthroughStage(name="test")
|
||||
|
||||
assert stage.name == "test"
|
||||
|
||||
def test_passthrough_stage_category(self):
|
||||
"""PassthroughStage has 'render' category."""
|
||||
stage = PassthroughStage()
|
||||
assert stage.category == "render"
|
||||
|
||||
def test_passthrough_stage_is_optional(self):
|
||||
"""PassthroughStage is optional."""
|
||||
stage = PassthroughStage()
|
||||
assert stage.optional is True
|
||||
|
||||
def test_passthrough_stage_capabilities(self):
|
||||
"""PassthroughStage advertises render output capability."""
|
||||
stage = PassthroughStage()
|
||||
assert "render.output" in stage.capabilities
|
||||
|
||||
def test_passthrough_stage_dependencies(self):
|
||||
"""PassthroughStage depends on source."""
|
||||
stage = PassthroughStage()
|
||||
assert "source" in stage.dependencies
|
||||
|
||||
def test_passthrough_stage_process_returns_data_unchanged(self):
|
||||
def test_passthrough_stage_process_unchanged(self):
|
||||
"""PassthroughStage.process() returns data unchanged."""
|
||||
stage = PassthroughStage()
|
||||
ctx = PipelineContext()
|
||||
@@ -210,32 +139,17 @@ class TestPassthroughStage:
|
||||
class TestSourceItemsToBufferStage:
|
||||
"""Test SourceItemsToBufferStage adapter."""
|
||||
|
||||
def test_source_items_to_buffer_stage_name(self):
|
||||
"""SourceItemsToBufferStage stores name correctly."""
|
||||
def test_source_items_to_buffer_stage_properties(self):
|
||||
"""SourceItemsToBufferStage has correct properties."""
|
||||
stage = SourceItemsToBufferStage(name="custom-name")
|
||||
|
||||
assert stage.name == "custom-name"
|
||||
|
||||
def test_source_items_to_buffer_stage_category(self):
|
||||
"""SourceItemsToBufferStage has 'render' category."""
|
||||
stage = SourceItemsToBufferStage()
|
||||
assert stage.category == "render"
|
||||
|
||||
def test_source_items_to_buffer_stage_is_optional(self):
|
||||
"""SourceItemsToBufferStage is optional."""
|
||||
stage = SourceItemsToBufferStage()
|
||||
assert stage.optional is True
|
||||
|
||||
def test_source_items_to_buffer_stage_capabilities(self):
|
||||
"""SourceItemsToBufferStage advertises render output capability."""
|
||||
stage = SourceItemsToBufferStage()
|
||||
assert "render.output" in stage.capabilities
|
||||
|
||||
def test_source_items_to_buffer_stage_dependencies(self):
|
||||
"""SourceItemsToBufferStage depends on source."""
|
||||
stage = SourceItemsToBufferStage()
|
||||
assert "source" in stage.dependencies
|
||||
|
||||
def test_source_items_to_buffer_stage_process_single_line_item(self):
|
||||
def test_source_items_to_buffer_stage_process_single_line(self):
|
||||
"""SourceItemsToBufferStage converts single-line SourceItem."""
|
||||
stage = SourceItemsToBufferStage()
|
||||
ctx = PipelineContext()
|
||||
@@ -247,10 +161,10 @@ class TestSourceItemsToBufferStage:
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 1
|
||||
# Result should be lines of text
|
||||
assert all(isinstance(line, str) for line in result)
|
||||
assert "Single line content" in result[0]
|
||||
|
||||
def test_source_items_to_buffer_stage_process_multiline_item(self):
|
||||
def test_source_items_to_buffer_stage_process_multiline(self):
|
||||
"""SourceItemsToBufferStage splits multiline SourceItem content."""
|
||||
stage = SourceItemsToBufferStage()
|
||||
ctx = PipelineContext()
|
||||
@@ -283,63 +197,76 @@ class TestSourceItemsToBufferStage:
|
||||
|
||||
|
||||
class TestEffectPluginStage:
|
||||
"""Test EffectPluginStage adapter."""
|
||||
"""Test EffectPluginStage adapter with real effect plugins."""
|
||||
|
||||
def test_effect_plugin_stage_name(self):
|
||||
"""EffectPluginStage stores name correctly."""
|
||||
mock_effect = MagicMock()
|
||||
stage = EffectPluginStage(mock_effect, name="blur")
|
||||
assert stage.name == "blur"
|
||||
def test_effect_plugin_stage_properties(self):
|
||||
"""EffectPluginStage has correct properties for real effects."""
|
||||
discover_plugins()
|
||||
registry = get_registry()
|
||||
effect = registry.get("noise")
|
||||
|
||||
def test_effect_plugin_stage_category(self):
|
||||
"""EffectPluginStage has 'effect' category."""
|
||||
mock_effect = MagicMock()
|
||||
stage = EffectPluginStage(mock_effect, name="blur")
|
||||
stage = EffectPluginStage(effect, name="noise")
|
||||
|
||||
assert stage.name == "noise"
|
||||
assert stage.category == "effect"
|
||||
|
||||
def test_effect_plugin_stage_is_not_optional(self):
|
||||
"""EffectPluginStage is required when configured."""
|
||||
mock_effect = MagicMock()
|
||||
stage = EffectPluginStage(mock_effect, name="blur")
|
||||
assert stage.optional is False
|
||||
|
||||
def test_effect_plugin_stage_capabilities(self):
|
||||
"""EffectPluginStage advertises effect capability with name."""
|
||||
mock_effect = MagicMock()
|
||||
stage = EffectPluginStage(mock_effect, name="blur")
|
||||
assert "effect.blur" in stage.capabilities
|
||||
|
||||
def test_effect_plugin_stage_dependencies(self):
|
||||
"""EffectPluginStage has no static dependencies."""
|
||||
mock_effect = MagicMock()
|
||||
stage = EffectPluginStage(mock_effect, name="blur")
|
||||
# EffectPluginStage has empty dependencies - they are resolved dynamically
|
||||
assert stage.dependencies == set()
|
||||
|
||||
def test_effect_plugin_stage_stage_type(self):
|
||||
"""EffectPluginStage.stage_type returns effect for non-HUD."""
|
||||
mock_effect = MagicMock()
|
||||
stage = EffectPluginStage(mock_effect, name="blur")
|
||||
assert stage.stage_type == "effect"
|
||||
assert "effect.noise" in stage.capabilities
|
||||
|
||||
def test_effect_plugin_stage_hud_special_handling(self):
|
||||
"""EffectPluginStage has special handling for HUD effect."""
|
||||
mock_effect = MagicMock()
|
||||
stage = EffectPluginStage(mock_effect, name="hud")
|
||||
discover_plugins()
|
||||
registry = get_registry()
|
||||
hud_effect = registry.get("hud")
|
||||
|
||||
stage = EffectPluginStage(hud_effect, name="hud")
|
||||
|
||||
assert stage.stage_type == "overlay"
|
||||
assert stage.is_overlay is True
|
||||
assert stage.render_order == 100
|
||||
|
||||
def test_effect_plugin_stage_process(self):
|
||||
"""EffectPluginStage.process() calls effect.process()."""
|
||||
mock_effect = MagicMock()
|
||||
mock_effect.process.return_value = "processed_data"
|
||||
def test_effect_plugin_stage_process_real_effect(self):
|
||||
"""EffectPluginStage.process() calls real effect.process()."""
|
||||
from engine.pipeline.params import PipelineParams
|
||||
|
||||
stage = EffectPluginStage(mock_effect, name="blur")
|
||||
discover_plugins()
|
||||
registry = get_registry()
|
||||
effect = registry.get("noise")
|
||||
|
||||
stage = EffectPluginStage(effect, name="noise")
|
||||
ctx = PipelineContext()
|
||||
test_buffer = "test_buffer"
|
||||
ctx.params = PipelineParams()
|
||||
ctx.params.viewport_width = 80
|
||||
ctx.params.viewport_height = 24
|
||||
ctx.params.frame_number = 0
|
||||
|
||||
test_buffer = ["Line 1", "Line 2", "Line 3"]
|
||||
result = stage.process(test_buffer, ctx)
|
||||
|
||||
assert result == "processed_data"
|
||||
mock_effect.process.assert_called_once()
|
||||
# Should return a list (possibly modified buffer)
|
||||
assert isinstance(result, list)
|
||||
# Noise effect should preserve line count
|
||||
assert len(result) == len(test_buffer)
|
||||
|
||||
def test_effect_plugin_stage_process_with_real_figment(self):
|
||||
"""EffectPluginStage processes figment effect correctly."""
|
||||
from engine.pipeline.params import PipelineParams
|
||||
|
||||
discover_plugins()
|
||||
registry = get_registry()
|
||||
figment = registry.get("figment")
|
||||
|
||||
stage = EffectPluginStage(figment, name="figment")
|
||||
ctx = PipelineContext()
|
||||
ctx.params = PipelineParams()
|
||||
ctx.params.viewport_width = 80
|
||||
ctx.params.viewport_height = 24
|
||||
ctx.params.frame_number = 0
|
||||
|
||||
test_buffer = ["Line 1", "Line 2", "Line 3"]
|
||||
result = stage.process(test_buffer, ctx)
|
||||
|
||||
# Figment is an overlay effect
|
||||
assert stage.is_overlay is True
|
||||
assert stage.stage_type == "overlay"
|
||||
# Result should be a list
|
||||
assert isinstance(result, list)
|
||||
|
||||
103
tests/test_figment_effect.py
Normal file
103
tests/test_figment_effect.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""
|
||||
Tests for the FigmentOverlayEffect plugin.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from engine.effects.plugins import discover_plugins
|
||||
from engine.effects.registry import get_registry
|
||||
from engine.effects.types import EffectConfig, create_effect_context
|
||||
from engine.pipeline.adapters import EffectPluginStage
|
||||
|
||||
|
||||
class TestFigmentEffect:
|
||||
"""Tests for FigmentOverlayEffect."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Discover plugins before each test."""
|
||||
discover_plugins()
|
||||
|
||||
def test_figment_plugin_discovered(self):
|
||||
"""Figment plugin is discovered and registered."""
|
||||
registry = get_registry()
|
||||
figment = registry.get("figment")
|
||||
assert figment is not None
|
||||
assert figment.name == "figment"
|
||||
|
||||
def test_figment_plugin_enabled_by_default(self):
|
||||
"""Figment plugin is enabled by default."""
|
||||
registry = get_registry()
|
||||
figment = registry.get("figment")
|
||||
assert figment.config.enabled is True
|
||||
|
||||
def test_figment_renders_overlay(self):
|
||||
"""Figment renders SVG overlay after interval."""
|
||||
registry = get_registry()
|
||||
figment = registry.get("figment")
|
||||
|
||||
# Configure with short interval for testing
|
||||
config = EffectConfig(
|
||||
enabled=True,
|
||||
intensity=1.0,
|
||||
params={
|
||||
"interval_secs": 0.1, # 100ms
|
||||
"display_secs": 1.0,
|
||||
"figment_dir": "figments",
|
||||
},
|
||||
)
|
||||
figment.configure(config)
|
||||
|
||||
# Create test buffer
|
||||
buf = [" " * 80 for _ in range(24)]
|
||||
|
||||
# Create context
|
||||
ctx = create_effect_context(
|
||||
terminal_width=80,
|
||||
terminal_height=24,
|
||||
frame_number=0,
|
||||
)
|
||||
|
||||
# Process frames until figment renders
|
||||
for i in range(20):
|
||||
result = figment.process(buf, ctx)
|
||||
if len(result) > len(buf):
|
||||
# Figment rendered overlay
|
||||
assert len(result) > len(buf)
|
||||
# Check that overlay lines contain ANSI escape codes
|
||||
overlay_lines = result[len(buf) :]
|
||||
assert len(overlay_lines) > 0
|
||||
# First overlay line should contain cursor positioning
|
||||
assert "\x1b[" in overlay_lines[0]
|
||||
assert "H" in overlay_lines[0]
|
||||
return
|
||||
ctx.frame_number += 1
|
||||
|
||||
pytest.fail("Figment did not render in 20 frames")
|
||||
|
||||
def test_figment_stage_capabilities(self):
|
||||
"""EffectPluginStage wraps figment correctly."""
|
||||
registry = get_registry()
|
||||
figment = registry.get("figment")
|
||||
|
||||
stage = EffectPluginStage(figment, name="figment")
|
||||
assert "effect.figment" in stage.capabilities
|
||||
|
||||
def test_figment_configure_preserves_params(self):
|
||||
"""Figment configuration preserves figment_dir."""
|
||||
registry = get_registry()
|
||||
figment = registry.get("figment")
|
||||
|
||||
# Configure without figment_dir
|
||||
config = EffectConfig(
|
||||
enabled=True,
|
||||
intensity=1.0,
|
||||
params={
|
||||
"interval_secs": 30.0,
|
||||
"display_secs": 3.0,
|
||||
},
|
||||
)
|
||||
figment.configure(config)
|
||||
|
||||
# figment_dir should be preserved
|
||||
assert "figment_dir" in figment.config.params
|
||||
assert figment.config.params["figment_dir"] == "figments"
|
||||
79
tests/test_figment_pipeline.py
Normal file
79
tests/test_figment_pipeline.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Integration tests for figment effect in the pipeline.
|
||||
"""
|
||||
|
||||
from engine.effects.plugins import discover_plugins
|
||||
from engine.effects.registry import get_registry
|
||||
from engine.pipeline import Pipeline, PipelineConfig, get_preset
|
||||
from engine.pipeline.adapters import (
|
||||
EffectPluginStage,
|
||||
SourceItemsToBufferStage,
|
||||
create_stage_from_display,
|
||||
)
|
||||
from engine.pipeline.controller import PipelineRunner
|
||||
|
||||
|
||||
class TestFigmentPipeline:
|
||||
"""Tests for figment effect in pipeline integration."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Discover plugins before each test."""
|
||||
discover_plugins()
|
||||
|
||||
def test_figment_in_pipeline(self):
|
||||
"""Figment effect can be added to pipeline."""
|
||||
registry = get_registry()
|
||||
figment = registry.get("figment")
|
||||
|
||||
pipeline = Pipeline(
|
||||
config=PipelineConfig(
|
||||
source="empty",
|
||||
display="null",
|
||||
camera="feed",
|
||||
effects=["figment"],
|
||||
)
|
||||
)
|
||||
|
||||
# Add source stage
|
||||
from engine.data_sources.sources import EmptyDataSource
|
||||
from engine.pipeline.adapters import DataSourceStage
|
||||
|
||||
empty_source = EmptyDataSource(width=80, height=24)
|
||||
pipeline.add_stage("source", DataSourceStage(empty_source, name="empty"))
|
||||
|
||||
# Add render stage
|
||||
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
|
||||
|
||||
# Add figment effect stage
|
||||
pipeline.add_stage("effect_figment", EffectPluginStage(figment, name="figment"))
|
||||
|
||||
# Add display stage
|
||||
from engine.display import DisplayRegistry
|
||||
|
||||
display = DisplayRegistry.create("null")
|
||||
display.init(0, 0)
|
||||
pipeline.add_stage("display", create_stage_from_display(display, "null"))
|
||||
|
||||
# Build and initialize pipeline
|
||||
pipeline.build()
|
||||
assert pipeline.initialize()
|
||||
|
||||
# Use PipelineRunner to step through frames
|
||||
runner = PipelineRunner(pipeline)
|
||||
runner.start()
|
||||
|
||||
# Run pipeline for a few frames
|
||||
for i in range(10):
|
||||
runner.step()
|
||||
# Result might be None for null display, but that's okay
|
||||
|
||||
# Verify pipeline ran without errors
|
||||
assert pipeline.context.params.frame_number == 10
|
||||
|
||||
def test_figment_preset(self):
|
||||
"""Figment preset is properly configured."""
|
||||
preset = get_preset("test-figment")
|
||||
assert preset is not None
|
||||
assert preset.source == "empty"
|
||||
assert preset.display == "terminal"
|
||||
assert "figment" in preset.effects
|
||||
104
tests/test_figment_render.py
Normal file
104
tests/test_figment_render.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
Tests to verify figment rendering in the pipeline.
|
||||
"""
|
||||
|
||||
from engine.effects.plugins import discover_plugins
|
||||
from engine.effects.registry import get_registry
|
||||
from engine.effects.types import EffectConfig
|
||||
from engine.pipeline import Pipeline, PipelineConfig
|
||||
from engine.pipeline.adapters import (
|
||||
EffectPluginStage,
|
||||
SourceItemsToBufferStage,
|
||||
create_stage_from_display,
|
||||
)
|
||||
from engine.pipeline.controller import PipelineRunner
|
||||
|
||||
|
||||
def test_figment_renders_in_pipeline():
|
||||
"""Verify figment renders overlay in pipeline."""
|
||||
# Discover plugins
|
||||
discover_plugins()
|
||||
|
||||
# Get figment plugin
|
||||
registry = get_registry()
|
||||
figment = registry.get("figment")
|
||||
|
||||
# Configure with short interval for testing
|
||||
config = EffectConfig(
|
||||
enabled=True,
|
||||
intensity=1.0,
|
||||
params={
|
||||
"interval_secs": 0.1, # 100ms
|
||||
"display_secs": 1.0,
|
||||
"figment_dir": "figments",
|
||||
},
|
||||
)
|
||||
figment.configure(config)
|
||||
|
||||
# Create pipeline
|
||||
pipeline = Pipeline(
|
||||
config=PipelineConfig(
|
||||
source="empty",
|
||||
display="null",
|
||||
camera="feed",
|
||||
effects=["figment"],
|
||||
)
|
||||
)
|
||||
|
||||
# Add source stage
|
||||
from engine.data_sources.sources import EmptyDataSource
|
||||
from engine.pipeline.adapters import DataSourceStage
|
||||
|
||||
empty_source = EmptyDataSource(width=80, height=24)
|
||||
pipeline.add_stage("source", DataSourceStage(empty_source, name="empty"))
|
||||
|
||||
# Add render stage
|
||||
pipeline.add_stage("render", SourceItemsToBufferStage(name="items-to-buffer"))
|
||||
|
||||
# Add figment effect stage
|
||||
pipeline.add_stage("effect_figment", EffectPluginStage(figment, name="figment"))
|
||||
|
||||
# Add display stage
|
||||
from engine.display import DisplayRegistry
|
||||
|
||||
display = DisplayRegistry.create("null")
|
||||
display.init(0, 0)
|
||||
pipeline.add_stage("display", create_stage_from_display(display, "null"))
|
||||
|
||||
# Build and initialize pipeline
|
||||
pipeline.build()
|
||||
assert pipeline.initialize()
|
||||
|
||||
# Use PipelineRunner to step through frames
|
||||
runner = PipelineRunner(pipeline)
|
||||
runner.start()
|
||||
|
||||
# Run pipeline until figment renders (or timeout)
|
||||
figment_rendered = False
|
||||
for i in range(30):
|
||||
runner.step()
|
||||
|
||||
# Check if figment rendered by inspecting the display's internal buffer
|
||||
# The null display stores the last rendered buffer
|
||||
if hasattr(display, "_last_buffer") and display._last_buffer:
|
||||
buffer = display._last_buffer
|
||||
# Check if buffer contains ANSI escape codes (indicating figment overlay)
|
||||
# Figment adds overlay lines at the end of the buffer
|
||||
for line in buffer:
|
||||
if "\x1b[" in line:
|
||||
figment_rendered = True
|
||||
print(f"Figment rendered at frame {i}")
|
||||
# Print first few lines containing escape codes
|
||||
for j, line in enumerate(buffer[:10]):
|
||||
if "\x1b[" in line:
|
||||
print(f"Line {j}: {repr(line[:80])}")
|
||||
break
|
||||
if figment_rendered:
|
||||
break
|
||||
|
||||
assert figment_rendered, "Figment did not render in 30 frames"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_figment_renders_in_pipeline()
|
||||
print("Test passed!")
|
||||
Reference in New Issue
Block a user