sched: add occurrence identity and UTC helpers (T02)
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""Occurrence identity and UTC persistence helpers (T02)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from wf_scheduling.calendar import CronSource
|
||||
from wf_scheduling.occurrences import (
|
||||
Occurrence,
|
||||
is_already_consumed,
|
||||
monotonic_ns,
|
||||
occurrence_id,
|
||||
scheduled_at_str,
|
||||
wall_now_utc,
|
||||
)
|
||||
|
||||
|
||||
def utc(*args: int) -> datetime:
|
||||
return datetime(*args, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def test_occurrence_id_is_deterministic_utc_pair() -> None:
|
||||
at = utc(2026, 9, 8, 13, 0, 0)
|
||||
assert occurrence_id("sched-1", at) == occurrence_id("sched-1", at)
|
||||
assert occurrence_id("sched-1", at) != occurrence_id("sched-2", at)
|
||||
assert occurrence_id("sched-1", at) != occurrence_id(
|
||||
"sched-1", utc(2026, 9, 8, 14, 0, 0)
|
||||
)
|
||||
assert "sched-1" in occurrence_id("sched-1", at)
|
||||
|
||||
|
||||
def test_occurrence_id_rejects_naive() -> None:
|
||||
with pytest.raises(ValueError, match="aware"):
|
||||
occurrence_id("s", datetime(2026, 9, 8, 12, 0))
|
||||
|
||||
|
||||
def test_scheduled_at_serializes_as_utc_rfc3339() -> None:
|
||||
from datetime import timedelta
|
||||
|
||||
at = datetime(2026, 9, 8, 12, 0, tzinfo=timezone(timedelta(hours=2)))
|
||||
assert scheduled_at_str(at) == "2026-09-08T10:00:00+00:00"
|
||||
with pytest.raises(ValueError, match="aware"):
|
||||
scheduled_at_str(datetime(2026, 9, 8, 12, 0))
|
||||
|
||||
|
||||
def test_occurrence_model_exposes_contract_fields() -> None:
|
||||
occ = Occurrence(schedule_id="s", scheduled_at=utc(2026, 9, 8, 13, 0, 0))
|
||||
assert occ.schedule_id == "s"
|
||||
assert occ.occurrence_id == occurrence_id("s", utc(2026, 9, 8, 13, 0, 0))
|
||||
assert occ.scheduled_at == utc(2026, 9, 8, 13, 0, 0)
|
||||
payload = occ.to_binding_dict()
|
||||
assert payload["schedule_id"] == "s"
|
||||
assert payload["scheduled_at"] == "2026-09-08T13:00:00+00:00"
|
||||
|
||||
|
||||
def test_calendar_identities_are_unique_and_increasing() -> None:
|
||||
src = CronSource("30 9 * * *", "UTC")
|
||||
cursor = utc(2026, 9, 1, 0, 0, 0)
|
||||
seen: set[str] = set()
|
||||
prev: datetime | None = None
|
||||
for _ in range(10):
|
||||
nxt = src.next_after(cursor)
|
||||
assert nxt is not None
|
||||
ident = occurrence_id("s", nxt)
|
||||
assert ident not in seen
|
||||
seen.add(ident)
|
||||
if prev is not None:
|
||||
assert nxt > prev
|
||||
prev = nxt
|
||||
cursor = nxt
|
||||
|
||||
|
||||
def test_rollback_guard_never_readmits_consumed() -> None:
|
||||
consumed = utc(2026, 9, 8, 12, 20, 0)
|
||||
assert is_already_consumed(utc(2026, 9, 8, 12, 0, 0), consumed)
|
||||
assert is_already_consumed(consumed, consumed)
|
||||
assert not is_already_consumed(utc(2026, 9, 8, 12, 30, 0), consumed)
|
||||
|
||||
|
||||
def test_wall_and_monotonic_clocks_split() -> None:
|
||||
wall = wall_now_utc()
|
||||
assert wall.tzinfo is not None
|
||||
assert wall.utcoffset() is not None
|
||||
first = monotonic_ns()
|
||||
second = monotonic_ns()
|
||||
assert second >= first
|
||||
Reference in New Issue
Block a user