mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-11 18:33:57 +01:00
Real one-line docstrings stating what each test pins down. Files that would pass 250 lines moved whole classes into _part2 siblings (sick_panel, earned, poke_server, shutdown_reading, manual_workout_dialog). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WzxdWYdwRAtMg7ki6TKxuU
191 lines
7.6 KiB
Python
191 lines
7.6 KiB
Python
"""A credit that lands after tonight's shutdown restarts the clock from it.
|
|
|
|
kuhy, 2026-10-17: shutting down must never make a finished task impossible to
|
|
submit. A credit timed at or after tonight's shutdown moves it to the
|
|
credit's own time plus its reward (capped at the ceiling), so LeetCode done
|
|
at 19:00 after an 18:00 shutdown means 20:00. Anchored on the credit's time,
|
|
never the sync's: a workout done at 17:00 and uploaded at 19:00 is not late.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, datetime
|
|
from typing import TYPE_CHECKING, Any
|
|
from unittest.mock import patch
|
|
|
|
import earned_time
|
|
import pytest
|
|
|
|
from screen_locker import _grace_floor, _late_credit, _shutdown, _sick_floor
|
|
from screen_locker._earned import first_minutes
|
|
from screen_locker._grace_floor import plan_absorb, save_lift
|
|
from screen_locker._shutdown import ShutdownMixin
|
|
from screen_locker.tests._sick_helpers import mark_sick
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
|
|
# Captured before conftest pins it to noon for every test.
|
|
_real_now = _late_credit._now
|
|
|
|
_DAY = date(2026, 10, 17) # a Saturday: the Thu-Sun band is tonight's
|
|
_ISO = _DAY.isoformat()
|
|
|
|
|
|
def at(hhmm: str, day: date = _DAY) -> float:
|
|
"""Unix time of ``hhmm`` local on ``day``."""
|
|
hour, minute = (int(p) for p in hhmm.split(":"))
|
|
return datetime(day.year, day.month, day.day, hour, minute).astimezone().timestamp()
|
|
|
|
|
|
def host_workout_minutes() -> int:
|
|
"""The reward in minutes that the workout earner pays on the test day."""
|
|
return first_minutes(earned_time.WORKOUT, _DAY)
|
|
|
|
|
|
def m(hhmm: str) -> int:
|
|
"""Convert an HH:MM string into minutes after midnight."""
|
|
hour, minute = (int(p) for p in hhmm.split(":"))
|
|
return hour * 60 + minute
|
|
|
|
|
|
class Host(ShutdownMixin):
|
|
"""The real add paths over an in-memory config."""
|
|
|
|
def __init__(self, tonight: str, *, ok: bool = True) -> None:
|
|
self.config: tuple[int, int, int] = (m(tonight), m(tonight), 300)
|
|
self.ok = ok
|
|
|
|
def _read_shutdown_config(self) -> tuple[int, int, int] | None:
|
|
return self.config
|
|
|
|
def _write_shutdown_config(
|
|
self, mon_wed: int, thu_sun: int, morning_end: int, *, restore: bool = False
|
|
) -> bool:
|
|
assert restore
|
|
if self.ok:
|
|
self.config = (mon_wed, thu_sun, morning_end)
|
|
return self.ok
|
|
|
|
@property
|
|
def tonight(self) -> int:
|
|
"""Tonight's shutdown time in minutes (the Thu-Sun band)."""
|
|
return self.config[1]
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _on_the_day() -> Iterator[Any]:
|
|
with (
|
|
patch.object(_sick_floor, "today_str", return_value=_ISO),
|
|
patch.object(_grace_floor, "today_str", return_value=_ISO),
|
|
patch.object(_shutdown, "today_str", return_value=_ISO),
|
|
patch.object(_late_credit, "today_str", return_value=_ISO),
|
|
):
|
|
yield
|
|
|
|
|
|
class TestLateCredit:
|
|
"""Credits timed after tonight's shutdown restart the clock from their own time."""
|
|
|
|
def test_a_credit_before_the_shutdown_adds_as_before(self) -> None:
|
|
"""A credit before the shutdown simply adds its reward to tonight."""
|
|
host = Host("20:00")
|
|
assert host._adjust_shutdown_time_by(60, at=at("18:30")) is True
|
|
assert host.tonight == m("21:00")
|
|
|
|
def test_a_credit_after_the_shutdown_counts_from_its_own_time(self) -> None:
|
|
"""A late credit sets tonight to its own time plus the reward, in both bands."""
|
|
host = Host("18:00")
|
|
assert host._adjust_shutdown_time_by(60, at=at("19:00")) is True
|
|
assert host.tonight == m("20:00")
|
|
assert host.config[0] == m("20:00") # both bands, like the floors
|
|
|
|
def test_a_credit_exactly_at_the_shutdown_is_late(self) -> None:
|
|
"""The check script locks at ``now >= shutdown``."""
|
|
host = Host("19:00")
|
|
host._adjust_shutdown_time_by(60, at=at("19:00"))
|
|
assert host.tonight == m("20:00")
|
|
|
|
def test_capped_at_the_ceiling(self) -> None:
|
|
"""A late credit never pushes tonight past the 23:00 ceiling."""
|
|
host = Host("22:30")
|
|
host._adjust_shutdown_time_by(120, at=at("22:45"))
|
|
assert host.tonight == m("23:00")
|
|
|
|
def test_after_the_ceiling_nothing_moves_past_it(self) -> None:
|
|
"""A credit after the ceiling leaves tonight at 23:00."""
|
|
host = Host("23:00")
|
|
host._adjust_shutdown_time_by(60, at=at("23:10"))
|
|
assert host.tonight == m("23:00")
|
|
|
|
def test_a_late_upload_is_not_a_late_credit(self) -> None:
|
|
"""Done at 17:00, synced at 19:00: 18:00 + 2 h, not 19:00 + 2 h."""
|
|
host = Host("18:00")
|
|
assert host._adjust_shutdown_time_later(at=at("17:00")) is True
|
|
assert host.tonight == m("18:00") + host_workout_minutes()
|
|
|
|
def test_a_late_workout_counts_from_its_end(self) -> None:
|
|
"""A workout finishing after the shutdown counts from its end time."""
|
|
host = Host("18:00")
|
|
host._adjust_shutdown_time_later(at=at("19:00"))
|
|
assert host.tonight == min(m("23:00"), m("19:00") + host_workout_minutes())
|
|
|
|
def test_a_credit_timed_on_another_day_is_never_late(self) -> None:
|
|
"""A credit stamped on a different day is treated as not late."""
|
|
host = Host("18:00")
|
|
host._adjust_shutdown_time_by(60, at=at("19:00", date(2026, 10, 16)))
|
|
assert host.tonight == m("19:00")
|
|
|
|
def test_no_time_means_now(self) -> None:
|
|
"""Without an explicit time the credit is anchored on the current time."""
|
|
host = Host("18:00")
|
|
with patch.object(_late_credit, "_now", return_value=at("19:30")):
|
|
host._adjust_shutdown_time_by(60)
|
|
assert host.tonight == m("20:30")
|
|
|
|
def test_now_is_the_wall_clock(self) -> None:
|
|
"""The default clock reads the real wall-clock time."""
|
|
with patch.object(_late_credit.time, "time", return_value=123.0):
|
|
assert _real_now() == 123.0
|
|
|
|
def test_a_late_credit_clears_the_lift(self) -> None:
|
|
"""Past the shutdown the floor is spent: nothing is left to absorb."""
|
|
save_lift(30)
|
|
host = Host("18:00")
|
|
host._adjust_shutdown_time_by(60, at=at("19:00"))
|
|
assert host.tonight == m("20:00")
|
|
assert plan_absorb(60) == (60, 0)
|
|
|
|
def test_a_failed_write_keeps_the_lift(self) -> None:
|
|
"""If writing the config fails the saved lift is kept."""
|
|
save_lift(30)
|
|
host = Host("18:00", ok=False)
|
|
assert host._adjust_shutdown_time_by(60, at=at("19:00")) is False
|
|
assert plan_absorb(0) == (0, 30)
|
|
|
|
def test_unreadable_config_fails(self) -> None:
|
|
"""An unreadable shutdown config makes the late adjustment fail."""
|
|
host = Host("18:00")
|
|
with patch.object(Host, "_read_shutdown_config", return_value=None):
|
|
assert host._adjust_shutdown_time_by(60, at=at("19:00")) is False
|
|
|
|
|
|
class TestLateOnASickDay:
|
|
"""Late-credit behaviour on a sick day, where the sick target is the floor."""
|
|
|
|
def test_late_after_the_sick_target(self) -> None:
|
|
"""A credit after the sick target counts from its own time."""
|
|
mark_sick(_DAY)
|
|
host = Host("20:00")
|
|
with patch.object(_sick_floor, "sick_target", return_value=m("20:00")):
|
|
assert host._adjust_shutdown_time_by(60, at=at("21:00")) is True
|
|
assert host.tonight == m("22:00")
|
|
|
|
def test_not_late_raises_to_the_target_only(self) -> None:
|
|
"""A not-late credit on a sick day only raises tonight to the sick target."""
|
|
mark_sick(_DAY)
|
|
host = Host("21:00")
|
|
with patch.object(_sick_floor, "sick_target", return_value=m("22:00")):
|
|
host._adjust_shutdown_time_by(60, at=at("15:00"))
|
|
assert host.tonight == m("22:00")
|