mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-24 13:23:32 +00:00
test: add 306 new tests covering previously untested modules
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
This commit is contained in:
299
test/test_background_data_service.py
Normal file
299
test/test_background_data_service.py
Normal file
@@ -0,0 +1,299 @@
|
||||
"""
|
||||
Tests for src/background_data_service.py
|
||||
|
||||
Covers BackgroundDataService: submit_fetch_request, get_result,
|
||||
is_request_complete, get_request_status, cancel_request, get_statistics,
|
||||
_cleanup_completed_requests, shutdown, and get_background_service singleton.
|
||||
"""
|
||||
|
||||
import time
|
||||
import pytest
|
||||
from unittest.mock import MagicMock, patch, Mock
|
||||
from concurrent.futures import Future
|
||||
|
||||
from src.background_data_service import (
|
||||
BackgroundDataService,
|
||||
FetchStatus,
|
||||
FetchResult,
|
||||
FetchRequest,
|
||||
get_background_service,
|
||||
shutdown_background_service,
|
||||
)
|
||||
import src.background_data_service as bds_module
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_global_service():
|
||||
"""Ensure each test starts with no global singleton."""
|
||||
shutdown_background_service()
|
||||
yield
|
||||
shutdown_background_service()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cache_manager():
|
||||
m = MagicMock()
|
||||
m.get.return_value = None
|
||||
m.set.return_value = None
|
||||
m.generate_sport_cache_key.return_value = "test_key"
|
||||
return m
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service(mock_cache_manager):
|
||||
svc = BackgroundDataService(mock_cache_manager, max_workers=2, request_timeout=5)
|
||||
yield svc
|
||||
svc.shutdown(wait=False)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Initialisation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestInitialisation:
|
||||
def test_stats_zeroed(self, service):
|
||||
stats = service.get_statistics()
|
||||
assert stats["total_requests"] == 0
|
||||
assert stats["completed_requests"] == 0
|
||||
assert stats["failed_requests"] == 0
|
||||
|
||||
def test_no_active_requests(self, service):
|
||||
assert len(service.active_requests) == 0
|
||||
|
||||
def test_not_shutdown(self, service):
|
||||
assert service._shutdown is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cache hit path
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCacheHit:
|
||||
def test_cache_hit_returns_request_id(self, service, mock_cache_manager):
|
||||
mock_cache_manager.get.return_value = {"events": [{"id": "1"}]}
|
||||
req_id = service.submit_fetch_request(
|
||||
sport="nfl", year=2024,
|
||||
url="https://example.com/nfl",
|
||||
cache_key="nfl_key",
|
||||
)
|
||||
assert req_id is not None
|
||||
# Request should be immediately complete due to cache hit
|
||||
result = service.get_result(req_id)
|
||||
assert result is not None
|
||||
assert result.success is True
|
||||
assert result.cached is True
|
||||
|
||||
def test_cache_hit_increments_stat(self, service, mock_cache_manager):
|
||||
mock_cache_manager.get.return_value = {"events": []}
|
||||
service.submit_fetch_request(sport="nba", year=2024, url="https://x.com", cache_key="k")
|
||||
stats = service.get_statistics()
|
||||
assert stats["cached_hits"] == 1
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Actual fetch path (mocked HTTP)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestFetchPath:
|
||||
def _valid_payload(self) -> dict:
|
||||
return {"events": [{"id": "g1"}, {"id": "g2"}]}
|
||||
|
||||
def test_successful_fetch_completes(self, service, mock_cache_manager):
|
||||
mock_resp = Mock()
|
||||
mock_resp.json.return_value = self._valid_payload()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
|
||||
with patch.object(service.session, "get", return_value=mock_resp):
|
||||
req_id = service.submit_fetch_request(
|
||||
sport="nfl", year=2024,
|
||||
url="https://example.com/nfl",
|
||||
cache_key="nfl_test",
|
||||
)
|
||||
# Wait for the background thread
|
||||
deadline = time.time() + 5
|
||||
while not service.is_request_complete(req_id) and time.time() < deadline:
|
||||
time.sleep(0.05)
|
||||
|
||||
result = service.get_result(req_id)
|
||||
assert result is not None
|
||||
assert result.success is True
|
||||
assert result.data == self._valid_payload()
|
||||
|
||||
def test_failed_fetch_records_error(self, service, mock_cache_manager):
|
||||
with patch.object(service.session, "get", side_effect=Exception("network error")):
|
||||
req_id = service.submit_fetch_request(
|
||||
sport="nba", year=2024,
|
||||
url="https://example.com/nba",
|
||||
cache_key="nba_test",
|
||||
max_retries=0,
|
||||
)
|
||||
deadline = time.time() + 5
|
||||
while not service.is_request_complete(req_id) and time.time() < deadline:
|
||||
time.sleep(0.05)
|
||||
|
||||
result = service.get_result(req_id)
|
||||
assert result is not None
|
||||
assert result.success is False
|
||||
assert result.error is not None
|
||||
|
||||
def test_cache_miss_increments_stat(self, service, mock_cache_manager):
|
||||
mock_resp = Mock()
|
||||
mock_resp.json.return_value = self._valid_payload()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
|
||||
with patch.object(service.session, "get", return_value=mock_resp):
|
||||
service.submit_fetch_request(
|
||||
sport="nfl", year=2024, url="https://x.com", cache_key="new_key",
|
||||
)
|
||||
stats = service.get_statistics()
|
||||
assert stats["cache_misses"] == 1
|
||||
|
||||
def test_callback_called_on_success(self, service, mock_cache_manager):
|
||||
callback = Mock()
|
||||
mock_resp = Mock()
|
||||
mock_resp.json.return_value = self._valid_payload()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
|
||||
with patch.object(service.session, "get", return_value=mock_resp):
|
||||
req_id = service.submit_fetch_request(
|
||||
sport="nfl", year=2024, url="https://x.com",
|
||||
cache_key="cb_key", callback=callback, max_retries=0,
|
||||
)
|
||||
deadline = time.time() + 5
|
||||
while not service.is_request_complete(req_id) and time.time() < deadline:
|
||||
time.sleep(0.05)
|
||||
|
||||
callback.assert_called_once()
|
||||
call_arg = callback.call_args[0][0]
|
||||
assert isinstance(call_arg, FetchResult)
|
||||
|
||||
def test_data_cached_after_successful_fetch(self, service, mock_cache_manager):
|
||||
mock_resp = Mock()
|
||||
mock_resp.json.return_value = self._valid_payload()
|
||||
mock_resp.raise_for_status.return_value = None
|
||||
|
||||
with patch.object(service.session, "get", return_value=mock_resp):
|
||||
req_id = service.submit_fetch_request(
|
||||
sport="nfl", year=2024, url="https://x.com", cache_key="cache_after_key",
|
||||
)
|
||||
deadline = time.time() + 5
|
||||
while not service.is_request_complete(req_id) and time.time() < deadline:
|
||||
time.sleep(0.05)
|
||||
|
||||
mock_cache_manager.set.assert_called()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request status / cancel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestRequestStatusAndCancel:
|
||||
def test_unknown_request_status_is_none(self, service):
|
||||
assert service.get_request_status("nonexistent") is None
|
||||
|
||||
def test_cancel_active_request(self, service, mock_cache_manager):
|
||||
# Manually insert an active request
|
||||
req = FetchRequest(
|
||||
id="r1", sport="nfl", year=2024,
|
||||
cache_key="k", url="https://x.com",
|
||||
)
|
||||
req.status = FetchStatus.PENDING
|
||||
service.active_requests["r1"] = req
|
||||
result = service.cancel_request("r1")
|
||||
assert result is True
|
||||
assert "r1" not in service.active_requests
|
||||
|
||||
def test_cancel_nonexistent_request(self, service):
|
||||
assert service.cancel_request("does-not-exist") is False
|
||||
|
||||
def test_is_request_complete_false_for_active(self, service, mock_cache_manager):
|
||||
req = FetchRequest(
|
||||
id="r2", sport="mlb", year=2024,
|
||||
cache_key="k2", url="https://x.com",
|
||||
)
|
||||
service.active_requests["r2"] = req
|
||||
assert service.is_request_complete("r2") is False
|
||||
|
||||
def test_is_request_complete_true_for_done(self, service):
|
||||
result = FetchResult(request_id="r3", success=True)
|
||||
service.completed_requests["r3"] = result
|
||||
assert service.is_request_complete("r3") is True
|
||||
|
||||
def test_get_result_returns_none_for_unknown(self, service):
|
||||
assert service.get_result("unknown") is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shutdown
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestShutdown:
|
||||
def test_shutdown_sets_flag(self, service):
|
||||
service.shutdown(wait=False)
|
||||
assert service._shutdown is True
|
||||
|
||||
def test_submit_after_shutdown_raises(self, service, mock_cache_manager):
|
||||
service.shutdown(wait=False)
|
||||
with pytest.raises(RuntimeError, match="shutting down"):
|
||||
service.submit_fetch_request(
|
||||
sport="nfl", year=2024, url="https://x.com", cache_key="k"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Cleanup
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCleanup:
|
||||
def test_cleanup_removes_old_requests(self, service):
|
||||
old_result = FetchResult(request_id="old", success=True)
|
||||
old_result.completed_at = time.time() - 7200 # 2 hours ago
|
||||
service.completed_requests["old"] = old_result
|
||||
service._last_completed_requests_cleanup = 0 # force cleanup
|
||||
removed = service._cleanup_completed_requests(force=True)
|
||||
assert removed >= 1
|
||||
assert "old" not in service.completed_requests
|
||||
|
||||
def test_cleanup_respects_interval(self, service):
|
||||
old_result = FetchResult(request_id="r", success=True)
|
||||
old_result.completed_at = time.time() - 7200
|
||||
service.completed_requests["r"] = old_result
|
||||
# Cleanup interval not passed, should skip
|
||||
service._last_completed_requests_cleanup = time.time()
|
||||
removed = service._cleanup_completed_requests(force=False)
|
||||
assert removed == 0
|
||||
|
||||
def test_size_limit_enforcement(self, service):
|
||||
service._max_completed_requests = 3
|
||||
for i in range(5):
|
||||
result = FetchResult(request_id=str(i), success=True)
|
||||
result.completed_at = time.time() - (5 - i) * 100 # oldest first
|
||||
service.completed_requests[str(i)] = result
|
||||
service._last_completed_requests_cleanup = 0
|
||||
service._cleanup_completed_requests(force=True)
|
||||
assert len(service.completed_requests) <= 3
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Singleton get_background_service
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGetBackgroundService:
|
||||
def test_first_call_requires_cache_manager(self):
|
||||
with pytest.raises(ValueError, match="cache_manager is required"):
|
||||
get_background_service()
|
||||
|
||||
def test_creates_singleton(self, mock_cache_manager):
|
||||
svc1 = get_background_service(mock_cache_manager)
|
||||
svc2 = get_background_service()
|
||||
assert svc1 is svc2
|
||||
|
||||
def test_shutdown_clears_singleton(self, mock_cache_manager):
|
||||
get_background_service(mock_cache_manager)
|
||||
shutdown_background_service()
|
||||
with pytest.raises(ValueError):
|
||||
get_background_service()
|
||||
Reference in New Issue
Block a user