- 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
302 lines
10 KiB
Python
302 lines
10 KiB
Python
"""
|
|
Tests for engine.config module.
|
|
"""
|
|
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from engine import config
|
|
|
|
|
|
class TestArgValue:
|
|
"""Tests for _arg_value helper."""
|
|
|
|
def test_returns_value_when_flag_present(self):
|
|
"""Returns the value following the flag."""
|
|
with patch.object(sys, "argv", ["prog", "--font-file", "test.otf"]):
|
|
result = config._arg_value("--font-file")
|
|
assert result == "test.otf"
|
|
|
|
def test_returns_none_when_flag_missing(self):
|
|
"""Returns None when flag is not present."""
|
|
with patch.object(sys, "argv", ["prog"]):
|
|
result = config._arg_value("--font-file")
|
|
assert result is None
|
|
|
|
def test_returns_none_when_no_value(self):
|
|
"""Returns None when flag is last."""
|
|
with patch.object(sys, "argv", ["prog", "--font-file"]):
|
|
result = config._arg_value("--font-file")
|
|
assert result is None
|
|
|
|
|
|
class TestArgInt:
|
|
"""Tests for _arg_int helper."""
|
|
|
|
def test_parses_valid_int(self):
|
|
"""Parses valid integer."""
|
|
with patch.object(sys, "argv", ["prog", "--font-index", "5"]):
|
|
result = config._arg_int("--font-index", 0)
|
|
assert result == 5
|
|
|
|
def test_returns_default_on_invalid(self):
|
|
"""Returns default on invalid input."""
|
|
with patch.object(sys, "argv", ["prog", "--font-index", "abc"]):
|
|
result = config._arg_int("--font-index", 0)
|
|
assert result == 0
|
|
|
|
def test_returns_default_when_missing(self):
|
|
"""Returns default when flag missing."""
|
|
with patch.object(sys, "argv", ["prog"]):
|
|
result = config._arg_int("--font-index", 10)
|
|
assert result == 10
|
|
|
|
|
|
class TestResolveFontPath:
|
|
"""Tests for _resolve_font_path helper."""
|
|
|
|
def test_returns_absolute_paths(self):
|
|
"""Absolute paths are returned as-is."""
|
|
result = config._resolve_font_path("/absolute/path.otf")
|
|
assert result == "/absolute/path.otf"
|
|
|
|
def test_resolves_relative_paths(self):
|
|
"""Relative paths are resolved to repo root."""
|
|
result = config._resolve_font_path("fonts/test.otf")
|
|
assert str(config._REPO_ROOT) in result
|
|
|
|
def test_expands_user_home(self):
|
|
"""Tilde paths are expanded."""
|
|
with patch("pathlib.Path.expanduser", return_value=Path("/home/user/fonts")):
|
|
result = config._resolve_font_path("~/fonts/test.otf")
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestListFontFiles:
|
|
"""Tests for _list_font_files helper."""
|
|
|
|
def test_returns_empty_for_missing_dir(self):
|
|
"""Returns empty list for missing directory."""
|
|
result = config._list_font_files("/nonexistent/directory")
|
|
assert result == []
|
|
|
|
def test_filters_by_extension(self):
|
|
"""Only returns valid font extensions."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
Path(tmpdir, "valid.otf").touch()
|
|
Path(tmpdir, "valid.ttf").touch()
|
|
Path(tmpdir, "invalid.txt").touch()
|
|
Path(tmpdir, "image.png").touch()
|
|
|
|
result = config._list_font_files(tmpdir)
|
|
assert len(result) == 2
|
|
assert all(f.endswith((".otf", ".ttf")) for f in result)
|
|
|
|
def test_sorts_alphabetically(self):
|
|
"""Results are sorted alphabetically."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
Path(tmpdir, "zfont.otf").touch()
|
|
Path(tmpdir, "afont.otf").touch()
|
|
|
|
result = config._list_font_files(tmpdir)
|
|
filenames = [Path(f).name for f in result]
|
|
assert filenames == ["afont.otf", "zfont.otf"]
|
|
|
|
|
|
class TestDefaults:
|
|
"""Tests for default configuration values."""
|
|
|
|
def test_headline_limit(self):
|
|
"""HEADLINE_LIMIT has sensible default."""
|
|
assert config.HEADLINE_LIMIT > 0
|
|
|
|
def test_feed_timeout(self):
|
|
"""FEED_TIMEOUT has sensible default."""
|
|
assert config.FEED_TIMEOUT > 0
|
|
|
|
def test_font_extensions(self):
|
|
"""Font extensions are defined."""
|
|
assert ".otf" in config._FONT_EXTENSIONS
|
|
assert ".ttf" in config._FONT_EXTENSIONS
|
|
assert ".ttc" in config._FONT_EXTENSIONS
|
|
|
|
|
|
class TestGlyphs:
|
|
"""Tests for glyph constants."""
|
|
|
|
def test_glitch_glyphs_defined(self):
|
|
"""GLITCH glyphs are defined."""
|
|
assert len(config.GLITCH) > 0
|
|
|
|
def test_kata_glyphs_defined(self):
|
|
"""KATA glyphs are defined."""
|
|
assert len(config.KATA) > 0
|
|
|
|
|
|
class TestSetFontSelection:
|
|
"""Tests for set_font_selection function."""
|
|
|
|
def test_updates_font_path(self):
|
|
"""Updates FONT_PATH globally."""
|
|
original = config.FONT_PATH
|
|
config.set_font_selection(font_path="/new/path.otf")
|
|
assert config.FONT_PATH == "/new/path.otf"
|
|
config.FONT_PATH = original
|
|
|
|
def test_updates_font_index(self):
|
|
"""Updates FONT_INDEX globally."""
|
|
original = config.FONT_INDEX
|
|
config.set_font_selection(font_index=5)
|
|
assert config.FONT_INDEX == 5
|
|
config.FONT_INDEX = original
|
|
|
|
def test_handles_none_values(self):
|
|
"""Handles None values gracefully."""
|
|
original_path = config.FONT_PATH
|
|
original_index = config.FONT_INDEX
|
|
|
|
config.set_font_selection(font_path=None, font_index=None)
|
|
assert original_path == config.FONT_PATH
|
|
assert original_index == config.FONT_INDEX
|
|
|
|
|
|
class TestConfigDataclass:
|
|
"""Tests for Config dataclass."""
|
|
|
|
def test_config_has_required_fields(self):
|
|
"""Config has all required fields."""
|
|
c = config.Config()
|
|
assert hasattr(c, "headline_limit")
|
|
assert hasattr(c, "feed_timeout")
|
|
assert hasattr(c, "mic_threshold_db")
|
|
assert hasattr(c, "mode")
|
|
assert hasattr(c, "firehose")
|
|
assert hasattr(c, "ntfy_topic")
|
|
assert hasattr(c, "ntfy_reconnect_delay")
|
|
assert hasattr(c, "message_display_secs")
|
|
assert hasattr(c, "font_dir")
|
|
assert hasattr(c, "font_path")
|
|
assert hasattr(c, "font_index")
|
|
assert hasattr(c, "font_picker")
|
|
assert hasattr(c, "font_sz")
|
|
assert hasattr(c, "render_h")
|
|
assert hasattr(c, "ssaa")
|
|
assert hasattr(c, "scroll_dur")
|
|
assert hasattr(c, "frame_dt")
|
|
assert hasattr(c, "firehose_h")
|
|
assert hasattr(c, "grad_speed")
|
|
assert hasattr(c, "glitch_glyphs")
|
|
assert hasattr(c, "kata_glyphs")
|
|
assert hasattr(c, "script_fonts")
|
|
|
|
def test_config_defaults(self):
|
|
"""Config has sensible defaults."""
|
|
c = config.Config()
|
|
assert c.headline_limit == 1000
|
|
assert c.feed_timeout == 10
|
|
assert c.mic_threshold_db == 50
|
|
assert c.mode == "news"
|
|
assert c.firehose is False
|
|
assert c.ntfy_reconnect_delay == 5
|
|
assert c.message_display_secs == 30
|
|
|
|
def test_config_is_immutable(self):
|
|
"""Config is frozen (immutable)."""
|
|
c = config.Config()
|
|
with pytest.raises(AttributeError):
|
|
c.headline_limit = 500 # type: ignore
|
|
|
|
def test_config_custom_values(self):
|
|
"""Config accepts custom values."""
|
|
c = config.Config(
|
|
headline_limit=500,
|
|
mode="poetry",
|
|
firehose=True,
|
|
ntfy_topic="https://ntfy.sh/test",
|
|
)
|
|
assert c.headline_limit == 500
|
|
assert c.mode == "poetry"
|
|
assert c.firehose is True
|
|
assert c.ntfy_topic == "https://ntfy.sh/test"
|
|
|
|
|
|
class TestConfigFromArgs:
|
|
"""Tests for Config.from_args method."""
|
|
|
|
def test_from_args_defaults(self):
|
|
"""from_args creates config with defaults from empty argv."""
|
|
c = config.Config.from_args(["prog"])
|
|
assert c.mode == "news"
|
|
assert c.firehose is False
|
|
assert c.font_picker is True
|
|
|
|
def test_from_args_poetry_mode(self):
|
|
"""from_args detects --poetry flag."""
|
|
c = config.Config.from_args(["prog", "--poetry"])
|
|
assert c.mode == "poetry"
|
|
|
|
def test_from_args_poetry_short_flag(self):
|
|
"""from_args detects -p short flag."""
|
|
c = config.Config.from_args(["prog", "-p"])
|
|
assert c.mode == "poetry"
|
|
|
|
def test_from_args_firehose(self):
|
|
"""from_args detects --firehose flag."""
|
|
c = config.Config.from_args(["prog", "--firehose"])
|
|
assert c.firehose is True
|
|
|
|
def test_from_args_no_font_picker(self):
|
|
"""from_args detects --no-font-picker flag."""
|
|
c = config.Config.from_args(["prog", "--no-font-picker"])
|
|
assert c.font_picker is False
|
|
|
|
def test_from_args_font_index(self):
|
|
"""from_args parses --font-index."""
|
|
c = config.Config.from_args(["prog", "--font-index", "3"])
|
|
assert c.font_index == 3
|
|
|
|
|
|
class TestGetSetConfig:
|
|
"""Tests for get_config and set_config functions."""
|
|
|
|
def test_get_config_returns_config(self):
|
|
"""get_config returns a Config instance."""
|
|
c = config.get_config()
|
|
assert isinstance(c, config.Config)
|
|
|
|
def test_set_config_allows_injection(self):
|
|
"""set_config allows injecting a custom config."""
|
|
custom = config.Config(mode="poetry", headline_limit=100)
|
|
config.set_config(custom)
|
|
assert config.get_config().mode == "poetry"
|
|
assert config.get_config().headline_limit == 100
|
|
|
|
def test_set_config_then_get_config(self):
|
|
"""set_config followed by get_config returns the set config."""
|
|
original = config.get_config()
|
|
test_config = config.Config(headline_limit=42)
|
|
config.set_config(test_config)
|
|
result = config.get_config()
|
|
assert result.headline_limit == 42
|
|
config.set_config(original)
|
|
|
|
|
|
class TestPlatformFontPaths:
|
|
"""Tests for platform font path detection."""
|
|
|
|
def test_get_platform_font_paths_returns_dict(self):
|
|
"""_get_platform_font_paths returns a dictionary."""
|
|
fonts = config._get_platform_font_paths()
|
|
assert isinstance(fonts, dict)
|
|
|
|
def test_platform_font_paths_common_languages(self):
|
|
"""Common language font mappings exist."""
|
|
fonts = config._get_platform_font_paths()
|
|
common = {"ja", "zh-cn", "ko", "ru", "ar", "hi"}
|
|
found = set(fonts.keys()) & common
|
|
assert len(found) > 0
|