Files
sideline/tests/test_pipeline.py
David Gwilliam 828b8489e1 feat(pipeline): improve new pipeline architecture
- Add TransformDataSource for filtering/mapping source items
- Add MetricsDataSource for rendering live pipeline metrics as ASCII art
- Fix display stage registration in StageRegistry
- Register sources with both class name and simple name aliases
- Fix DisplayStage.init() to pass reuse parameter
- Simplify create_default_pipeline to use DataSourceStage wrapper
- Set pygame as default display
- Remove old pipeline tasks from mise.toml
- Add tests for new pipeline architecture
2026-03-16 11:30:21 -07:00

282 lines
8.7 KiB
Python

"""
Tests for the new unified pipeline architecture.
"""
from unittest.mock import MagicMock
from engine.pipeline import (
Pipeline,
PipelineConfig,
PipelineContext,
Stage,
StageRegistry,
create_default_pipeline,
discover_stages,
)
class TestStageRegistry:
"""Tests for StageRegistry."""
def setup_method(self):
"""Reset registry before each test."""
StageRegistry._discovered = False
StageRegistry._categories.clear()
StageRegistry._instances.clear()
def test_discover_stages_registers_sources(self):
"""discover_stages registers source stages."""
discover_stages()
sources = StageRegistry.list("source")
assert "HeadlinesDataSource" in sources
assert "PoetryDataSource" in sources
assert "PipelineDataSource" in sources
def test_discover_stages_registers_displays(self):
"""discover_stages registers display stages."""
discover_stages()
displays = StageRegistry.list("display")
assert "terminal" in displays
assert "pygame" in displays
assert "websocket" in displays
assert "null" in displays
assert "sixel" in displays
def test_create_source_stage(self):
"""StageRegistry.create creates source stages."""
discover_stages()
source = StageRegistry.create("source", "HeadlinesDataSource")
assert source is not None
assert source.name == "headlines"
def test_create_display_stage(self):
"""StageRegistry.create creates display stages."""
discover_stages()
display = StageRegistry.create("display", "terminal")
assert display is not None
assert hasattr(display, "_display")
def test_create_display_stage_pygame(self):
"""StageRegistry.create creates pygame display stage."""
discover_stages()
display = StageRegistry.create("display", "pygame")
assert display is not None
class TestPipeline:
"""Tests for Pipeline class."""
def setup_method(self):
"""Reset registry before each test."""
StageRegistry._discovered = False
StageRegistry._categories.clear()
StageRegistry._instances.clear()
discover_stages()
def test_create_pipeline(self):
"""Pipeline can be created with config."""
config = PipelineConfig(source="headlines", display="terminal")
pipeline = Pipeline(config=config)
assert pipeline.config is not None
assert pipeline.config.source == "headlines"
assert pipeline.config.display == "terminal"
def test_add_stage(self):
"""Pipeline.add_stage adds a stage."""
pipeline = Pipeline()
mock_stage = MagicMock(spec=Stage)
mock_stage.name = "test_stage"
mock_stage.category = "test"
pipeline.add_stage("test", mock_stage)
assert "test" in pipeline.stages
def test_build_resolves_dependencies(self):
"""Pipeline.build resolves execution order."""
pipeline = Pipeline()
mock_source = MagicMock(spec=Stage)
mock_source.name = "source"
mock_source.category = "source"
mock_source.dependencies = set()
mock_display = MagicMock(spec=Stage)
mock_display.name = "display"
mock_display.category = "display"
mock_display.dependencies = {"source"}
pipeline.add_stage("source", mock_source)
pipeline.add_stage("display", mock_display)
pipeline.build()
assert pipeline._initialized is True
assert "source" in pipeline.execution_order
assert "display" in pipeline.execution_order
def test_execute_runs_stages(self):
"""Pipeline.execute runs all stages in order."""
pipeline = Pipeline()
call_order = []
mock_source = MagicMock(spec=Stage)
mock_source.name = "source"
mock_source.category = "source"
mock_source.dependencies = set()
mock_source.process = lambda data, ctx: call_order.append("source") or "data"
mock_effect = MagicMock(spec=Stage)
mock_effect.name = "effect"
mock_effect.category = "effect"
mock_effect.dependencies = {"source"}
mock_effect.process = lambda data, ctx: call_order.append("effect") or data
mock_display = MagicMock(spec=Stage)
mock_display.name = "display"
mock_display.category = "display"
mock_display.dependencies = {"effect"}
mock_display.process = lambda data, ctx: call_order.append("display") or data
pipeline.add_stage("source", mock_source)
pipeline.add_stage("effect", mock_effect)
pipeline.add_stage("display", mock_display)
pipeline.build()
result = pipeline.execute(None)
assert result.success is True
assert call_order == ["source", "effect", "display"]
def test_execute_handles_stage_failure(self):
"""Pipeline.execute handles stage failures."""
pipeline = Pipeline()
mock_source = MagicMock(spec=Stage)
mock_source.name = "source"
mock_source.category = "source"
mock_source.dependencies = set()
mock_source.process = lambda data, ctx: "data"
mock_failing = MagicMock(spec=Stage)
mock_failing.name = "failing"
mock_failing.category = "effect"
mock_failing.dependencies = {"source"}
mock_failing.optional = False
mock_failing.process = lambda data, ctx: (_ for _ in ()).throw(
Exception("fail")
)
pipeline.add_stage("source", mock_source)
pipeline.add_stage("failing", mock_failing)
pipeline.build()
result = pipeline.execute(None)
assert result.success is False
assert result.error is not None
def test_optional_stage_failure_continues(self):
"""Pipeline.execute continues on optional stage failure."""
pipeline = Pipeline()
mock_source = MagicMock(spec=Stage)
mock_source.name = "source"
mock_source.category = "source"
mock_source.dependencies = set()
mock_source.process = lambda data, ctx: "data"
mock_optional = MagicMock(spec=Stage)
mock_optional.name = "optional"
mock_optional.category = "effect"
mock_optional.dependencies = {"source"}
mock_optional.optional = True
mock_optional.process = lambda data, ctx: (_ for _ in ()).throw(
Exception("fail")
)
pipeline.add_stage("source", mock_source)
pipeline.add_stage("optional", mock_optional)
pipeline.build()
result = pipeline.execute(None)
assert result.success is True
class TestPipelineContext:
"""Tests for PipelineContext."""
def test_init_empty(self):
"""PipelineContext initializes with empty services and state."""
ctx = PipelineContext()
assert ctx.services == {}
assert ctx.state == {}
def test_init_with_services(self):
"""PipelineContext accepts initial services."""
ctx = PipelineContext(services={"display": MagicMock()})
assert "display" in ctx.services
def test_init_with_state(self):
"""PipelineContext accepts initial state."""
ctx = PipelineContext(initial_state={"count": 42})
assert ctx.get_state("count") == 42
def test_get_set_services(self):
"""PipelineContext can get/set services."""
ctx = PipelineContext()
mock_service = MagicMock()
ctx.set("test_service", mock_service)
assert ctx.get("test_service") == mock_service
def test_get_set_state(self):
"""PipelineContext can get/set state."""
ctx = PipelineContext()
ctx.set_state("counter", 100)
assert ctx.get_state("counter") == 100
def test_lazy_resolver(self):
"""PipelineContext resolves lazy services."""
ctx = PipelineContext()
config = ctx.get("config")
assert config is not None
def test_has_capability(self):
"""PipelineContext.has_capability checks for services."""
ctx = PipelineContext(services={"display.output": MagicMock()})
assert ctx.has_capability("display.output") is True
assert ctx.has_capability("missing") is False
class TestCreateDefaultPipeline:
"""Tests for create_default_pipeline function."""
def setup_method(self):
"""Reset registry before each test."""
StageRegistry._discovered = False
StageRegistry._categories.clear()
StageRegistry._instances.clear()
discover_stages()
def test_create_default_pipeline(self):
"""create_default_pipeline creates a working pipeline."""
pipeline = create_default_pipeline()
assert pipeline is not None
assert "display" in pipeline.stages