feat: modernize project with uv, add pytest test suite
- Add pyproject.toml with modern Python packaging (PEP 517/518) - Add uv-based dependency management replacing inline venv bootstrap - Add requirements.txt and requirements-dev.txt for compatibility - Add mise.toml with dev tasks (test, lint, run, sync, ci) - Add .python-version pinned to Python 3.12 - Add comprehensive pytest test suite (73 tests) for: - engine/config, filter, terminal, sources, mic, ntfy modules - Configure pytest with coverage reporting (16% total, 100% on tested modules) - Configure ruff for linting with Python 3.10+ target - Remove redundant venv bootstrap code from mainline.py - Update .gitignore for uv/venv artifacts Run 'uv sync' to install dependencies, 'uv run pytest' to test.
This commit is contained in:
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
162
tests/test_config.py
Normal file
162
tests/test_config.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""
|
||||
Tests for engine.config module.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
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
|
||||
93
tests/test_filter.py
Normal file
93
tests/test_filter.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Tests for engine.filter module.
|
||||
"""
|
||||
|
||||
from engine.filter import skip, strip_tags
|
||||
|
||||
|
||||
class TestStripTags:
|
||||
"""Tests for strip_tags function."""
|
||||
|
||||
def test_strips_simple_html(self):
|
||||
"""Basic HTML tags are removed."""
|
||||
assert strip_tags("<p>Hello</p>") == "Hello"
|
||||
assert strip_tags("<b>Bold</b>") == "Bold"
|
||||
assert strip_tags("<em>Italic</em>") == "Italic"
|
||||
|
||||
def test_strips_nested_html(self):
|
||||
"""Nested HTML tags are handled."""
|
||||
assert strip_tags("<div><p>Nested</p></div>") == "Nested"
|
||||
assert strip_tags("<span><strong>Deep</strong></span>") == "Deep"
|
||||
|
||||
def test_strips_html_with_attributes(self):
|
||||
"""HTML with attributes is handled."""
|
||||
assert strip_tags('<a href="http://example.com">Link</a>') == "Link"
|
||||
assert strip_tags('<img src="test.jpg" alt="test">') == ""
|
||||
|
||||
def test_handles_empty_string(self):
|
||||
"""Empty string returns empty string."""
|
||||
assert strip_tags("") == ""
|
||||
assert strip_tags(None) == ""
|
||||
|
||||
def test_handles_plain_text(self):
|
||||
"""Plain text without tags passes through."""
|
||||
assert strip_tags("Plain text") == "Plain text"
|
||||
|
||||
def test_unescapes_html_entities(self):
|
||||
"""HTML entities are decoded and tags are stripped."""
|
||||
assert strip_tags(" test") == "test"
|
||||
assert strip_tags("Hello & World") == "Hello & World"
|
||||
|
||||
def test_handles_malformed_html(self):
|
||||
"""Malformed HTML is handled gracefully."""
|
||||
assert strip_tags("<p>Unclosed") == "Unclosed"
|
||||
assert strip_tags("</p>No start") == "No start"
|
||||
|
||||
|
||||
class TestSkip:
|
||||
"""Tests for skip function - content filtering."""
|
||||
|
||||
def test_skips_sports_content(self):
|
||||
"""Sports-related headlines are skipped."""
|
||||
assert skip("Football: Team wins championship") is True
|
||||
assert skip("NBA Finals Game 7 results") is True
|
||||
assert skip("Soccer match ends in draw") is True
|
||||
assert skip("Premier League transfer news") is True
|
||||
assert skip("Super Bowl halftime show") is True
|
||||
|
||||
def test_skips_vapid_content(self):
|
||||
"""Vapid/celebrity content is skipped."""
|
||||
assert skip("Kim Kardashian's new look") is True
|
||||
assert skip("Influencer goes viral") is True
|
||||
assert skip("Red carpet best dressed") is True
|
||||
assert skip("Celebrity couple splits") is True
|
||||
|
||||
def test_allows_real_news(self):
|
||||
"""Legitimate news headlines are allowed."""
|
||||
assert skip("Scientists discover new planet") is False
|
||||
assert skip("Economy grows by 3%") is False
|
||||
assert skip("World leaders meet for summit") is False
|
||||
assert skip("New technology breakthrough") is False
|
||||
|
||||
def test_case_insensitive(self):
|
||||
"""Filter is case insensitive."""
|
||||
assert skip("FOOTBALL scores") is True
|
||||
assert skip("Football SCORES") is True
|
||||
assert skip("Kardashian") is True
|
||||
|
||||
def test_word_boundary_matching(self):
|
||||
"""Word boundary matching works correctly."""
|
||||
assert skip("The football stadium") is True
|
||||
assert skip("Footballer scores") is False
|
||||
assert skip("Footballs on sale") is False
|
||||
|
||||
|
||||
class TestIntegration:
|
||||
"""Integration tests combining filter functions."""
|
||||
|
||||
def test_full_pipeline(self):
|
||||
"""Test strip_tags followed by skip."""
|
||||
html = '<p><a href="#">Breaking: Football championship final</a></p>'
|
||||
text = strip_tags(html)
|
||||
assert text == "Breaking: Football championship final"
|
||||
assert skip(text) is True
|
||||
83
tests/test_mic.py
Normal file
83
tests/test_mic.py
Normal file
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
Tests for engine.mic module.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestMicMonitorImport:
|
||||
"""Tests for module import behavior."""
|
||||
|
||||
def test_mic_monitor_imports_without_error(self):
|
||||
"""MicMonitor can be imported even without sounddevice."""
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
assert MicMonitor is not None
|
||||
|
||||
|
||||
class TestMicMonitorInit:
|
||||
"""Tests for MicMonitor initialization."""
|
||||
|
||||
def test_init_sets_threshold(self):
|
||||
"""Threshold is set correctly."""
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
monitor = MicMonitor(threshold_db=60)
|
||||
assert monitor.threshold_db == 60
|
||||
|
||||
def test_init_defaults(self):
|
||||
"""Default values are set correctly."""
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
monitor = MicMonitor()
|
||||
assert monitor.threshold_db == 50
|
||||
|
||||
def test_init_db_starts_at_negative(self):
|
||||
"""_db starts at negative value."""
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
monitor = MicMonitor()
|
||||
assert monitor.db == -99.0
|
||||
|
||||
|
||||
class TestMicMonitorProperties:
|
||||
"""Tests for MicMonitor properties."""
|
||||
|
||||
def test_excess_returns_positive_when_above_threshold(self):
|
||||
"""excess returns positive value when above threshold."""
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
monitor = MicMonitor(threshold_db=50)
|
||||
with patch.object(monitor, "_db", 60.0):
|
||||
assert monitor.excess == 10.0
|
||||
|
||||
def test_excess_returns_zero_when_below_threshold(self):
|
||||
"""excess returns zero when below threshold."""
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
monitor = MicMonitor(threshold_db=50)
|
||||
with patch.object(monitor, "_db", 40.0):
|
||||
assert monitor.excess == 0.0
|
||||
|
||||
|
||||
class TestMicMonitorAvailable:
|
||||
"""Tests for MicMonitor.available property."""
|
||||
|
||||
def test_available_is_bool(self):
|
||||
"""available returns a boolean."""
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
monitor = MicMonitor()
|
||||
assert isinstance(monitor.available, bool)
|
||||
|
||||
|
||||
class TestMicMonitorStop:
|
||||
"""Tests for MicMonitor.stop method."""
|
||||
|
||||
def test_stop_does_nothing_when_no_stream(self):
|
||||
"""stop() does nothing if no stream exists."""
|
||||
from engine.mic import MicMonitor
|
||||
|
||||
monitor = MicMonitor()
|
||||
monitor.stop()
|
||||
assert monitor._stream is None
|
||||
70
tests/test_ntfy.py
Normal file
70
tests/test_ntfy.py
Normal file
@@ -0,0 +1,70 @@
|
||||
"""
|
||||
Tests for engine.ntfy module.
|
||||
"""
|
||||
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from engine.ntfy import NtfyPoller
|
||||
|
||||
|
||||
class TestNtfyPollerInit:
|
||||
"""Tests for NtfyPoller initialization."""
|
||||
|
||||
def test_init_sets_defaults(self):
|
||||
"""Default values are set correctly."""
|
||||
poller = NtfyPoller("http://example.com/topic")
|
||||
assert poller.topic_url == "http://example.com/topic"
|
||||
assert poller.poll_interval == 15
|
||||
assert poller.display_secs == 30
|
||||
|
||||
def test_init_custom_values(self):
|
||||
"""Custom values are set correctly."""
|
||||
poller = NtfyPoller(
|
||||
"http://example.com/topic", poll_interval=5, display_secs=60
|
||||
)
|
||||
assert poller.poll_interval == 5
|
||||
assert poller.display_secs == 60
|
||||
|
||||
|
||||
class TestNtfyPollerStart:
|
||||
"""Tests for NtfyPoller.start method."""
|
||||
|
||||
@patch("engine.ntfy.threading.Thread")
|
||||
def test_start_creates_daemon_thread(self, mock_thread):
|
||||
"""start() creates and starts a daemon thread."""
|
||||
mock_thread_instance = MagicMock()
|
||||
mock_thread.return_value = mock_thread_instance
|
||||
|
||||
poller = NtfyPoller("http://example.com/topic")
|
||||
result = poller.start()
|
||||
|
||||
assert result is True
|
||||
mock_thread.assert_called_once()
|
||||
args, kwargs = mock_thread.call_args
|
||||
assert kwargs.get("daemon") is True
|
||||
mock_thread_instance.start.assert_called_once()
|
||||
|
||||
|
||||
class TestNtfyPollerGetActiveMessage:
|
||||
"""Tests for NtfyPoller.get_active_message method."""
|
||||
|
||||
def test_returns_none_when_no_message(self):
|
||||
"""Returns None when no message has been received."""
|
||||
poller = NtfyPoller("http://example.com/topic")
|
||||
result = poller.get_active_message()
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestNtfyPollerDismiss:
|
||||
"""Tests for NtfyPoller.dismiss method."""
|
||||
|
||||
def test_dismiss_clears_message(self):
|
||||
"""dismiss() clears the current message."""
|
||||
poller = NtfyPoller("http://example.com/topic")
|
||||
|
||||
with patch.object(poller, "_lock"):
|
||||
poller._message = ("Title", "Body", time.monotonic())
|
||||
poller.dismiss()
|
||||
|
||||
assert poller._message is None
|
||||
93
tests/test_sources.py
Normal file
93
tests/test_sources.py
Normal file
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
Tests for engine.sources module - data validation.
|
||||
"""
|
||||
|
||||
from engine import sources
|
||||
|
||||
|
||||
class TestFeeds:
|
||||
"""Tests for FEEDS data."""
|
||||
|
||||
def test_feeds_is_dict(self):
|
||||
"""FEEDS is a dictionary."""
|
||||
assert isinstance(sources.FEEDS, dict)
|
||||
|
||||
def test_feeds_has_entries(self):
|
||||
"""FEEDS has feed entries."""
|
||||
assert len(sources.FEEDS) > 0
|
||||
|
||||
def test_feeds_have_valid_urls(self):
|
||||
"""All feeds have valid URL format."""
|
||||
for name, url in sources.FEEDS.items():
|
||||
assert name
|
||||
assert url.startswith("http://") or url.startswith("https://")
|
||||
|
||||
|
||||
class TestPoetrySources:
|
||||
"""Tests for POETRY_SOURCES data."""
|
||||
|
||||
def test_poetry_is_dict(self):
|
||||
"""POETRY_SOURCES is a dictionary."""
|
||||
assert isinstance(sources.POETRY_SOURCES, dict)
|
||||
|
||||
def test_poetry_has_entries(self):
|
||||
"""POETRY_SOURCES has entries."""
|
||||
assert len(sources.POETRY_SOURCES) > 0
|
||||
|
||||
def test_poetry_have_gutenberg_urls(self):
|
||||
"""All poetry sources are from Gutenberg."""
|
||||
for _name, url in sources.POETRY_SOURCES.items():
|
||||
assert "gutenberg.org" in url
|
||||
|
||||
|
||||
class TestSourceLangs:
|
||||
"""Tests for SOURCE_LANGS mapping."""
|
||||
|
||||
def test_source_langs_is_dict(self):
|
||||
"""SOURCE_LANGS is a dictionary."""
|
||||
assert isinstance(sources.SOURCE_LANGS, dict)
|
||||
|
||||
def test_source_langs_valid_language_codes(self):
|
||||
"""Language codes are valid ISO codes."""
|
||||
valid_codes = {"de", "fr", "ja", "zh-cn", "ar", "hi"}
|
||||
for code in sources.SOURCE_LANGS.values():
|
||||
assert code in valid_codes
|
||||
|
||||
|
||||
class TestLocationLangs:
|
||||
"""Tests for LOCATION_LANGS mapping."""
|
||||
|
||||
def test_location_langs_is_dict(self):
|
||||
"""LOCATION_LANGS is a dictionary."""
|
||||
assert isinstance(sources.LOCATION_LANGS, dict)
|
||||
|
||||
def test_location_langs_has_patterns(self):
|
||||
"""LOCATION_LANGS has regex patterns."""
|
||||
assert len(sources.LOCATION_LANGS) > 0
|
||||
|
||||
|
||||
class TestScriptFonts:
|
||||
"""Tests for SCRIPT_FONTS mapping."""
|
||||
|
||||
def test_script_fonts_is_dict(self):
|
||||
"""SCRIPT_FONTS is a dictionary."""
|
||||
assert isinstance(sources.SCRIPT_FONTS, dict)
|
||||
|
||||
def test_script_fonts_has_paths(self):
|
||||
"""All script fonts have paths."""
|
||||
for _lang, path in sources.SCRIPT_FONTS.items():
|
||||
assert path
|
||||
|
||||
|
||||
class TestNoUpper:
|
||||
"""Tests for NO_UPPER set."""
|
||||
|
||||
def test_no_upper_is_set(self):
|
||||
"""NO_UPPER is a set."""
|
||||
assert isinstance(sources.NO_UPPER, set)
|
||||
|
||||
def test_no_upper_contains_scripts(self):
|
||||
"""NO_UPPER contains non-Latin scripts."""
|
||||
assert "zh-cn" in sources.NO_UPPER
|
||||
assert "ja" in sources.NO_UPPER
|
||||
assert "ar" in sources.NO_UPPER
|
||||
124
tests/test_terminal.py
Normal file
124
tests/test_terminal.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""
|
||||
Tests for engine.terminal module.
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
from unittest.mock import patch
|
||||
|
||||
from engine import terminal
|
||||
|
||||
|
||||
class TestTerminalDimensions:
|
||||
"""Tests for terminal width/height functions."""
|
||||
|
||||
def test_tw_returns_columns(self):
|
||||
"""tw() returns terminal width."""
|
||||
with patch.object(sys.stdout, "isatty", return_value=True):
|
||||
with patch("os.get_terminal_size") as mock_size:
|
||||
mock_size.return_value = io.StringIO("columns=120")
|
||||
mock_size.columns = 120
|
||||
result = terminal.tw()
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_th_returns_lines(self):
|
||||
"""th() returns terminal height."""
|
||||
with patch.object(sys.stdout, "isatty", return_value=True):
|
||||
with patch("os.get_terminal_size") as mock_size:
|
||||
mock_size.return_value = io.StringIO("lines=30")
|
||||
mock_size.lines = 30
|
||||
result = terminal.th()
|
||||
assert isinstance(result, int)
|
||||
|
||||
def test_tw_fallback_on_error(self):
|
||||
"""tw() falls back to 80 on error."""
|
||||
with patch("os.get_terminal_size", side_effect=OSError):
|
||||
result = terminal.tw()
|
||||
assert result == 80
|
||||
|
||||
def test_th_fallback_on_error(self):
|
||||
"""th() falls back to 24 on error."""
|
||||
with patch("os.get_terminal_size", side_effect=OSError):
|
||||
result = terminal.th()
|
||||
assert result == 24
|
||||
|
||||
|
||||
class TestANSICodes:
|
||||
"""Tests for ANSI escape code constants."""
|
||||
|
||||
def test_ansi_constants_exist(self):
|
||||
"""All ANSI constants are defined."""
|
||||
assert terminal.RST == "\033[0m"
|
||||
assert terminal.BOLD == "\033[1m"
|
||||
assert terminal.DIM == "\033[2m"
|
||||
|
||||
def test_green_shades_defined(self):
|
||||
"""Green gradient colors are defined."""
|
||||
assert terminal.G_HI == "\033[38;5;46m"
|
||||
assert terminal.G_MID == "\033[38;5;34m"
|
||||
assert terminal.G_LO == "\033[38;5;22m"
|
||||
|
||||
def test_white_shades_defined(self):
|
||||
"""White/gray tones are defined."""
|
||||
assert terminal.W_COOL == "\033[38;5;250m"
|
||||
assert terminal.W_DIM == "\033[2;38;5;245m"
|
||||
|
||||
def test_cursor_controls_defined(self):
|
||||
"""Cursor control codes are defined."""
|
||||
assert "?" in terminal.CURSOR_OFF
|
||||
assert "?" in terminal.CURSOR_ON
|
||||
|
||||
|
||||
class TestTypeOut:
|
||||
"""Tests for type_out function."""
|
||||
|
||||
@patch("sys.stdout", new_callable=io.StringIO)
|
||||
@patch("time.sleep")
|
||||
def test_type_out_writes_text(self, mock_sleep, mock_stdout):
|
||||
"""type_out writes text to stdout."""
|
||||
with patch("random.random", return_value=0.5):
|
||||
terminal.type_out("Hi", color=terminal.G_HI)
|
||||
output = mock_stdout.getvalue()
|
||||
assert len(output) > 0
|
||||
|
||||
@patch("time.sleep")
|
||||
def test_type_out_uses_color(self, mock_sleep):
|
||||
"""type_out applies color codes."""
|
||||
with patch("sys.stdout", new_callable=io.StringIO):
|
||||
with patch("random.random", return_value=0.5):
|
||||
terminal.type_out("Test", color=terminal.G_HI)
|
||||
|
||||
|
||||
class TestSlowPrint:
|
||||
"""Tests for slow_print function."""
|
||||
|
||||
@patch("sys.stdout", new_callable=io.StringIO)
|
||||
@patch("time.sleep")
|
||||
def test_slow_print_writes_text(self, mock_sleep, mock_stdout):
|
||||
"""slow_print writes text to stdout."""
|
||||
terminal.slow_print("Hi", color=terminal.G_DIM, delay=0)
|
||||
output = mock_stdout.getvalue()
|
||||
assert len(output) > 0
|
||||
|
||||
|
||||
class TestBootLn:
|
||||
"""Tests for boot_ln function."""
|
||||
|
||||
@patch("sys.stdout", new_callable=io.StringIO)
|
||||
@patch("time.sleep")
|
||||
def test_boot_ln_writes_label_and_status(self, mock_sleep, mock_stdout):
|
||||
"""boot_ln shows label and status."""
|
||||
with patch("random.uniform", return_value=0):
|
||||
terminal.boot_ln("Loading", "OK", ok=True)
|
||||
output = mock_stdout.getvalue()
|
||||
assert "Loading" in output
|
||||
assert "OK" in output
|
||||
|
||||
@patch("sys.stdout", new_callable=io.StringIO)
|
||||
@patch("time.sleep")
|
||||
def test_boot_ln_error_status(self, mock_sleep, mock_stdout):
|
||||
"""boot_ln shows red for error status."""
|
||||
with patch("random.uniform", return_value=0):
|
||||
terminal.boot_ln("Loading", "FAIL", ok=False)
|
||||
output = mock_stdout.getvalue()
|
||||
assert "FAIL" in output
|
||||
Reference in New Issue
Block a user