forked from genewildish/Mainline
- 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
282 lines
8.7 KiB
Python
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
|