Commit 2636c3c1 authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

test(fm): wait for schedule runs instead of counting them in a fixed window

The schedule tests counted check runs inside 0.1-0.3 s windows. On the CI
runner the failing-check test logged a full traceback per run and fitted
only one run in 0.15 s. Each test now waits (up to 5 s) for the runs it
needs.

Co-Authored-By: default avatarClaude Opus 5.5 <noreply@anthropic.com>
parent 27cda76a
Loading
Loading
Loading
Loading
Loading
+29 −24
Changes for tests/test_zone_offer_schedule.py: 29 added lines, 24 removed lines.
Original line number Diff line number Diff line
@@ -3,66 +3,71 @@ from datetime import datetime, timedelta, timezone

from federation_manager.application.zone_offer_schedule import ZoneOfferSchedule

# generous: the tests wait for an outcome, never count runs inside a fixed window
PATIENCE = 5.0


class Checks:
    def __init__(self, boundary_in: float | None = None) -> None:
    def __init__(self, boundary_in: float | None = None, fail: bool = False) -> None:
        self.runs = 0
        self.boundary_in = boundary_in
        self.fail = fail
        self._changed = asyncio.Event()

    async def __call__(self) -> datetime | None:
        self.runs += 1
        self._changed.set()
        if self.fail:
            raise RuntimeError("database gone")
        if self.boundary_in is None:
            return None
        return datetime.now(timezone.utc) + timedelta(seconds=self.boundary_in)

    async def reach(self, runs: int) -> None:
        async def wait() -> None:
            while self.runs < runs:
                self._changed.clear()
                await self._changed.wait()

async def _run_for(schedule: ZoneOfferSchedule, seconds: float) -> None:
    task = asyncio.create_task(schedule.run())
    await asyncio.sleep(seconds)
    task.cancel()
        await asyncio.wait_for(wait(), PATIENCE)


async def test_the_check_runs_at_start_and_again_at_the_next_agreement_boundary() -> None:
    checks = Checks(boundary_in=0.05)
    schedule = ZoneOfferSchedule(checks, timedelta(hours=1))
    task = asyncio.create_task(ZoneOfferSchedule(checks, timedelta(hours=1)).run())

    await _run_for(schedule, 0.3)
    await checks.reach(3)

    assert checks.runs >= 3
    task.cancel()


async def test_with_no_boundary_ahead_it_waits_for_the_sweep() -> None:
    checks = Checks()
    schedule = ZoneOfferSchedule(checks, timedelta(hours=1))
    task = asyncio.create_task(ZoneOfferSchedule(checks, timedelta(hours=1)).run())
    await checks.reach(1)

    await _run_for(schedule, 0.1)
    await asyncio.sleep(0.1)

    assert checks.runs == 1
    task.cancel()


async def test_an_agreement_write_wakes_it() -> None:
    checks = Checks()
    schedule = ZoneOfferSchedule(checks, timedelta(hours=1))
    task = asyncio.create_task(schedule.run())
    await asyncio.sleep(0.05)
    await checks.reach(1)

    schedule.wake()
    await asyncio.sleep(0.05)
    task.cancel()

    assert checks.runs == 2
    await checks.reach(2)
    task.cancel()


async def test_a_failing_check_does_not_stop_the_schedule() -> None:
    runs = 0
    checks = Checks(fail=True)
    task = asyncio.create_task(ZoneOfferSchedule(checks, timedelta(seconds=0.02)).run())

    async def failing() -> datetime | None:
        nonlocal runs
        runs += 1
        raise RuntimeError("database gone")
    await checks.reach(3)

    schedule = ZoneOfferSchedule(failing, timedelta(seconds=0.02))

    await _run_for(schedule, 0.15)

    assert runs >= 3
    task.cancel()