forked from genewildish/Mainline
feat(pipeline): add PureData-style inlet/outlet typing
- Add DataType enum (SOURCE_ITEMS, TEXT_BUFFER, etc.) - Add inlet_types and outlet_types to Stage - Add _validate_types() for type checking at build time - Update tests with proper type annotations
This commit is contained in:
@@ -100,16 +100,28 @@ class TestPipeline:
|
||||
|
||||
def test_build_resolves_dependencies(self):
|
||||
"""Pipeline.build resolves execution order."""
|
||||
from engine.pipeline.core import DataType
|
||||
|
||||
pipeline = Pipeline()
|
||||
mock_source = MagicMock(spec=Stage)
|
||||
mock_source.name = "source"
|
||||
mock_source.category = "source"
|
||||
mock_source.stage_type = "source"
|
||||
mock_source.render_order = 0
|
||||
mock_source.is_overlay = False
|
||||
mock_source.inlet_types = {DataType.NONE}
|
||||
mock_source.outlet_types = {DataType.SOURCE_ITEMS}
|
||||
mock_source.dependencies = set()
|
||||
mock_source.capabilities = {"source"}
|
||||
|
||||
mock_display = MagicMock(spec=Stage)
|
||||
mock_display.name = "display"
|
||||
mock_display.category = "display"
|
||||
mock_display.stage_type = "display"
|
||||
mock_display.render_order = 0
|
||||
mock_display.is_overlay = False
|
||||
mock_display.inlet_types = {DataType.ANY} # Accept any type
|
||||
mock_display.outlet_types = {DataType.NONE}
|
||||
mock_display.dependencies = {"source"}
|
||||
mock_display.capabilities = {"display"}
|
||||
|
||||
@@ -123,6 +135,8 @@ class TestPipeline:
|
||||
|
||||
def test_execute_runs_stages(self):
|
||||
"""Pipeline.execute runs all stages in order."""
|
||||
from engine.pipeline.core import DataType
|
||||
|
||||
pipeline = Pipeline()
|
||||
|
||||
call_order = []
|
||||
@@ -130,6 +144,11 @@ class TestPipeline:
|
||||
mock_source = MagicMock(spec=Stage)
|
||||
mock_source.name = "source"
|
||||
mock_source.category = "source"
|
||||
mock_source.stage_type = "source"
|
||||
mock_source.render_order = 0
|
||||
mock_source.is_overlay = False
|
||||
mock_source.inlet_types = {DataType.NONE}
|
||||
mock_source.outlet_types = {DataType.SOURCE_ITEMS}
|
||||
mock_source.dependencies = set()
|
||||
mock_source.capabilities = {"source"}
|
||||
mock_source.process = lambda data, ctx: call_order.append("source") or "data"
|
||||
@@ -137,6 +156,11 @@ class TestPipeline:
|
||||
mock_effect = MagicMock(spec=Stage)
|
||||
mock_effect.name = "effect"
|
||||
mock_effect.category = "effect"
|
||||
mock_effect.stage_type = "effect"
|
||||
mock_effect.render_order = 0
|
||||
mock_effect.is_overlay = False
|
||||
mock_effect.inlet_types = {DataType.SOURCE_ITEMS}
|
||||
mock_effect.outlet_types = {DataType.TEXT_BUFFER}
|
||||
mock_effect.dependencies = {"source"}
|
||||
mock_effect.capabilities = {"effect"}
|
||||
mock_effect.process = lambda data, ctx: call_order.append("effect") or data
|
||||
@@ -144,6 +168,11 @@ class TestPipeline:
|
||||
mock_display = MagicMock(spec=Stage)
|
||||
mock_display.name = "display"
|
||||
mock_display.category = "display"
|
||||
mock_display.stage_type = "display"
|
||||
mock_display.render_order = 0
|
||||
mock_display.is_overlay = False
|
||||
mock_display.inlet_types = {DataType.TEXT_BUFFER}
|
||||
mock_display.outlet_types = {DataType.NONE}
|
||||
mock_display.dependencies = {"effect"}
|
||||
mock_display.capabilities = {"display"}
|
||||
mock_display.process = lambda data, ctx: call_order.append("display") or data
|
||||
@@ -165,6 +194,9 @@ class TestPipeline:
|
||||
mock_source = MagicMock(spec=Stage)
|
||||
mock_source.name = "source"
|
||||
mock_source.category = "source"
|
||||
mock_source.stage_type = "source"
|
||||
mock_source.render_order = 0
|
||||
mock_source.is_overlay = False
|
||||
mock_source.dependencies = set()
|
||||
mock_source.capabilities = {"source"}
|
||||
mock_source.process = lambda data, ctx: "data"
|
||||
@@ -172,6 +204,9 @@ class TestPipeline:
|
||||
mock_failing = MagicMock(spec=Stage)
|
||||
mock_failing.name = "failing"
|
||||
mock_failing.category = "effect"
|
||||
mock_failing.stage_type = "effect"
|
||||
mock_failing.render_order = 0
|
||||
mock_failing.is_overlay = False
|
||||
mock_failing.dependencies = {"source"}
|
||||
mock_failing.capabilities = {"effect"}
|
||||
mock_failing.optional = False
|
||||
@@ -195,6 +230,9 @@ class TestPipeline:
|
||||
mock_source = MagicMock(spec=Stage)
|
||||
mock_source.name = "source"
|
||||
mock_source.category = "source"
|
||||
mock_source.stage_type = "source"
|
||||
mock_source.render_order = 0
|
||||
mock_source.is_overlay = False
|
||||
mock_source.dependencies = set()
|
||||
mock_source.capabilities = {"source"}
|
||||
mock_source.process = lambda data, ctx: "data"
|
||||
@@ -202,6 +240,9 @@ class TestPipeline:
|
||||
mock_optional = MagicMock(spec=Stage)
|
||||
mock_optional.name = "optional"
|
||||
mock_optional.category = "effect"
|
||||
mock_optional.stage_type = "effect"
|
||||
mock_optional.render_order = 0
|
||||
mock_optional.is_overlay = False
|
||||
mock_optional.dependencies = {"source"}
|
||||
mock_optional.capabilities = {"effect"}
|
||||
mock_optional.optional = True
|
||||
@@ -918,3 +959,227 @@ class TestPipelineMetrics:
|
||||
# After reset, metrics collection starts fresh
|
||||
pipeline.execute("test3")
|
||||
assert pipeline.get_metrics_summary()["frame_count"] == 1
|
||||
|
||||
|
||||
class TestOverlayStages:
|
||||
"""Tests for overlay stage support."""
|
||||
|
||||
def test_stage_is_overlay_property(self):
|
||||
"""Stage has is_overlay property defaulting to False."""
|
||||
from engine.pipeline.core import Stage
|
||||
|
||||
class TestStage(Stage):
|
||||
name = "test"
|
||||
category = "effect"
|
||||
|
||||
def process(self, data, ctx):
|
||||
return data
|
||||
|
||||
stage = TestStage()
|
||||
assert stage.is_overlay is False
|
||||
|
||||
def test_stage_render_order_property(self):
|
||||
"""Stage has render_order property defaulting to 0."""
|
||||
from engine.pipeline.core import Stage
|
||||
|
||||
class TestStage(Stage):
|
||||
name = "test"
|
||||
category = "effect"
|
||||
|
||||
def process(self, data, ctx):
|
||||
return data
|
||||
|
||||
stage = TestStage()
|
||||
assert stage.render_order == 0
|
||||
|
||||
def test_stage_stage_type_property(self):
|
||||
"""Stage has stage_type property defaulting to category."""
|
||||
from engine.pipeline.core import Stage
|
||||
|
||||
class TestStage(Stage):
|
||||
name = "test"
|
||||
category = "effect"
|
||||
|
||||
def process(self, data, ctx):
|
||||
return data
|
||||
|
||||
stage = TestStage()
|
||||
assert stage.stage_type == "effect"
|
||||
|
||||
def test_pipeline_get_overlay_stages(self):
|
||||
"""Pipeline.get_overlay_stages returns overlay stages sorted by render_order."""
|
||||
from engine.pipeline.controller import Pipeline
|
||||
from engine.pipeline.core import Stage
|
||||
|
||||
class OverlayStageA(Stage):
|
||||
name = "overlay_a"
|
||||
category = "overlay"
|
||||
|
||||
@property
|
||||
def is_overlay(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def render_order(self):
|
||||
return 10
|
||||
|
||||
def process(self, data, ctx):
|
||||
return data
|
||||
|
||||
class OverlayStageB(Stage):
|
||||
name = "overlay_b"
|
||||
category = "overlay"
|
||||
|
||||
@property
|
||||
def is_overlay(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def render_order(self):
|
||||
return 5
|
||||
|
||||
def process(self, data, ctx):
|
||||
return data
|
||||
|
||||
class RegularStage(Stage):
|
||||
name = "regular"
|
||||
category = "effect"
|
||||
|
||||
def process(self, data, ctx):
|
||||
return data
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_stage("overlay_a", OverlayStageA())
|
||||
pipeline.add_stage("overlay_b", OverlayStageB())
|
||||
pipeline.add_stage("regular", RegularStage())
|
||||
pipeline.build()
|
||||
|
||||
overlays = pipeline.get_overlay_stages()
|
||||
assert len(overlays) == 2
|
||||
# Should be sorted by render_order
|
||||
assert overlays[0].name == "overlay_b" # render_order=5
|
||||
assert overlays[1].name == "overlay_a" # render_order=10
|
||||
|
||||
def test_pipeline_executes_overlays_after_regular(self):
|
||||
"""Pipeline executes overlays after regular stages."""
|
||||
from engine.pipeline.controller import Pipeline
|
||||
from engine.pipeline.core import Stage
|
||||
|
||||
call_order = []
|
||||
|
||||
class RegularStage(Stage):
|
||||
name = "regular"
|
||||
category = "effect"
|
||||
|
||||
def process(self, data, ctx):
|
||||
call_order.append("regular")
|
||||
return data
|
||||
|
||||
class OverlayStage(Stage):
|
||||
name = "overlay"
|
||||
category = "overlay"
|
||||
|
||||
@property
|
||||
def is_overlay(self):
|
||||
return True
|
||||
|
||||
@property
|
||||
def render_order(self):
|
||||
return 100
|
||||
|
||||
def process(self, data, ctx):
|
||||
call_order.append("overlay")
|
||||
return data
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_stage("regular", RegularStage())
|
||||
pipeline.add_stage("overlay", OverlayStage())
|
||||
pipeline.build()
|
||||
|
||||
pipeline.execute("data")
|
||||
|
||||
assert call_order == ["regular", "overlay"]
|
||||
|
||||
def test_effect_plugin_stage_hud_is_overlay(self):
|
||||
"""EffectPluginStage marks HUD as overlay."""
|
||||
from engine.effects.types import EffectConfig, EffectPlugin
|
||||
from engine.pipeline.adapters import EffectPluginStage
|
||||
|
||||
class HudEffect(EffectPlugin):
|
||||
name = "hud"
|
||||
config = EffectConfig(enabled=True)
|
||||
|
||||
def process(self, buf, ctx):
|
||||
return buf
|
||||
|
||||
def configure(self, config):
|
||||
pass
|
||||
|
||||
stage = EffectPluginStage(HudEffect(), name="hud")
|
||||
assert stage.is_overlay is True
|
||||
assert stage.stage_type == "overlay"
|
||||
assert stage.render_order == 100
|
||||
|
||||
def test_effect_plugin_stage_non_hud_not_overlay(self):
|
||||
"""EffectPluginStage marks non-HUD effects as not overlay."""
|
||||
from engine.effects.types import EffectConfig, EffectPlugin
|
||||
from engine.pipeline.adapters import EffectPluginStage
|
||||
|
||||
class FadeEffect(EffectPlugin):
|
||||
name = "fade"
|
||||
config = EffectConfig(enabled=True)
|
||||
|
||||
def process(self, buf, ctx):
|
||||
return buf
|
||||
|
||||
def configure(self, config):
|
||||
pass
|
||||
|
||||
stage = EffectPluginStage(FadeEffect(), name="fade")
|
||||
assert stage.is_overlay is False
|
||||
assert stage.stage_type == "effect"
|
||||
assert stage.render_order == 0
|
||||
|
||||
def test_pipeline_get_stage_type(self):
|
||||
"""Pipeline.get_stage_type returns stage_type for a stage."""
|
||||
from engine.pipeline.controller import Pipeline
|
||||
from engine.pipeline.core import Stage
|
||||
|
||||
class TestStage(Stage):
|
||||
name = "test"
|
||||
category = "effect"
|
||||
|
||||
@property
|
||||
def stage_type(self):
|
||||
return "overlay"
|
||||
|
||||
def process(self, data, ctx):
|
||||
return data
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_stage("test", TestStage())
|
||||
pipeline.build()
|
||||
|
||||
assert pipeline.get_stage_type("test") == "overlay"
|
||||
|
||||
def test_pipeline_get_render_order(self):
|
||||
"""Pipeline.get_render_order returns render_order for a stage."""
|
||||
from engine.pipeline.controller import Pipeline
|
||||
from engine.pipeline.core import Stage
|
||||
|
||||
class TestStage(Stage):
|
||||
name = "test"
|
||||
category = "effect"
|
||||
|
||||
@property
|
||||
def render_order(self):
|
||||
return 42
|
||||
|
||||
def process(self, data, ctx):
|
||||
return data
|
||||
|
||||
pipeline = Pipeline()
|
||||
pipeline.add_stage("test", TestStage())
|
||||
pipeline.build()
|
||||
|
||||
assert pipeline.get_render_order("test") == 42
|
||||
|
||||
Reference in New Issue
Block a user