137 lines
4.4 KiB
Python
137 lines
4.4 KiB
Python
import asyncio
|
|
from datetime import datetime
|
|
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.pool import StaticPool
|
|
|
|
from app.db.models import Base
|
|
from app.db.repositories import SchedulerSettingsRepository
|
|
from app.db.session import create_session_factory
|
|
from app.sync.scheduler import DayNightIntervalProvider, SyncScheduler, is_daytime
|
|
|
|
|
|
class FakeSyncManager:
|
|
def __init__(self, results=None) -> None:
|
|
self.results = results if results is not None else []
|
|
self.calls = 0
|
|
|
|
async def sync_all_enabled(self):
|
|
self.calls += 1
|
|
if self.results and isinstance(self.results[0], Exception):
|
|
raise self.results[0]
|
|
return list(self.results)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduler_calls_sync_all_and_survives_failure() -> None:
|
|
fake = FakeSyncManager(results=[RuntimeError("one user failed")])
|
|
scheduler = SyncScheduler(fake, interval_seconds=0.01)
|
|
await scheduler.start()
|
|
await asyncio.sleep(0.035)
|
|
await scheduler.stop()
|
|
assert fake.calls >= 2
|
|
assert scheduler.last_tick is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduler_stop_cancels_the_loop() -> None:
|
|
fake = FakeSyncManager()
|
|
scheduler = SyncScheduler(fake, interval_seconds=0.01)
|
|
await scheduler.start()
|
|
await asyncio.sleep(0.035)
|
|
await scheduler.stop()
|
|
|
|
assert scheduler._task is None
|
|
calls_after_stop = fake.calls
|
|
await asyncio.sleep(0.05)
|
|
assert fake.calls == calls_after_stop
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("hour", "day_start", "night_start", "expected"),
|
|
[
|
|
(6, 6, 22, True),
|
|
(21, 6, 22, True),
|
|
(5, 6, 22, False),
|
|
(22, 6, 22, False),
|
|
(0, 6, 22, False),
|
|
# wraps past midnight: day period is [20, 6)
|
|
(23, 20, 6, True),
|
|
(2, 20, 6, True),
|
|
(10, 20, 6, False),
|
|
(20, 20, 6, True),
|
|
(6, 20, 6, False),
|
|
# degenerate: identical start hours means always day
|
|
(13, 9, 9, True),
|
|
],
|
|
)
|
|
def test_is_daytime(hour, day_start, night_start, expected) -> None:
|
|
assert is_daytime(hour, day_start, night_start) is expected
|
|
|
|
|
|
@pytest.fixture
|
|
def db_session_factory():
|
|
engine = create_engine("sqlite://", connect_args={"check_same_thread": False}, poolclass=StaticPool)
|
|
Base.metadata.create_all(engine)
|
|
factory = create_session_factory(engine)
|
|
try:
|
|
yield factory
|
|
finally:
|
|
engine.dispose()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interval_provider_uses_day_interval_during_the_day(db_session_factory) -> None:
|
|
provider = DayNightIntervalProvider(
|
|
db_session_factory, default_minutes=5, now=lambda: datetime(2026, 1, 1, 12, 0)
|
|
)
|
|
assert provider() == 5 * 60
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_interval_provider_uses_night_interval_at_night(db_session_factory) -> None:
|
|
with db_session_factory() as session:
|
|
repo = SchedulerSettingsRepository(session)
|
|
row = repo.get_or_create(default_minutes=5)
|
|
repo.update(row, night_interval_minutes=30)
|
|
|
|
provider = DayNightIntervalProvider(
|
|
db_session_factory, default_minutes=5, now=lambda: datetime(2026, 1, 1, 23, 0)
|
|
)
|
|
assert provider() == 30 * 60
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduler_uses_interval_provider_for_next_tick(db_session_factory) -> None:
|
|
with db_session_factory() as session:
|
|
repo = SchedulerSettingsRepository(session)
|
|
row = repo.get_or_create(default_minutes=5)
|
|
repo.update(row, day_interval_minutes=1, night_interval_minutes=1)
|
|
|
|
provider = DayNightIntervalProvider(
|
|
db_session_factory, default_minutes=5, now=lambda: datetime(2026, 1, 1, 12, 0)
|
|
)
|
|
fake = FakeSyncManager()
|
|
scheduler = SyncScheduler(fake, interval_seconds=999, interval_provider=provider)
|
|
|
|
await scheduler.run_once()
|
|
|
|
expected_seconds = 60 # day_interval_minutes=1 -> 60s, not the unrelated interval_seconds=999 fallback
|
|
delta = (scheduler.next_tick - scheduler.last_tick).total_seconds()
|
|
assert abs(delta - expected_seconds) < 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scheduler_falls_back_when_interval_provider_raises() -> None:
|
|
def broken_provider():
|
|
raise RuntimeError("db unavailable")
|
|
|
|
fake = FakeSyncManager()
|
|
scheduler = SyncScheduler(fake, interval_seconds=42, interval_provider=broken_provider)
|
|
|
|
await scheduler.run_once()
|
|
|
|
delta = (scheduler.next_tick - scheduler.last_tick).total_seconds()
|
|
assert abs(delta - 42) < 1
|