117 lines
4.1 KiB
Python
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"
|