"""Tests for source runner resilience.""" import time from unittest.mock import MagicMock, patch import pytest from app.services.source_runner import ( AllSourcesFailed, SourceDataInsufficient, SourceResult, SourceUnavailable, _backoff_seconds, _count_rows, _is_transient_error, run_sources, ) class TestCountRows: def test_list(self): assert _count_rows([1, 2, 3]) == 3 def test_dict_with_daily_key(self): assert _count_rows({"daily": [1, 2], "other": "value"}) == 2 def test_dict_no_list_keys(self): assert _count_rows({"key": "value"}) is None def test_none(self): assert _count_rows(None) is None class TestIsTransientError: def test_timeout_is_transient(self): assert _is_transient_error(TimeoutError("timeout")) is True def test_source_unavailable_is_transient(self): assert _is_transient_error(SourceUnavailable("unavailable")) is True def test_source_data_insufficient_not_transient(self): assert _is_transient_error(SourceDataInsufficient("insufficient")) is False def test_generic_exception_not_transient(self): assert _is_transient_error(ValueError("bad")) is False class TestBackoffSeconds: def test_exponential_growth(self): b1 = _backoff_seconds(1.0, 0.0, 10.0, 1) b2 = _backoff_seconds(1.0, 0.0, 10.0, 2) b3 = _backoff_seconds(1.0, 0.0, 10.0, 3) assert b1 < b2 < b3 def test_capped_at_max(self): delay = _backoff_seconds(1.0, 0.0, 5.0, 10) assert delay <= 5.0 class TestRunSources: def test_success_returns_data(self): sources = [("test", lambda: {"daily": [1, 2, 3]})] result, attempts = run_sources("test", sources, timeout_seconds=5, retry_attempts=1) assert result.source == "test" assert len(attempts) == 1 assert attempts[0]["ok"] is True def test_min_rows_rejects_insufficient(self): sources = [("test", lambda: {"daily": [1]})] with pytest.raises(AllSourcesFailed): run_sources("test", sources, timeout_seconds=5, retry_attempts=1, min_rows=5) def test_fallback_on_failure(self): def good(): return {"daily": [1, 2, 3]} def bad(): raise ValueError("fail") sources = [("bad", bad), ("good", good)] result, attempts = run_sources("test", sources, timeout_seconds=5, retry_attempts=1) assert result.source == "good" assert len(attempts) == 2 assert attempts[0]["ok"] is False assert attempts[1]["ok"] is True def test_all_fail_raises(self): def fail(): raise ValueError("fail") sources = [("fail", fail)] with pytest.raises(AllSourcesFailed): run_sources("test", sources, timeout_seconds=5, retry_attempts=1)