screen-locker/screen_locker/tests/test_shutdown_leetcode.py
Krzysztof kuhy Rudnicki bbc14ec04c
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (flutter, flutter-analyze, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (mypy, mypy, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (pylint, pylint, ) (push) Waiting to run
pre-commit / pre-commit (${{ matrix.group }}) (rest, , mypy,pylint,flutter-analyze) (push) Waiting to run
Tests / test (push) Waiting to run
Tests / lock-popup-safety (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (0) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (1) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (2) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (3) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (4) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (5) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (6) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (7) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (8) (push) Waiting to run
Tests / flutter test (shard ${{ matrix.shard }}) (9) (push) Waiting to run
Tests / flutter-coverage (push) Blocked by required conditions
Tests / web-coverage (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
earned_time 0.6.0: per-day registry consumer, sleep-ladder date-proof tests, pin v0.6.0
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-09 21:36:53 +02:00

145 lines
5.3 KiB
Python

"""Tests for the LeetCode hour as a term of the shutdown derivation.
Two entry points share one stamp: the daily reset folds the hour into its
target and stamps it, and the live pass adds it once for a solve that lands
after the reset. Neither may apply it twice, and neither may lose it.
"""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
import earned_time
import pytest
from screen_locker._day import today_str
from screen_locker._shutdown_base import (
_apply_flat_bonus,
base_minutes,
reset_to_base_if_new_day,
)
from screen_locker.tests._earned_fixtures import answering
if TYPE_CHECKING:
from collections.abc import Iterator
from pathlib import Path
def _mixin(*, adjust_ok: bool = True) -> MagicMock:
mixin = MagicMock()
mixin._read_shutdown_config.return_value = (base_minutes(), base_minutes(), 300)
mixin._write_shutdown_config.return_value = True
mixin._adjust_shutdown_time_by.return_value = adjust_ok
return mixin
def apply_leetcode_bonus_if_new(state: Path, mixin: MagicMock) -> bool:
"""The live pass for the LeetCode earner alone."""
return _apply_flat_bonus(state, mixin, earned_time.LEETCODE)
@pytest.fixture
def solved() -> Iterator[MagicMock]:
"""LeetCode solved today; reading not, so the stamps here are LeetCode's."""
with answering({"leetcode": True, "reading": False}) as m:
yield m
@pytest.fixture
def unsolved() -> Iterator[MagicMock]:
with answering({"leetcode": False, "reading": False}) as m:
yield m
@pytest.mark.usefixtures("pre_ladder")
class TestResetIncludesTheHour:
def test_reset_writes_base_plus_leetcode(
self, tmp_path: Path, solved: MagicMock
) -> None:
state = tmp_path / "state.json"
mixin = _mixin()
assert reset_to_base_if_new_day(state, mixin) is True
mixin._write_shutdown_config.assert_called_once_with(
base_minutes() + 60, base_minutes() + 60, 300, restore=True
)
assert json.loads(state.read_text()) == {
"last_reset_date": today_str(),
"leetcode_bonus_date": today_str(),
}
def test_reset_without_solve_leaves_no_stamp(
self, tmp_path: Path, unsolved: MagicMock
) -> None:
state = tmp_path / "state.json"
assert reset_to_base_if_new_day(state, _mixin()) is True
assert json.loads(state.read_text()) == {"last_reset_date": today_str()}
def test_live_pass_after_a_reset_that_included_it_is_a_no_op(
self, tmp_path: Path, solved: MagicMock
) -> None:
"""The 2026-09-13 shape, inverted: reset first, then the tick."""
state = tmp_path / "state.json"
mixin = _mixin()
reset_to_base_if_new_day(state, mixin)
assert apply_leetcode_bonus_if_new(state, mixin) is False
mixin._adjust_shutdown_time_by.assert_not_called()
@pytest.mark.usefixtures("pre_ladder")
class TestLivePass:
def test_applies_once_and_stamps(self, tmp_path: Path, solved: MagicMock) -> None:
state = tmp_path / "state.json"
state.write_text(json.dumps({"last_reset_date": today_str()}))
mixin = _mixin()
assert apply_leetcode_bonus_if_new(state, mixin) is True
assert apply_leetcode_bonus_if_new(state, mixin) is False
mixin._adjust_shutdown_time_by.assert_called_once_with(60)
assert json.loads(state.read_text()) == {
"last_reset_date": today_str(),
"leetcode_bonus_date": today_str(),
}
def test_yesterdays_stamp_does_not_block_today(
self, tmp_path: Path, solved: MagicMock
) -> None:
state = tmp_path / "state.json"
state.write_text(json.dumps({"leetcode_bonus_date": "2000-01-01"}))
assert apply_leetcode_bonus_if_new(state, _mixin()) is True
def test_no_solve_no_write(self, tmp_path: Path, unsolved: MagicMock) -> None:
mixin = _mixin()
assert apply_leetcode_bonus_if_new(tmp_path / "state.json", mixin) is False
mixin._adjust_shutdown_time_by.assert_not_called()
def test_failed_write_leaves_no_stamp(
self, tmp_path: Path, solved: MagicMock
) -> None:
"""A stamp without a write would silently forfeit the hour for the day."""
state = tmp_path / "state.json"
assert apply_leetcode_bonus_if_new(state, _mixin(adjust_ok=False)) is False
assert not state.exists()
def test_reset_after_a_live_pass_does_not_double(
self, tmp_path: Path, solved: MagicMock
) -> None:
"""Live pass before today's reset: the reset rewrites from scratch."""
state = tmp_path / "state.json"
mixin = _mixin()
assert apply_leetcode_bonus_if_new(state, mixin) is True
assert reset_to_base_if_new_day(state, mixin) is True
mixin._write_shutdown_config.assert_called_once_with(
base_minutes() + 60, base_minutes() + 60, 300, restore=True
)
assert apply_leetcode_bonus_if_new(state, mixin) is False
mixin._adjust_shutdown_time_by.assert_called_once()
def test_state_write_failure_is_logged_not_raised(
self, tmp_path: Path, solved: MagicMock
) -> None:
state = MagicMock()
state.exists.return_value = False
state.open.side_effect = OSError("disk full")
assert apply_leetcode_bonus_if_new(state, _mixin()) is True