forked from genewildish/Mainline
Major changes: - Pipeline architecture with capability-based dependency resolution - Effects plugin system with performance monitoring - Display abstraction with multiple backends (terminal, null, websocket) - Camera system for viewport scrolling - Sensor framework for real-time input - Command-and-control system via ntfy - WebSocket display backend for browser clients - Comprehensive test suite and documentation Issue #48: ADR for preset scripting language included This commit consolidates 110 individual commits into a single feature integration that can be reviewed and tested before further refinement.
221 lines
7.4 KiB
Python
221 lines
7.4 KiB
Python
"""
|
|
Tests for engine/data_sources/sources.py - data source implementations.
|
|
|
|
Tests HeadlinesDataSource, PoetryDataSource, EmptyDataSource, and the
|
|
base DataSource class functionality.
|
|
"""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from engine.data_sources.sources import (
|
|
EmptyDataSource,
|
|
HeadlinesDataSource,
|
|
PoetryDataSource,
|
|
SourceItem,
|
|
)
|
|
|
|
|
|
class TestSourceItem:
|
|
"""Test SourceItem dataclass."""
|
|
|
|
def test_source_item_creation(self):
|
|
"""SourceItem can be created with required fields."""
|
|
item = SourceItem(
|
|
content="Test headline",
|
|
source="test_source",
|
|
timestamp="2024-01-01",
|
|
)
|
|
assert item.content == "Test headline"
|
|
assert item.source == "test_source"
|
|
assert item.timestamp == "2024-01-01"
|
|
assert item.metadata is None
|
|
|
|
def test_source_item_with_metadata(self):
|
|
"""SourceItem can include optional metadata."""
|
|
metadata = {"author": "John", "category": "tech"}
|
|
item = SourceItem(
|
|
content="Test",
|
|
source="test",
|
|
timestamp="2024-01-01",
|
|
metadata=metadata,
|
|
)
|
|
assert item.metadata == metadata
|
|
|
|
|
|
class TestEmptyDataSource:
|
|
"""Test EmptyDataSource."""
|
|
|
|
def test_empty_source_name(self):
|
|
"""EmptyDataSource has correct name."""
|
|
source = EmptyDataSource()
|
|
assert source.name == "empty"
|
|
|
|
def test_empty_source_is_not_dynamic(self):
|
|
"""EmptyDataSource is static, not dynamic."""
|
|
source = EmptyDataSource()
|
|
assert source.is_dynamic is False
|
|
|
|
def test_empty_source_fetch_returns_blank_content(self):
|
|
"""EmptyDataSource.fetch() returns blank lines."""
|
|
source = EmptyDataSource(width=80, height=24)
|
|
items = source.fetch()
|
|
|
|
assert len(items) == 1
|
|
assert isinstance(items[0], SourceItem)
|
|
assert items[0].source == "empty"
|
|
# Content should be 24 lines of 80 spaces
|
|
lines = items[0].content.split("\n")
|
|
assert len(lines) == 24
|
|
assert all(len(line) == 80 for line in lines)
|
|
|
|
def test_empty_source_get_items_caches_result(self):
|
|
"""EmptyDataSource.get_items() caches the result."""
|
|
source = EmptyDataSource()
|
|
items1 = source.get_items()
|
|
items2 = source.get_items()
|
|
# Should return same cached items (same object reference)
|
|
assert items1 is items2
|
|
|
|
|
|
class TestHeadlinesDataSource:
|
|
"""Test HeadlinesDataSource."""
|
|
|
|
def test_headlines_source_name(self):
|
|
"""HeadlinesDataSource has correct name."""
|
|
source = HeadlinesDataSource()
|
|
assert source.name == "headlines"
|
|
|
|
def test_headlines_source_is_static(self):
|
|
"""HeadlinesDataSource is static."""
|
|
source = HeadlinesDataSource()
|
|
assert source.is_dynamic is False
|
|
|
|
def test_headlines_fetch_returns_source_items(self):
|
|
"""HeadlinesDataSource.fetch() returns SourceItem list."""
|
|
mock_items = [
|
|
("Test Article 1", "source1", "10:30"),
|
|
("Test Article 2", "source2", "11:45"),
|
|
]
|
|
with patch("engine.fetch.fetch_all") as mock_fetch_all:
|
|
mock_fetch_all.return_value = (mock_items, 2, 0)
|
|
|
|
source = HeadlinesDataSource()
|
|
items = source.fetch()
|
|
|
|
assert len(items) == 2
|
|
assert all(isinstance(item, SourceItem) for item in items)
|
|
assert items[0].content == "Test Article 1"
|
|
assert items[0].source == "source1"
|
|
assert items[0].timestamp == "10:30"
|
|
|
|
def test_headlines_fetch_with_empty_feed(self):
|
|
"""HeadlinesDataSource handles empty feeds gracefully."""
|
|
with patch("engine.fetch.fetch_all") as mock_fetch_all:
|
|
mock_fetch_all.return_value = ([], 0, 1)
|
|
|
|
source = HeadlinesDataSource()
|
|
items = source.fetch()
|
|
|
|
# Should return empty list
|
|
assert isinstance(items, list)
|
|
assert len(items) == 0
|
|
|
|
def test_headlines_get_items_caches_result(self):
|
|
"""HeadlinesDataSource.get_items() caches the result."""
|
|
mock_items = [("Test Article", "source", "12:00")]
|
|
with patch("engine.fetch.fetch_all") as mock_fetch_all:
|
|
mock_fetch_all.return_value = (mock_items, 1, 0)
|
|
|
|
source = HeadlinesDataSource()
|
|
items1 = source.get_items()
|
|
items2 = source.get_items()
|
|
|
|
# Should only call fetch once (cached)
|
|
assert mock_fetch_all.call_count == 1
|
|
assert items1 is items2
|
|
|
|
def test_headlines_refresh_clears_cache(self):
|
|
"""HeadlinesDataSource.refresh() clears cache and refetches."""
|
|
mock_items = [("Test Article", "source", "12:00")]
|
|
with patch("engine.fetch.fetch_all") as mock_fetch_all:
|
|
mock_fetch_all.return_value = (mock_items, 1, 0)
|
|
|
|
source = HeadlinesDataSource()
|
|
source.get_items()
|
|
source.refresh()
|
|
source.get_items()
|
|
|
|
# Should call fetch twice (once for initial, once for refresh)
|
|
assert mock_fetch_all.call_count == 2
|
|
|
|
|
|
class TestPoetryDataSource:
|
|
"""Test PoetryDataSource."""
|
|
|
|
def test_poetry_source_name(self):
|
|
"""PoetryDataSource has correct name."""
|
|
source = PoetryDataSource()
|
|
assert source.name == "poetry"
|
|
|
|
def test_poetry_source_is_static(self):
|
|
"""PoetryDataSource is static."""
|
|
source = PoetryDataSource()
|
|
assert source.is_dynamic is False
|
|
|
|
def test_poetry_fetch_returns_source_items(self):
|
|
"""PoetryDataSource.fetch() returns SourceItem list."""
|
|
mock_items = [
|
|
("Poetry line 1", "Poetry Source 1", ""),
|
|
("Poetry line 2", "Poetry Source 2", ""),
|
|
]
|
|
with patch("engine.fetch.fetch_poetry") as mock_fetch_poetry:
|
|
mock_fetch_poetry.return_value = (mock_items, 2, 0)
|
|
|
|
source = PoetryDataSource()
|
|
items = source.fetch()
|
|
|
|
assert len(items) == 2
|
|
assert all(isinstance(item, SourceItem) for item in items)
|
|
assert items[0].content == "Poetry line 1"
|
|
assert items[0].source == "Poetry Source 1"
|
|
|
|
def test_poetry_get_items_caches_result(self):
|
|
"""PoetryDataSource.get_items() caches result."""
|
|
mock_items = [("Poetry line", "Poetry Source", "")]
|
|
with patch("engine.fetch.fetch_poetry") as mock_fetch_poetry:
|
|
mock_fetch_poetry.return_value = (mock_items, 1, 0)
|
|
|
|
source = PoetryDataSource()
|
|
items1 = source.get_items()
|
|
items2 = source.get_items()
|
|
|
|
# Should only fetch once (cached)
|
|
assert mock_fetch_poetry.call_count == 1
|
|
assert items1 is items2
|
|
|
|
|
|
class TestDataSourceInterface:
|
|
"""Test DataSource base class interface."""
|
|
|
|
def test_data_source_stream_not_implemented(self):
|
|
"""DataSource.stream() raises NotImplementedError."""
|
|
source = EmptyDataSource()
|
|
with pytest.raises(NotImplementedError):
|
|
source.stream()
|
|
|
|
def test_data_source_is_dynamic_defaults_false(self):
|
|
"""DataSource.is_dynamic defaults to False."""
|
|
source = EmptyDataSource()
|
|
assert source.is_dynamic is False
|
|
|
|
def test_data_source_refresh_updates_cache(self):
|
|
"""DataSource.refresh() updates internal cache."""
|
|
source = EmptyDataSource()
|
|
source.get_items()
|
|
items_refreshed = source.refresh()
|
|
|
|
# refresh() should return new items
|
|
assert isinstance(items_refreshed, list)
|