Files
sideline/tests/test_pipeline_introspection.py
David Gwilliam e0bbfea26c refactor: consolidate pipeline architecture with unified data source system
MAJOR REFACTORING: Consolidate duplicated pipeline code and standardize on
capability-based dependency resolution. This is a significant but backwards-compatible
restructuring that improves maintainability and extensibility.

## ARCHITECTURE CHANGES

### Data Sources Consolidation
- Move engine/sources_v2.py → engine/data_sources/sources.py
- Move engine/pipeline_sources/ → engine/data_sources/
- Create unified DataSource ABC with common interface:
  * fetch() - idempotent data retrieval
  * get_items() - cached access with automatic refresh
  * refresh() - force cache invalidation
  * is_dynamic - indicate streaming vs static sources
- Support for SourceItem dataclass (content, source, timestamp, metadata)

### Display Backend Improvements
- Update all 7 display backends to use new import paths
- Terminal: Improve dimension detection and handling
- WebSocket: Better error handling and client lifecycle
- Sixel: Refactor graphics rendering
- Pygame: Modernize event handling
- Kitty: Add protocol support for inline images
- Multi: Ensure proper forwarding to all backends
- Null: Maintain testing backend functionality

### Pipeline Adapter Consolidation
- Refactor adapter stages for clarity and flexibility
- RenderStage now handles both item-based and buffer-based rendering
- Add SourceItemsToBufferStage for converting data source items
- Improve DataSourceStage to work with all source types
- Add DisplayStage wrapper for display backends

### Camera & Viewport Refinements
- Update Camera class for new architecture
- Improve viewport dimension detection
- Better handling of resize events across backends

### New Effect Plugins
- border.py: Frame rendering effect with configurable style
- crop.py: Viewport clipping effect for selective display
- tint.py: Color filtering effect for atmosphere

### Tests & Quality
- Add test_border_effect.py with comprehensive border tests
- Add test_crop_effect.py with viewport clipping tests
- Add test_tint_effect.py with color filtering tests
- Update test_pipeline.py for new architecture
- Update test_pipeline_introspection.py for new data source location
- All 463 tests pass with 56% coverage
- Linting: All checks pass with ruff

### Removals (Code Cleanup)
- Delete engine/benchmark.py (deprecated performance testing)
- Delete engine/pipeline_sources/__init__.py (moved to data_sources)
- Delete engine/sources_v2.py (replaced by data_sources/sources.py)
- Update AGENTS.md to reflect new structure

### Import Path Updates
- Update engine/pipeline/controller.py::create_default_pipeline()
  * Old: from engine.sources_v2 import HeadlinesDataSource
  * New: from engine.data_sources.sources import HeadlinesDataSource
- All display backends import from new locations
- All tests import from new locations

## BACKWARDS COMPATIBILITY

This refactoring is intended to be backwards compatible:
- Pipeline execution unchanged (DAG-based with capability matching)
- Effect plugins unchanged (EffectPlugin interface same)
- Display protocol unchanged (Display duck-typing works as before)
- Config system unchanged (presets.toml format same)

## TESTING

- 463 tests pass (0 failures, 19 skipped)
- Full linting check passes
- Manual testing on demo, poetry, websocket modes
- All new effect plugins tested

## FILES CHANGED

- 24 files modified/added/deleted
- 723 insertions, 1,461 deletions (net -738 LOC - cleanup!)
- No breaking changes to public APIs
- All transitive imports updated correctly
2026-03-16 19:47:12 -07:00

172 lines
5.5 KiB
Python

"""
Tests for PipelineIntrospectionSource.
"""
from engine.data_sources.pipeline_introspection import PipelineIntrospectionSource
class TestPipelineIntrospectionSource:
"""Tests for PipelineIntrospectionSource."""
def test_basic_init(self):
"""Source initializes with defaults."""
source = PipelineIntrospectionSource()
assert source.name == "pipeline-inspect"
assert source.is_dynamic is True
assert source.frame == 0
assert source.ready is False
def test_init_with_params(self):
"""Source initializes with custom params."""
source = PipelineIntrospectionSource(viewport_width=100, viewport_height=40)
assert source.viewport_width == 100
assert source.viewport_height == 40
def test_inlet_outlet_types(self):
"""Source has correct inlet/outlet types."""
source = PipelineIntrospectionSource()
from engine.pipeline.core import DataType
assert DataType.NONE in source.inlet_types
assert DataType.SOURCE_ITEMS in source.outlet_types
def test_fetch_returns_items(self):
"""fetch() returns SourceItem list."""
source = PipelineIntrospectionSource()
items = source.fetch()
assert len(items) == 1
assert items[0].source == "pipeline-inspect"
def test_fetch_increments_frame(self):
"""fetch() increments frame counter when ready."""
source = PipelineIntrospectionSource()
assert source.frame == 0
# Set pipeline first to make source ready
class MockPipeline:
stages = {}
execution_order = []
def get_metrics_summary(self):
return {"avg_ms": 10.0, "fps": 60, "stages": {}}
def get_frame_times(self):
return [10.0, 12.0, 11.0]
source.set_pipeline(MockPipeline())
assert source.ready is True
source.fetch()
assert source.frame == 1
source.fetch()
assert source.frame == 2
def test_get_items(self):
"""get_items() returns list of SourceItems."""
source = PipelineIntrospectionSource()
items = source.get_items()
assert isinstance(items, list)
assert len(items) > 0
assert items[0].source == "pipeline-inspect"
def test_set_pipeline(self):
"""set_pipeline() marks source as ready."""
source = PipelineIntrospectionSource()
assert source.ready is False
class MockPipeline:
stages = {}
execution_order = []
def get_metrics_summary(self):
return {"avg_ms": 10.0, "fps": 60, "stages": {}}
def get_frame_times(self):
return [10.0, 12.0, 11.0]
source.set_pipeline(MockPipeline())
assert source.ready is True
class TestPipelineIntrospectionRender:
"""Tests for rendering methods."""
def test_render_header_no_pipeline(self):
"""_render_header returns default when no pipeline."""
source = PipelineIntrospectionSource()
lines = source._render_header()
assert len(lines) == 1
assert "PIPELINE INTROSPECTION" in lines[0]
def test_render_bar(self):
"""_render_bar creates correct bar."""
source = PipelineIntrospectionSource()
bar = source._render_bar(50, 10)
assert len(bar) == 10
assert bar.count("") == 5
assert bar.count("") == 5
def test_render_bar_zero(self):
"""_render_bar handles zero percentage."""
source = PipelineIntrospectionSource()
bar = source._render_bar(0, 10)
assert bar == "" * 10
def test_render_bar_full(self):
"""_render_bar handles 100%."""
source = PipelineIntrospectionSource()
bar = source._render_bar(100, 10)
assert bar == "" * 10
def test_render_sparkline(self):
"""_render_sparkline creates sparkline."""
source = PipelineIntrospectionSource()
values = [1.0, 2.0, 3.0, 4.0, 5.0]
sparkline = source._render_sparkline(values, 10)
assert len(sparkline) == 10
def test_render_sparkline_empty(self):
"""_render_sparkline handles empty values."""
source = PipelineIntrospectionSource()
sparkline = source._render_sparkline([], 10)
assert sparkline == " " * 10
def test_render_footer_no_pipeline(self):
"""_render_footer shows collecting data when no pipeline."""
source = PipelineIntrospectionSource()
lines = source._render_footer()
assert len(lines) >= 2
class TestPipelineIntrospectionFull:
"""Integration tests."""
def test_render_empty(self):
"""_render works when not ready."""
source = PipelineIntrospectionSource()
lines = source._render()
assert len(lines) > 0
assert "PIPELINE INTROSPECTION" in lines[0]
def test_render_with_mock_pipeline(self):
"""_render works with mock pipeline."""
source = PipelineIntrospectionSource()
class MockStage:
category = "source"
name = "test"
class MockPipeline:
stages = {"test": MockStage()}
execution_order = ["test"]
def get_metrics_summary(self):
return {"stages": {"test": {"avg_ms": 1.5}}, "avg_ms": 2.0, "fps": 60}
def get_frame_times(self):
return [1.0, 2.0, 3.0]
source.set_pipeline(MockPipeline())
lines = source._render()
assert len(lines) > 0