forked from genewildish/Mainline
- Add Config dataclass with get_config()/set_config() for injection - Add Config.from_args() for CLI argument parsing (testable) - Add platform font path detection (Darwin/Linux) - Bound translate cache with @lru_cache(maxsize=500) - Add fixtures for external dependencies (network, feeds, config) - Add 15 tests for Config class, from_args, and platform detection This enables testability by: - Allowing config injection instead of global mutable state - Supporting custom argv in from_args() for testing - Providing reusable fixtures for mocking network/filesystem - Preventing unbounded memory growth in translation cache Fixes: _arg_value/_arg_int not accepting custom argv
237 lines
5.4 KiB
Python
237 lines
5.4 KiB
Python
"""
|
|
Pytest fixtures for mocking external dependencies (network, filesystem).
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_feed_response():
|
|
"""Mock RSS feed response data."""
|
|
return b"""<?xml version="1.0" encoding="UTF-8" ?>
|
|
<rss version="2.0">
|
|
<channel>
|
|
<title>Test Feed</title>
|
|
<link>https://example.com</link>
|
|
<item>
|
|
<title>Test Headline One</title>
|
|
<pubDate>Sat, 15 Mar 2025 12:00:00 GMT</pubDate>
|
|
</item>
|
|
<item>
|
|
<title>Test Headline Two</title>
|
|
<pubDate>Sat, 15 Mar 2025 11:00:00 GMT</pubDate>
|
|
</item>
|
|
<item>
|
|
<title>Sports: Team Wins Championship</title>
|
|
<pubDate>Sat, 15 Mar 2025 10:00:00 GMT</pubDate>
|
|
</item>
|
|
</channel>
|
|
</rss>"""
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_gutenberg_response():
|
|
"""Mock Project Gutenberg text response."""
|
|
return """Project Gutenberg's Collection, by Various
|
|
|
|
*** START OF SOME TEXT ***
|
|
This is a test poem with multiple lines
|
|
that should be parsed as stanzas.
|
|
|
|
Another stanza here with different content
|
|
and more lines to test the parsing logic.
|
|
|
|
Yet another stanza for variety
|
|
in the test data.
|
|
|
|
*** END OF SOME TEXT ***"""
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_gutenberg_empty():
|
|
"""Mock Gutenberg response with no valid stanzas."""
|
|
return """Project Gutenberg's Collection
|
|
|
|
*** START OF TEXT ***
|
|
THIS IS ALL CAPS AND SHOULD BE SKIPPED
|
|
|
|
I.
|
|
|
|
*** END OF TEXT ***"""
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_ntfy_message():
|
|
"""Mock ntfy.sh SSE message."""
|
|
return json.dumps(
|
|
{
|
|
"id": "test123",
|
|
"event": "message",
|
|
"title": "Test Title",
|
|
"message": "Test message body",
|
|
"time": 1234567890,
|
|
}
|
|
).encode()
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_ntfy_keepalive():
|
|
"""Mock ntfy.sh keepalive message."""
|
|
return b'data: {"event":"keepalive"}\n\n'
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_google_translate_response():
|
|
"""Mock Google Translate API response."""
|
|
return json.dumps(
|
|
[
|
|
[["Translated text", "Original text", None, 0.8], None, "en"],
|
|
None,
|
|
None,
|
|
[],
|
|
[],
|
|
[],
|
|
[],
|
|
]
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_feedparser():
|
|
"""Create a mock feedparser.parse function."""
|
|
|
|
def _mock(data):
|
|
mock_result = MagicMock()
|
|
mock_result.bozo = False
|
|
mock_result.entries = [
|
|
{
|
|
"title": "Test Headline",
|
|
"published_parsed": (2025, 3, 15, 12, 0, 0, 0, 0, 0),
|
|
},
|
|
{
|
|
"title": "Another Headline",
|
|
"updated_parsed": (2025, 3, 15, 11, 0, 0, 0, 0, 0),
|
|
},
|
|
]
|
|
return mock_result
|
|
|
|
return _mock
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_urllib_open(mock_feed_response):
|
|
"""Create a mock urllib.request.urlopen that returns feed data."""
|
|
|
|
def _mock(url):
|
|
mock_response = MagicMock()
|
|
mock_response.read.return_value = mock_feed_response
|
|
return mock_response
|
|
|
|
return _mock
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_items():
|
|
"""Sample items as returned by fetch module (title, source, timestamp)."""
|
|
return [
|
|
("Headline One", "Test Source", "12:00"),
|
|
("Headline Two", "Another Source", "11:30"),
|
|
("Headline Three", "Third Source", "10:45"),
|
|
]
|
|
|
|
|
|
@pytest.fixture
|
|
def sample_config():
|
|
"""Sample config for testing."""
|
|
from engine.config import Config
|
|
|
|
return Config(
|
|
headline_limit=100,
|
|
feed_timeout=10,
|
|
mic_threshold_db=50,
|
|
mode="news",
|
|
firehose=False,
|
|
ntfy_topic="https://ntfy.sh/test/json",
|
|
ntfy_reconnect_delay=5,
|
|
message_display_secs=30,
|
|
font_dir="fonts",
|
|
font_path="",
|
|
font_index=0,
|
|
font_picker=False,
|
|
font_sz=60,
|
|
render_h=8,
|
|
ssaa=4,
|
|
scroll_dur=5.625,
|
|
frame_dt=0.05,
|
|
firehose_h=12,
|
|
grad_speed=0.08,
|
|
glitch_glyphs="░▒▓█▌▐",
|
|
kata_glyphs="ハミヒーウ",
|
|
script_fonts={},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def poetry_config():
|
|
"""Sample config for poetry mode."""
|
|
from engine.config import Config
|
|
|
|
return Config(
|
|
headline_limit=100,
|
|
feed_timeout=10,
|
|
mic_threshold_db=50,
|
|
mode="poetry",
|
|
firehose=False,
|
|
ntfy_topic="https://ntfy.sh/test/json",
|
|
ntfy_reconnect_delay=5,
|
|
message_display_secs=30,
|
|
font_dir="fonts",
|
|
font_path="",
|
|
font_index=0,
|
|
font_picker=False,
|
|
font_sz=60,
|
|
render_h=8,
|
|
ssaa=4,
|
|
scroll_dur=5.625,
|
|
frame_dt=0.05,
|
|
firehose_h=12,
|
|
grad_speed=0.08,
|
|
glitch_glyphs="░▒▓█▌▐",
|
|
kata_glyphs="ハミヒーウ",
|
|
script_fonts={},
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def firehose_config():
|
|
"""Sample config with firehose enabled."""
|
|
from engine.config import Config
|
|
|
|
return Config(
|
|
headline_limit=100,
|
|
feed_timeout=10,
|
|
mic_threshold_db=50,
|
|
mode="news",
|
|
firehose=True,
|
|
ntfy_topic="https://ntfy.sh/test/json",
|
|
ntfy_reconnect_delay=5,
|
|
message_display_secs=30,
|
|
font_dir="fonts",
|
|
font_path="",
|
|
font_index=0,
|
|
font_picker=False,
|
|
font_sz=60,
|
|
render_h=8,
|
|
ssaa=4,
|
|
scroll_dur=5.625,
|
|
frame_dt=0.05,
|
|
firehose_h=12,
|
|
grad_speed=0.08,
|
|
glitch_glyphs="░▒▓█▌▐",
|
|
kata_glyphs="ハミヒーウ",
|
|
script_fonts={},
|
|
)
|