Files
2026-09-02 14:57:29 +09:00

117 lines
4.1 KiB
Python

"""Week helpers and WeeklyClock schedule arithmetic."""
from __future__ import annotations
from datetime import date, datetime, timedelta
from zoneinfo import ZoneInfo
import pytest
from sebrief.week import WeeklyClock, parse_hhmm, week_id, week_range
KST = ZoneInfo("Asia/Seoul")
# ------------------------------------------------------------------------- week id / range
def test_week_id_and_range_roundtrip():
assert week_id(date(2026, 8, 31)) == "2026-W36"
assert week_id(date(2026, 9, 6)) == "2026-W36"
assert week_range("2026-W36") == (date(2026, 8, 31), date(2026, 9, 6))
def test_week_id_uses_iso_year_at_boundaries():
# 2026-01-01 is a Thursday -> belongs to ISO 2026-W01 starting Mon 2025-12-29
assert week_id(date(2025, 12, 29)) == "2026-W01"
assert week_range("2026-W01")[0] == date(2025, 12, 29)
@pytest.mark.parametrize("bad", ["2026-36", "2026-W99", "2026W36", "abcd-W01", "2026-W1"])
def test_week_range_rejects_invalid(bad):
with pytest.raises(ValueError):
week_range(bad)
@pytest.mark.parametrize("bad", ["8:", "25:00", "08:60", "abc", "0820"])
def test_parse_hhmm_rejects_invalid(bad):
with pytest.raises(ValueError):
parse_hhmm(bad)
def test_parse_hhmm_ok():
assert parse_hhmm("07:00") == (7, 0)
assert parse_hhmm(" 7:05 ") == (7, 5)
# ------------------------------------------------------------------------------ WeeklyClock
def test_clock_rejects_bad_config():
with pytest.raises(ValueError):
WeeklyClock(run_weekday=7)
with pytest.raises(ValueError):
WeeklyClock(run_time="25:00")
def test_next_run_before_and_after_the_weekly_slot():
clock = WeeklyClock(0, "07:00")
before = datetime(2026, 9, 7, 6, 59, tzinfo=KST) # Monday, one minute before the run
week, run_at = clock.next_run(before)
assert (week, run_at) == ("2026-W36", datetime(2026, 9, 7, 7, 0, tzinfo=KST))
after = datetime(2026, 9, 7, 7, 0, tzinfo=KST) # exactly at the slot -> next week's run
week, run_at = clock.next_run(after)
assert (week, run_at) == ("2026-W37", datetime(2026, 9, 14, 7, 0, tzinfo=KST))
def test_next_run_midweek_and_custom_weekday():
wed = datetime(2026, 9, 9, 12, 0, tzinfo=KST)
assert WeeklyClock(0, "07:00").next_run(wed) == (
"2026-W37",
datetime(2026, 9, 14, 7, 0, tzinfo=KST),
)
# Thursday 08:30 runs cover the week that ended the previous Sunday
assert WeeklyClock(3, "08:30").next_run(wed) == (
"2026-W36",
datetime(2026, 9, 10, 8, 30, tzinfo=KST),
)
def test_next_run_requires_tz_aware():
with pytest.raises(ValueError):
WeeklyClock().next_run(datetime(2026, 9, 7, 12, 0)) # noqa: DTZ001 - naive on purpose
def test_run_at_for_is_consistent_with_next_run():
clock = WeeklyClock(0, "07:00")
assert clock.run_at_for("2026-W36") == datetime(2026, 9, 7, 7, 0, tzinfo=KST)
clock2 = WeeklyClock(3, "08:30")
assert clock2.run_at_for("2026-W36") == datetime(2026, 9, 10, 8, 30, tzinfo=KST)
def test_last_run_and_latest_completed_week():
clock = WeeklyClock(0, "07:00")
monday_noon = datetime(2026, 9, 7, 12, 0, tzinfo=KST)
assert clock.last_run(monday_noon) == ("2026-W36", datetime(2026, 9, 7, 7, 0, tzinfo=KST))
assert clock.latest_completed_week(monday_noon) == "2026-W36"
# mid-week: the last run is still the past Monday
wed = datetime(2026, 9, 9, 9, 0, tzinfo=KST)
assert clock.last_run(wed) == ("2026-W36", datetime(2026, 9, 7, 7, 0, tzinfo=KST))
@pytest.mark.parametrize(
("now_delta", "expected"),
[
(timedelta(hours=1), "2026-W36"), # 1h after the slot: inside the window
(timedelta(hours=71), "2026-W36"), # still inside the 72h window
(timedelta(hours=73), None), # outside
],
)
def test_catchup_candidate_window(now_delta, expected):
clock = WeeklyClock(0, "07:00")
run_at = datetime(2026, 9, 7, 7, 0, tzinfo=KST)
assert clock.catchup_candidate(run_at + now_delta, 72) == expected
def test_covered_week():
clock = WeeklyClock(0, "07:00")
assert clock.covered_week(date(2026, 9, 7)) == "2026-W36"
assert clock.covered_week(date(2026, 9, 14)) == "2026-W37"