test: add foundation for app.py integration tests (Phase 2 WIP)
Added initial integration test suite structure for engine/app.py covering: - main() entry point preset loading - run_pipeline_mode() pipeline setup - Content fetching for different sources - Display initialization - CLI flag overrides STATUS: Tests are currently failing because imports in run_pipeline_mode() are internal to the function, making them difficult to patch. This requires: 1. Refactoring imports to module level, OR 2. Using more sophisticated patching strategies (patch at import time) This provides a foundation for Phase 2. Tests will be fixed in next iteration. PHASE 2 TASKS: - Fix app.py test patching issues - Add data source tests (currently 34% coverage) - Expand adapter tests (currently 50% coverage) - Target: 70% coverage on critical paths
This commit is contained in:
286
tests/test_app.py
Normal file
286
tests/test_app.py
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
"""
|
||||||
|
Integration tests for engine/app.py - pipeline orchestration.
|
||||||
|
|
||||||
|
Tests the main entry point and pipeline mode initialization,
|
||||||
|
including preset loading, display creation, and stage setup.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from engine.app import main, run_pipeline_mode
|
||||||
|
from engine.display import DisplayRegistry, NullDisplay
|
||||||
|
from engine.pipeline import get_preset
|
||||||
|
|
||||||
|
|
||||||
|
class TestMain:
|
||||||
|
"""Test main() entry point."""
|
||||||
|
|
||||||
|
def test_main_calls_run_pipeline_mode_with_default_preset(self):
|
||||||
|
"""main() runs default preset (demo) when no args provided."""
|
||||||
|
with patch("engine.app.run_pipeline_mode") as mock_run:
|
||||||
|
sys.argv = ["mainline.py"]
|
||||||
|
main()
|
||||||
|
mock_run.assert_called_once_with("demo")
|
||||||
|
|
||||||
|
def test_main_uses_preset_from_config(self):
|
||||||
|
"""main() uses PRESET from config if set."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.config") as mock_config,
|
||||||
|
patch("engine.app.run_pipeline_mode") as mock_run,
|
||||||
|
):
|
||||||
|
mock_config.PRESET = "border-test"
|
||||||
|
sys.argv = ["mainline.py"]
|
||||||
|
main()
|
||||||
|
mock_run.assert_called_once_with("border-test")
|
||||||
|
|
||||||
|
def test_main_exits_on_unknown_preset(self):
|
||||||
|
"""main() exits with error message for unknown preset."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.run_pipeline_mode"),
|
||||||
|
patch("engine.app.list_presets", return_value=["demo", "poetry"]),
|
||||||
|
pytest.raises(SystemExit) as exc_info,
|
||||||
|
patch("engine.app.config") as mock_config,
|
||||||
|
):
|
||||||
|
sys.argv = ["mainline.py"]
|
||||||
|
mock_config.PRESET = "nonexistent"
|
||||||
|
main()
|
||||||
|
assert exc_info.value.code == 1
|
||||||
|
|
||||||
|
def test_main_handles_pipeline_diagram_flag(self):
|
||||||
|
"""main() generates pipeline diagram if PIPELINE_DIAGRAM is set."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.config") as mock_config,
|
||||||
|
patch(
|
||||||
|
"engine.app.generate_pipeline_diagram", return_value="diagram"
|
||||||
|
) as mock_gen,
|
||||||
|
):
|
||||||
|
mock_config.PIPELINE_DIAGRAM = True
|
||||||
|
with patch("builtins.print") as mock_print:
|
||||||
|
main()
|
||||||
|
mock_gen.assert_called_once()
|
||||||
|
mock_print.assert_called_with("diagram")
|
||||||
|
|
||||||
|
|
||||||
|
class TestRunPipelineMode:
|
||||||
|
"""Test run_pipeline_mode() pipeline setup and execution."""
|
||||||
|
|
||||||
|
def setup_method(self):
|
||||||
|
"""Setup for each test."""
|
||||||
|
DisplayRegistry._backends = {}
|
||||||
|
DisplayRegistry._initialized = False
|
||||||
|
DisplayRegistry.register("null", NullDisplay)
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_loads_preset(self):
|
||||||
|
"""run_pipeline_mode() loads the specified preset."""
|
||||||
|
preset = get_preset("demo")
|
||||||
|
assert preset is not None
|
||||||
|
assert preset.name == "demo"
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_exits_on_unknown_preset(self):
|
||||||
|
"""run_pipeline_mode() exits if preset not found."""
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
run_pipeline_mode("nonexistent-preset")
|
||||||
|
assert exc_info.value.code == 1
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_fetches_content_for_headlines(self):
|
||||||
|
"""run_pipeline_mode() fetches content for headlines preset."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.load_cache", return_value=None),
|
||||||
|
patch(
|
||||||
|
"engine.app.fetch_all", return_value=(["item1", "item2"], None, None)
|
||||||
|
),
|
||||||
|
patch("engine.app.DisplayRegistry.create") as mock_create,
|
||||||
|
patch("engine.app.effects_plugins"),
|
||||||
|
):
|
||||||
|
mock_display = MagicMock()
|
||||||
|
mock_create.return_value = mock_display
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("engine.app.Pipeline") as mock_pipeline_class,
|
||||||
|
patch("engine.app.time.sleep"),
|
||||||
|
pytest.raises((StopIteration, AttributeError)),
|
||||||
|
):
|
||||||
|
mock_pipeline = MagicMock()
|
||||||
|
mock_pipeline_class.return_value = mock_pipeline
|
||||||
|
# Will timeout after first iteration
|
||||||
|
run_pipeline_mode("demo")
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_handles_empty_source(self):
|
||||||
|
"""run_pipeline_mode() handles empty source without fetching."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.fetch_all") as mock_fetch,
|
||||||
|
patch("engine.app.DisplayRegistry.create") as mock_create,
|
||||||
|
patch("engine.app.effects_plugins"),
|
||||||
|
):
|
||||||
|
mock_display = MagicMock()
|
||||||
|
mock_create.return_value = mock_display
|
||||||
|
|
||||||
|
with patch("engine.app.Pipeline") as mock_pipeline_class:
|
||||||
|
mock_pipeline = MagicMock()
|
||||||
|
mock_pipeline_class.return_value = mock_pipeline
|
||||||
|
with patch("engine.app.time.sleep"):
|
||||||
|
try:
|
||||||
|
run_pipeline_mode("border-test")
|
||||||
|
except (StopIteration, AttributeError):
|
||||||
|
pass
|
||||||
|
# Should NOT call fetch_all for empty source
|
||||||
|
mock_fetch.assert_not_called()
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_uses_cached_content(self):
|
||||||
|
"""run_pipeline_mode() uses cached content if available."""
|
||||||
|
cached_items = ["cached1", "cached2"]
|
||||||
|
with (
|
||||||
|
patch("engine.app.load_cache", return_value=cached_items),
|
||||||
|
patch("engine.app.fetch_all") as mock_fetch,
|
||||||
|
patch("engine.app.DisplayRegistry.create") as mock_create,
|
||||||
|
patch("engine.app.effects_plugins"),
|
||||||
|
):
|
||||||
|
mock_display = MagicMock()
|
||||||
|
mock_create.return_value = mock_display
|
||||||
|
|
||||||
|
with patch("engine.app.Pipeline") as mock_pipeline_class:
|
||||||
|
mock_pipeline = MagicMock()
|
||||||
|
mock_pipeline_class.return_value = mock_pipeline
|
||||||
|
with patch("engine.app.time.sleep"):
|
||||||
|
try:
|
||||||
|
run_pipeline_mode("demo")
|
||||||
|
except (StopIteration, AttributeError):
|
||||||
|
pass
|
||||||
|
# Should NOT call fetch_all when cache exists
|
||||||
|
mock_fetch.assert_not_called()
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_fetches_poetry_for_poetry_preset(self):
|
||||||
|
"""run_pipeline_mode() fetches poetry for poetry source."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.load_cache", return_value=None),
|
||||||
|
patch("engine.app.fetch_poetry", return_value=(["poem1"], None, None)),
|
||||||
|
patch("engine.app.fetch_all") as mock_fetch_all,
|
||||||
|
patch("engine.app.DisplayRegistry.create") as mock_create,
|
||||||
|
patch("engine.app.effects_plugins"),
|
||||||
|
):
|
||||||
|
mock_display = MagicMock()
|
||||||
|
mock_create.return_value = mock_display
|
||||||
|
|
||||||
|
with patch("engine.app.Pipeline") as mock_pipeline_class:
|
||||||
|
mock_pipeline = MagicMock()
|
||||||
|
mock_pipeline_class.return_value = mock_pipeline
|
||||||
|
with patch("engine.app.time.sleep"):
|
||||||
|
try:
|
||||||
|
run_pipeline_mode("poetry")
|
||||||
|
except (StopIteration, AttributeError):
|
||||||
|
pass
|
||||||
|
# Should NOT call fetch_all for poetry preset
|
||||||
|
mock_fetch_all.assert_not_called()
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_exits_when_no_content(self):
|
||||||
|
"""run_pipeline_mode() exits if no content available."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.load_cache", return_value=None),
|
||||||
|
patch("engine.app.fetch_all", return_value=([], None, None)),
|
||||||
|
patch("engine.app.effects_plugins"),
|
||||||
|
):
|
||||||
|
with pytest.raises(SystemExit) as exc_info:
|
||||||
|
run_pipeline_mode("demo")
|
||||||
|
assert exc_info.value.code == 1
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_display_flag_overrides_preset(self):
|
||||||
|
"""run_pipeline_mode() uses CLI --display flag over preset."""
|
||||||
|
sys.argv = ["mainline.py", "--preset", "border-test", "--display", "null"]
|
||||||
|
with (
|
||||||
|
patch("engine.app.load_cache", return_value=None),
|
||||||
|
patch("engine.app.fetch_all", return_value=(["item"], None, None)),
|
||||||
|
patch("engine.app.DisplayRegistry.create") as mock_create,
|
||||||
|
patch("engine.app.effects_plugins"),
|
||||||
|
):
|
||||||
|
mock_display = MagicMock()
|
||||||
|
mock_create.return_value = mock_display
|
||||||
|
|
||||||
|
with patch("engine.app.Pipeline") as mock_pipeline_class:
|
||||||
|
mock_pipeline = MagicMock()
|
||||||
|
mock_pipeline_class.return_value = mock_pipeline
|
||||||
|
with patch("engine.app.time.sleep"):
|
||||||
|
try:
|
||||||
|
run_pipeline_mode("border-test")
|
||||||
|
except (StopIteration, AttributeError):
|
||||||
|
pass
|
||||||
|
# Should create display with null, not terminal
|
||||||
|
mock_create.assert_called_with("null")
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_discovers_effects_plugins(self):
|
||||||
|
"""run_pipeline_mode() discovers available effect plugins."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.effects_plugins") as mock_effects,
|
||||||
|
patch("engine.app.load_cache", return_value=["item"]),
|
||||||
|
patch("engine.app.DisplayRegistry.create") as mock_create,
|
||||||
|
patch("engine.app.Pipeline") as mock_pipeline_class,
|
||||||
|
):
|
||||||
|
mock_display = MagicMock()
|
||||||
|
mock_create.return_value = mock_display
|
||||||
|
mock_pipeline = MagicMock()
|
||||||
|
mock_pipeline_class.return_value = mock_pipeline
|
||||||
|
with patch("engine.app.time.sleep"):
|
||||||
|
try:
|
||||||
|
run_pipeline_mode("demo")
|
||||||
|
except (StopIteration, AttributeError):
|
||||||
|
pass
|
||||||
|
# Should call discover_plugins
|
||||||
|
mock_effects.discover_plugins.assert_called_once()
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_creates_pipeline_with_preset_config(self):
|
||||||
|
"""run_pipeline_mode() creates pipeline with preset configuration."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.load_cache", return_value=["item"]),
|
||||||
|
patch("engine.app.DisplayRegistry.create") as mock_create,
|
||||||
|
patch("engine.app.effects_plugins"),
|
||||||
|
patch("engine.app.Pipeline") as mock_pipeline_class,
|
||||||
|
):
|
||||||
|
mock_display = MagicMock()
|
||||||
|
mock_create.return_value = mock_display
|
||||||
|
mock_pipeline = MagicMock()
|
||||||
|
mock_pipeline_class.return_value = mock_pipeline
|
||||||
|
with patch("engine.app.time.sleep"):
|
||||||
|
try:
|
||||||
|
run_pipeline_mode("demo")
|
||||||
|
except (StopIteration, AttributeError):
|
||||||
|
pass
|
||||||
|
# Verify Pipeline was created (call may vary, but it should be called)
|
||||||
|
assert mock_pipeline_class.called
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_initializes_display(self):
|
||||||
|
"""run_pipeline_mode() initializes display with dimensions."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.load_cache", return_value=["item"]),
|
||||||
|
patch("engine.app.DisplayRegistry.create") as mock_create,
|
||||||
|
patch("engine.app.effects_plugins"),
|
||||||
|
patch("engine.app.Pipeline"),
|
||||||
|
):
|
||||||
|
mock_display = MagicMock()
|
||||||
|
mock_create.return_value = mock_display
|
||||||
|
with patch("engine.app.time.sleep"):
|
||||||
|
try:
|
||||||
|
run_pipeline_mode("demo")
|
||||||
|
except (StopIteration, AttributeError):
|
||||||
|
pass
|
||||||
|
# Display should be initialized with dimensions
|
||||||
|
mock_display.init.assert_called()
|
||||||
|
|
||||||
|
def test_run_pipeline_mode_builds_pipeline_before_initialize(self):
|
||||||
|
"""run_pipeline_mode() calls pipeline.build() before initialize()."""
|
||||||
|
with (
|
||||||
|
patch("engine.app.load_cache", return_value=["item"]),
|
||||||
|
patch("engine.app.DisplayRegistry.create"),
|
||||||
|
patch("engine.app.effects_plugins"),
|
||||||
|
patch("engine.app.Pipeline") as mock_pipeline_class,
|
||||||
|
):
|
||||||
|
mock_pipeline = MagicMock()
|
||||||
|
mock_pipeline_class.return_value = mock_pipeline
|
||||||
|
with patch("engine.app.time.sleep"):
|
||||||
|
try:
|
||||||
|
run_pipeline_mode("demo")
|
||||||
|
except (StopIteration, AttributeError):
|
||||||
|
pass
|
||||||
|
# Build should be called
|
||||||
|
assert mock_pipeline.build.called
|
||||||
Reference in New Issue
Block a user