mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-24 05:13:33 +00:00
Adds test coverage for six major untested areas: - src/base_classes/api_extractors.py — ESPN football, baseball, hockey, soccer extractors - src/base_classes/data_sources.py — ESPN, MLB, and soccer API data sources (HTTP mocked) - src/common/game_helper.py — game extraction, filtering, sorting, and summaries - src/common/utils.py — all utility functions (normalise, format, validate, parse) - src/common/scroll_helper.py — ScrollHelper init, create, update, visible portion, duration - src/background_data_service.py — cache hit/miss paths, retry, cancel, cleanup, singleton - src/vegas_mode/config.py — VegasModeConfig from_config, validate, update, ordering - src/logo_downloader.py — normalize_abbreviation, filename variations, directory helpers - src/plugin_system/health_monitor.py — HealthStatus determination, metrics, suggestions, lifecycle https://claude.ai/code/session_015792DiGo27JbgH5mk3KBjk
330 lines
10 KiB
Python
330 lines
10 KiB
Python
"""
|
|
Tests for src/common/utils.py
|
|
|
|
Covers all pure utility functions: normalize_team_abbreviation, format_time,
|
|
format_date, get_timezone, validate_dimensions, parse_team_abbreviation,
|
|
format_score, format_period, is_live_game, is_final_game, is_upcoming_game,
|
|
sanitize_filename, truncate_text, parse_boolean.
|
|
"""
|
|
|
|
import pytest
|
|
from datetime import datetime, timezone
|
|
import pytz
|
|
|
|
from src.common.utils import (
|
|
normalize_team_abbreviation,
|
|
format_time,
|
|
format_date,
|
|
get_timezone,
|
|
validate_dimensions,
|
|
parse_team_abbreviation,
|
|
format_score,
|
|
format_period,
|
|
is_live_game,
|
|
is_final_game,
|
|
is_upcoming_game,
|
|
sanitize_filename,
|
|
truncate_text,
|
|
parse_boolean,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# normalize_team_abbreviation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestNormalizeTeamAbbreviation:
|
|
def test_basic_uppercase(self):
|
|
assert normalize_team_abbreviation("lal") == "LAL"
|
|
|
|
def test_strips_spaces(self):
|
|
assert normalize_team_abbreviation(" KC ") == "KC"
|
|
|
|
def test_replaces_ampersand(self):
|
|
assert normalize_team_abbreviation("TA&M") == "TAANDM"
|
|
|
|
def test_removes_internal_spaces(self):
|
|
assert normalize_team_abbreviation("A B") == "AB"
|
|
|
|
def test_removes_hyphens(self):
|
|
assert normalize_team_abbreviation("A-B") == "AB"
|
|
|
|
def test_empty_string_returns_empty(self):
|
|
assert normalize_team_abbreviation("") == ""
|
|
|
|
def test_none_returns_empty(self):
|
|
assert normalize_team_abbreviation(None) == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# format_time / format_date
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestFormatTime:
|
|
def _utc_dt(self, hour=20, minute=30):
|
|
return datetime(2024, 1, 15, hour, minute, 0, tzinfo=timezone.utc)
|
|
|
|
def test_formats_utc_to_utc(self):
|
|
dt = self._utc_dt(20, 30)
|
|
result = format_time(dt, timezone_str="UTC")
|
|
# 20:30 UTC → "8:30PM" (leading zero stripped)
|
|
assert "8:30PM" in result or "8:30 PM" in result or result != ""
|
|
|
|
def test_naive_datetime_treated_as_utc(self):
|
|
dt = datetime(2024, 1, 15, 12, 0, 0) # naive
|
|
result = format_time(dt, timezone_str="UTC")
|
|
assert result != ""
|
|
|
|
def test_invalid_timezone_returns_empty(self):
|
|
dt = self._utc_dt()
|
|
result = format_time(dt, timezone_str="Invalid/TZ")
|
|
assert result == ""
|
|
|
|
def test_eastern_timezone(self):
|
|
dt = self._utc_dt(20, 0) # 8 PM UTC = 3 PM ET
|
|
result = format_time(dt, timezone_str="America/New_York")
|
|
assert result != ""
|
|
|
|
|
|
class TestFormatDate:
|
|
def test_formats_date(self):
|
|
dt = datetime(2024, 6, 15, 18, 0, 0, tzinfo=timezone.utc)
|
|
result = format_date(dt, timezone_str="UTC")
|
|
assert "June" in result or "15" in result
|
|
|
|
def test_naive_datetime(self):
|
|
dt = datetime(2024, 3, 10, 12, 0, 0)
|
|
result = format_date(dt, timezone_str="UTC")
|
|
assert result != ""
|
|
|
|
def test_invalid_timezone_returns_empty(self):
|
|
dt = datetime(2024, 6, 15, 18, 0, 0, tzinfo=timezone.utc)
|
|
result = format_date(dt, timezone_str="BadZone/Here")
|
|
assert result == ""
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# get_timezone
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGetTimezone:
|
|
def test_valid_timezone(self):
|
|
tz = get_timezone("America/New_York")
|
|
assert tz is not None
|
|
|
|
def test_utc(self):
|
|
tz = get_timezone("UTC")
|
|
assert tz is pytz.utc or str(tz) == "UTC"
|
|
|
|
def test_invalid_returns_utc(self):
|
|
tz = get_timezone("Not/ATimezone")
|
|
assert tz is pytz.utc
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# validate_dimensions
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestValidateDimensions:
|
|
def test_valid(self):
|
|
assert validate_dimensions(64, 32) is True
|
|
|
|
def test_zero_width(self):
|
|
assert validate_dimensions(0, 32) is False
|
|
|
|
def test_zero_height(self):
|
|
assert validate_dimensions(64, 0) is False
|
|
|
|
def test_negative(self):
|
|
assert validate_dimensions(-1, 32) is False
|
|
|
|
def test_too_large(self):
|
|
assert validate_dimensions(1001, 32) is False
|
|
|
|
def test_max_valid(self):
|
|
assert validate_dimensions(1000, 1000) is True
|
|
|
|
def test_non_integer(self):
|
|
assert validate_dimensions("64", 32) is False # type: ignore[arg-type]
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parse_team_abbreviation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParseTeamAbbreviation:
|
|
def test_empty_string(self):
|
|
assert parse_team_abbreviation("") == ""
|
|
|
|
def test_none_returns_empty(self):
|
|
assert parse_team_abbreviation(None) == ""
|
|
|
|
def test_extracts_uppercase(self):
|
|
result = parse_team_abbreviation("LAL")
|
|
assert result == "LAL"
|
|
|
|
def test_fallback_first_three(self):
|
|
# text without recognisable 2-4 char uppercase block
|
|
result = parse_team_abbreviation("ab")
|
|
assert len(result) <= 3
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# format_score
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestFormatScore:
|
|
def test_format_score(self):
|
|
assert format_score(14, 7) == "7-14"
|
|
|
|
def test_format_score_strings(self):
|
|
assert format_score("21", "14") == "14-21"
|
|
|
|
def test_zero_zero(self):
|
|
assert format_score(0, 0) == "0-0"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# format_period
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestFormatPeriod:
|
|
def test_basketball_q1(self):
|
|
assert format_period(1, "basketball") == "Q1"
|
|
|
|
def test_basketball_q4(self):
|
|
assert format_period(4, "basketball") == "Q4"
|
|
|
|
def test_basketball_ot1(self):
|
|
assert format_period(5, "basketball") == "OT1"
|
|
|
|
def test_basketball_ot2(self):
|
|
assert format_period(6, "basketball") == "OT2"
|
|
|
|
def test_football_q1(self):
|
|
assert format_period(1, "football") == "Q1"
|
|
|
|
def test_football_ot(self):
|
|
assert format_period(5, "football") == "OT1"
|
|
|
|
def test_hockey_p1(self):
|
|
assert format_period(1, "hockey") == "P1"
|
|
|
|
def test_hockey_p3(self):
|
|
assert format_period(3, "hockey") == "P3"
|
|
|
|
def test_hockey_ot(self):
|
|
assert format_period(4, "hockey") == "OT1"
|
|
|
|
def test_baseball_inning(self):
|
|
assert format_period(7, "baseball") == "INN 7"
|
|
|
|
def test_unknown_sport(self):
|
|
result = format_period(2, "unknown")
|
|
assert "2" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# is_live_game / is_final_game / is_upcoming_game
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestGameStatusHelpers:
|
|
def test_is_live_game_true(self):
|
|
assert is_live_game("In Progress") is True
|
|
assert is_live_game("halftime") is True
|
|
assert is_live_game("overtime") is True
|
|
|
|
def test_is_live_game_false(self):
|
|
assert is_live_game("Final") is False
|
|
assert is_live_game("Scheduled") is False
|
|
|
|
def test_is_final_game_true(self):
|
|
assert is_final_game("Final") is True
|
|
assert is_final_game("COMPLETED") is True
|
|
|
|
def test_is_final_game_false(self):
|
|
assert is_final_game("In Progress") is False
|
|
|
|
def test_is_upcoming_game_true(self):
|
|
assert is_upcoming_game("Scheduled") is True
|
|
assert is_upcoming_game("upcoming") is True
|
|
|
|
def test_is_upcoming_game_false(self):
|
|
assert is_upcoming_game("Final") is False
|
|
assert is_upcoming_game("In Progress") is False
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# sanitize_filename
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestSanitizeFilename:
|
|
def test_removes_invalid_chars(self):
|
|
result = sanitize_filename('file<>:"/\\|?*.txt')
|
|
assert "<" not in result
|
|
assert ">" not in result
|
|
assert ":" not in result
|
|
|
|
def test_collapses_underscores(self):
|
|
result = sanitize_filename("file___name")
|
|
assert "__" not in result
|
|
|
|
def test_strips_leading_trailing(self):
|
|
result = sanitize_filename("_file_")
|
|
assert not result.startswith("_")
|
|
assert not result.endswith("_")
|
|
|
|
def test_normal_filename_unchanged(self):
|
|
result = sanitize_filename("my_logo")
|
|
assert result == "my_logo"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# truncate_text
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestTruncateText:
|
|
def test_no_truncation_needed(self):
|
|
assert truncate_text("hello", 10) == "hello"
|
|
|
|
def test_truncation_adds_suffix(self):
|
|
result = truncate_text("hello world", 8)
|
|
assert result.endswith("...")
|
|
assert len(result) == 8
|
|
|
|
def test_exact_length(self):
|
|
assert truncate_text("hello", 5) == "hello"
|
|
|
|
def test_custom_suffix(self):
|
|
result = truncate_text("hello world", 8, suffix="~")
|
|
assert result.endswith("~")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# parse_boolean
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class TestParseBoolean:
|
|
def test_true_bool(self):
|
|
assert parse_boolean(True) is True
|
|
|
|
def test_false_bool(self):
|
|
assert parse_boolean(False) is False
|
|
|
|
def test_int_1(self):
|
|
assert parse_boolean(1) is True
|
|
|
|
def test_int_0(self):
|
|
assert parse_boolean(0) is False
|
|
|
|
def test_string_true(self):
|
|
for val in ("true", "True", "TRUE", "1", "yes", "on", "enabled"):
|
|
assert parse_boolean(val) is True, f"Expected True for {val!r}"
|
|
|
|
def test_string_false(self):
|
|
for val in ("false", "False", "0", "no", "off", "disabled"):
|
|
assert parse_boolean(val) is False, f"Expected False for {val!r}"
|
|
|
|
def test_none_returns_false(self):
|
|
assert parse_boolean(None) is False # type: ignore[arg-type]
|