test: achieve 100% branch coverage across all python_pkg packages

- Add comprehensive tests for all packages (3572 tests, 100% branch coverage)
- Split oversized test files to stay under 500-line limit
- Add per-file ruff ignores for test-appropriate suppressions
- Fix _cache_decks.py to properly convert JSON lists to tuples
- Add session-scoped conftest fixture for logging handler cleanup (Python 3.14)
- Update ruff pre-commit hook to v0.15.2
- Add codespell ignore words for test data
- Add generated output files to .gitignore
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-03-21 17:51:36 +01:00
parent adcab0439b
commit d8f8b21827
8 changed files with 1205 additions and 0 deletions

View File

@ -4,6 +4,7 @@ from __future__ import annotations
from datetime import datetime, timezone from datetime import datetime, timezone
import json import json
import tkinter as tk
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock from unittest.mock import MagicMock
@ -390,3 +391,15 @@ class TestAdjustShutdownTimeLater:
result = locker._adjust_shutdown_time_later() result = locker._adjust_shutdown_time_later()
assert result is False assert result is False
class TestGrabInput:
"""Tests for _grab_input method."""
def test_production_global_grab_tcl_error(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test production mode falls back when global grab fails."""
mock_tk.Tk.return_value.grab_set_global.side_effect = tk.TclError("grab failed")
locker = create_locker(mock_tk, tmp_path, demo_mode=False)
assert locker.demo_mode is False

View File

@ -0,0 +1,268 @@
"""Tests for phone verification coverage gaps (part 2)."""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from python_pkg.screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
class TestGetWirelessSerial:
"""Tests for _get_wireless_serial method."""
def test_returns_wireless_serial(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns ip:port serial for a wireless device."""
locker = create_locker(mock_tk, tmp_path)
output = "List of devices attached\n192.168.1.42:5555\tdevice\n"
with patch.object(locker, "_run_adb", return_value=(True, output)):
result = locker._get_wireless_serial()
assert result == "192.168.1.42:5555"
def test_returns_none_when_adb_fails(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns None when adb devices fails."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(locker, "_run_adb", return_value=(False, "")):
result = locker._get_wireless_serial()
assert result is None
def test_returns_none_when_no_wireless_device(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns None when only USB devices are connected."""
locker = create_locker(mock_tk, tmp_path)
output = "List of devices attached\nABC123DEF456\tdevice\n"
with patch.object(locker, "_run_adb", return_value=(True, output)):
result = locker._get_wireless_serial()
assert result is None
def test_skips_offline_wireless_device(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test skips offline wireless devices."""
locker = create_locker(mock_tk, tmp_path)
output = "List of devices attached\n192.168.1.42:5555\toffline\n"
with patch.object(locker, "_run_adb", return_value=(True, output)):
result = locker._get_wireless_serial()
assert result is None
class TestTryAdbConnect:
"""Tests for _try_adb_connect method."""
def test_successful_connect(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test successful ADB connect."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(
locker, "_run_adb", return_value=(True, "connected to 192.168.1.42:5555")
):
result = locker._try_adb_connect("192.168.1.42:5555")
assert result is True
def test_failed_connect_unable(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test connect failure with 'unable' in output."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(
locker, "_run_adb", return_value=(False, "unable to connect")
):
result = locker._try_adb_connect("192.168.1.42:5555")
assert result is False
def test_failed_connect_with_failed(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test connect failure with 'failed' in output."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(
locker,
"_run_adb",
return_value=(False, "connected but failed to authenticate"),
):
result = locker._try_adb_connect("192.168.1.42:5555")
assert result is False
def test_no_connected_in_output(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test connect failure when 'connected' not in output."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(
locker, "_run_adb", return_value=(False, "some random output")
):
result = locker._try_adb_connect("192.168.1.42:5555")
assert result is False
class TestGetLocalSubnetPrefix:
"""Tests for _get_local_subnet_prefix method."""
def test_returns_prefix(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns first three octets of local IP."""
locker = create_locker(mock_tk, tmp_path)
mock_sock = MagicMock()
mock_sock.getsockname.return_value = ("192.168.1.100", 12345)
mock_sock.__enter__ = MagicMock(return_value=mock_sock)
mock_sock.__exit__ = MagicMock(return_value=False)
with patch(
"python_pkg.screen_locker._phone_verification.socket.socket",
return_value=mock_sock,
):
result = locker._get_local_subnet_prefix()
assert result == "192.168.1"
def test_returns_none_on_oserror(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns None when socket raises OSError."""
locker = create_locker(mock_tk, tmp_path)
with patch(
"python_pkg.screen_locker._phone_verification.socket.socket",
side_effect=OSError("no network"),
):
result = locker._get_local_subnet_prefix()
assert result is None
class TestTryWirelessReconnect:
"""Tests for _try_wireless_reconnect method."""
def test_returns_false_when_no_prefix(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when subnet prefix can't be determined."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(locker, "_get_local_subnet_prefix", return_value=None):
result = locker._try_wireless_reconnect()
assert result is False
def test_returns_true_when_probe_succeeds(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns True when a probe finds the phone."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
patch.object(locker, "_try_adb_connect", return_value=True),
patch.object(locker, "_has_adb_device", return_value=True),
patch(
"python_pkg.screen_locker._phone_verification.socket.create_connection",
) as mock_conn,
):
mock_sock = MagicMock()
mock_sock.__enter__ = MagicMock(return_value=mock_sock)
mock_sock.__exit__ = MagicMock(return_value=False)
mock_conn.return_value = mock_sock
result = locker._try_wireless_reconnect()
assert result is True
def test_returns_false_when_no_probe_succeeds(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when no probe finds the phone."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
patch(
"python_pkg.screen_locker._phone_verification.socket.create_connection",
side_effect=OSError("refused"),
),
):
result = locker._try_wireless_reconnect()
assert result is False
def test_probe_connect_succeeds_but_no_device(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test probe passes socket but adb_connect succeeds without device."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
patch.object(locker, "_try_adb_connect", return_value=True),
patch.object(locker, "_has_adb_device", return_value=False),
patch(
"python_pkg.screen_locker._phone_verification.socket.create_connection",
) as mock_conn,
):
mock_sock = MagicMock()
mock_sock.__enter__ = MagicMock(return_value=mock_sock)
mock_sock.__exit__ = MagicMock(return_value=False)
mock_conn.return_value = mock_sock
result = locker._try_wireless_reconnect()
assert result is False
def test_probe_adb_connect_fails(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test probe where socket connects but adb connect fails."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_get_local_subnet_prefix", return_value="192.168.1"),
patch.object(locker, "_try_adb_connect", return_value=False),
patch(
"python_pkg.screen_locker._phone_verification.socket.create_connection",
) as mock_conn,
):
mock_sock = MagicMock()
mock_sock.__enter__ = MagicMock(return_value=mock_sock)
mock_sock.__exit__ = MagicMock(return_value=False)
mock_conn.return_value = mock_sock
result = locker._try_wireless_reconnect()
assert result is False

View File

@ -0,0 +1,420 @@
"""Tests for shutdown schedule adjustment coverage gaps (part 2)."""
from __future__ import annotations
import json
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from python_pkg.screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
class TestApplyEarlierShutdown:
"""Tests for _apply_earlier_shutdown method."""
def test_returns_false_when_no_config(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when config can't be read."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(locker, "_read_shutdown_config", return_value=None):
assert locker._apply_earlier_shutdown("2026-03-21") is False
def test_returns_false_when_save_state_fails(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when saving state fails."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_read_shutdown_config", return_value=(21, 20, 8)),
patch.object(locker, "_save_sick_day_state", return_value=False),
):
assert locker._apply_earlier_shutdown("2026-03-21") is False
def test_success_applies_earlier_hours(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test successful application of earlier shutdown hours."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_read_shutdown_config", return_value=(21, 20, 8)),
patch.object(locker, "_save_sick_day_state", return_value=True),
patch.object(
locker, "_write_shutdown_config", return_value=True
) as mock_write,
):
result = locker._apply_earlier_shutdown("2026-03-21")
assert result is True
mock_write.assert_called_once_with(20, 19, 8)
def test_clamps_to_minimum_18(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test hours are clamped to minimum of 18."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_read_shutdown_config", return_value=(18, 18, 8)),
patch.object(locker, "_save_sick_day_state", return_value=True),
patch.object(
locker, "_write_shutdown_config", return_value=True
) as mock_write,
):
locker._apply_earlier_shutdown("2026-03-21")
mock_write.assert_called_once_with(18, 18, 8)
class TestAdjustShutdownTimeEarlier:
"""Tests for _adjust_shutdown_time_earlier method."""
def test_returns_false_when_sick_mode_used_today(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when sick mode already used today."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_restore_original_config_if_needed"),
patch.object(locker, "_sick_mode_used_today", return_value=True),
):
assert locker._adjust_shutdown_time_earlier() is False
def test_success(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test successful adjustment."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_restore_original_config_if_needed"),
patch.object(locker, "_sick_mode_used_today", return_value=False),
patch.object(locker, "_apply_earlier_shutdown", return_value=True),
):
assert locker._adjust_shutdown_time_earlier() is True
def test_handles_oserror(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test handles OSError during apply."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_restore_original_config_if_needed"),
patch.object(locker, "_sick_mode_used_today", return_value=False),
patch.object(
locker,
"_apply_earlier_shutdown",
side_effect=OSError("fail"),
),
):
assert locker._adjust_shutdown_time_earlier() is False
def test_handles_value_error(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test handles ValueError during apply."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_restore_original_config_if_needed"),
patch.object(locker, "_sick_mode_used_today", return_value=False),
patch.object(
locker,
"_apply_earlier_shutdown",
side_effect=ValueError("bad"),
),
):
assert locker._adjust_shutdown_time_earlier() is False
class TestAdjustShutdownTimeLater:
"""Tests for _adjust_shutdown_time_later method."""
def test_returns_false_when_no_config(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when config is missing."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(locker, "_read_shutdown_config", return_value=None):
assert locker._adjust_shutdown_time_later() is False
def test_success_applies_later_hours(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test successful later adjustment with restore flag."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_read_shutdown_config", return_value=(20, 19, 8)),
patch.object(
locker, "_write_shutdown_config", return_value=True
) as mock_write,
):
result = locker._adjust_shutdown_time_later()
assert result is True
mock_write.assert_called_once_with(22, 21, 8, restore=True)
def test_clamps_to_max_23(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test hours are clamped to maximum of 23."""
locker = create_locker(mock_tk, tmp_path)
with (
patch.object(locker, "_read_shutdown_config", return_value=(22, 23, 8)),
patch.object(
locker, "_write_shutdown_config", return_value=True
) as mock_write,
):
locker._adjust_shutdown_time_later()
mock_write.assert_called_once_with(23, 23, 8, restore=True)
def test_handles_oserror(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test handles OSError."""
locker = create_locker(mock_tk, tmp_path)
with patch.object(
locker,
"_read_shutdown_config",
side_effect=OSError("fail"),
):
assert locker._adjust_shutdown_time_later() is False
class TestSickModeUsedToday:
"""Tests for _sick_mode_used_today method."""
def test_returns_false_when_no_file(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when state file doesn't exist."""
locker = create_locker(mock_tk, tmp_path)
mock_file = MagicMock()
mock_file.exists.return_value = False
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
mock_file,
):
assert locker._sick_mode_used_today() is False
def test_returns_true_when_used_today(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns True when state matches today."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
):
from datetime import datetime, timezone
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
state_file.write_text(json.dumps({"date": today}))
assert locker._sick_mode_used_today() is True
def test_returns_false_when_different_date(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when state is from different date."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
):
state_file.write_text(json.dumps({"date": "2020-01-01"}))
assert locker._sick_mode_used_today() is False
def test_returns_false_on_json_error(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False on JSONDecodeError."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
):
state_file.write_text("not json{{{")
assert locker._sick_mode_used_today() is False
class TestSaveSickDayState:
"""Tests for _save_sick_day_state method."""
def test_saves_state_successfully(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test saves state file with correct content."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
):
result = locker._save_sick_day_state("2026-03-21", 21, 20)
assert result is True
data = json.loads(state_file.read_text())
assert data["date"] == "2026-03-21"
assert data["original_mon_wed_hour"] == 21
assert data["original_thu_sun_hour"] == 20
def test_returns_false_on_oserror(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when write fails."""
locker = create_locker(mock_tk, tmp_path)
mock_path = MagicMock()
mock_path.open.side_effect = OSError("permission denied")
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
mock_path,
):
result = locker._save_sick_day_state("2026-03-21", 21, 20)
assert result is False
class TestLoadSickDayState:
"""Tests for _load_sick_day_state method."""
def test_loads_valid_state(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test loads state with all fields present."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
state_file.write_text(
json.dumps(
{
"date": "2026-03-20",
"original_mon_wed_hour": 21,
"original_thu_sun_hour": 20,
}
)
)
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
):
result = locker._load_sick_day_state()
assert result == ("2026-03-20", 21, 20)
def test_returns_none_when_fields_missing(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns None when required fields are missing."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
state_file.write_text(json.dumps({"date": "2026-03-20"}))
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
):
result = locker._load_sick_day_state()
assert result is None
class TestWriteRestoredConfig:
"""Tests for _write_restored_config method."""
def test_restores_config_and_removes_state(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test restores config values and deletes state file."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
state_file.write_text("{}")
with (
patch.object(locker, "_read_shutdown_config", return_value=(20, 19, 8)),
patch.object(
locker, "_write_shutdown_config", return_value=True
) as mock_write,
patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
),
):
locker._write_restored_config(21, 20, "2026-03-20")
mock_write.assert_called_once_with(21, 20, 8, restore=True)
assert not state_file.exists()
def test_still_removes_state_when_config_read_fails(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test removes state file even when config read returns None."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
state_file.write_text("{}")
with (
patch.object(locker, "_read_shutdown_config", return_value=None),
patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
),
):
locker._write_restored_config(21, 20, "2026-03-20")
assert not state_file.exists()

View File

@ -0,0 +1,316 @@
"""Tests for shutdown schedule adjustment coverage gaps (part 3)."""
from __future__ import annotations
import json
import subprocess
from typing import TYPE_CHECKING
from unittest.mock import MagicMock, patch
from python_pkg.screen_locker._constants import ADJUST_SHUTDOWN_SCRIPT
from python_pkg.screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
class TestRestoreOriginalConfigIfNeeded:
"""Tests for _restore_original_config_if_needed method."""
def test_no_state_file_does_nothing(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test does nothing when no state file exists."""
locker = create_locker(mock_tk, tmp_path)
mock_file = MagicMock()
mock_file.exists.return_value = False
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
mock_file,
):
locker._restore_original_config_if_needed()
def test_restores_when_state_from_previous_day(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test restores config when state date differs from today."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
state_file.write_text(
json.dumps(
{
"date": "2020-01-01",
"original_mon_wed_hour": 21,
"original_thu_sun_hour": 20,
}
)
)
with (
patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
),
patch.object(locker, "_write_restored_config") as mock_restore,
):
locker._restore_original_config_if_needed()
mock_restore.assert_called_once_with(21, 20, "2020-01-01")
def test_does_not_restore_when_state_from_today(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test does not restore when state date matches today."""
locker = create_locker(mock_tk, tmp_path)
from datetime import datetime, timezone
today = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
state_file = tmp_path / "state.json"
state_file.write_text(
json.dumps(
{
"date": today,
"original_mon_wed_hour": 21,
"original_thu_sun_hour": 20,
}
)
)
with (
patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
),
patch.object(locker, "_write_restored_config") as mock_restore,
):
locker._restore_original_config_if_needed()
mock_restore.assert_not_called()
def test_returns_when_loaded_state_is_none(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns early when loaded state is None."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
state_file.write_text(json.dumps({"date": "2020-01-01"}))
with (
patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
),
patch.object(locker, "_write_restored_config") as mock_restore,
):
locker._restore_original_config_if_needed()
mock_restore.assert_not_called()
def test_handles_oserror(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test handles OSError when loading state."""
locker = create_locker(mock_tk, tmp_path)
mock_file = MagicMock()
mock_file.exists.return_value = True
mock_file.open.side_effect = OSError("fail")
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
mock_file,
):
locker._restore_original_config_if_needed()
def test_handles_json_decode_error(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test handles JSONDecodeError when loading state."""
locker = create_locker(mock_tk, tmp_path)
state_file = tmp_path / "state.json"
state_file.write_text("not valid json{{{")
with patch(
"python_pkg.screen_locker._shutdown.SICK_DAY_STATE_FILE",
state_file,
):
locker._restore_original_config_if_needed()
class TestReadShutdownConfig:
"""Tests for _read_shutdown_config method."""
def test_returns_none_when_file_missing(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns None when config file doesn't exist."""
locker = create_locker(mock_tk, tmp_path)
mock_file = MagicMock()
mock_file.exists.return_value = False
with patch(
"python_pkg.screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
mock_file,
):
assert locker._read_shutdown_config() is None
def test_reads_valid_config(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test reads all three config values from file."""
locker = create_locker(mock_tk, tmp_path)
config_file = tmp_path / "shutdown.conf"
config_file.write_text("MON_WED_HOUR=21\nTHU_SUN_HOUR=20\nMORNING_END_HOUR=8\n")
with patch(
"python_pkg.screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
config_file,
):
result = locker._read_shutdown_config()
assert result == (21, 20, 8)
def test_returns_none_when_values_missing(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns None when config has missing keys."""
locker = create_locker(mock_tk, tmp_path)
config_file = tmp_path / "shutdown.conf"
config_file.write_text("MON_WED_HOUR=21\n")
with patch(
"python_pkg.screen_locker._shutdown.SHUTDOWN_CONFIG_FILE",
config_file,
):
result = locker._read_shutdown_config()
assert result is None
class TestBuildShutdownCmd:
"""Tests for _build_shutdown_cmd method."""
def test_without_restore(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test command without restore flag."""
locker = create_locker(mock_tk, tmp_path)
cmd = locker._build_shutdown_cmd(21, 20, 8, restore=False)
assert cmd == [
"/usr/bin/sudo",
str(ADJUST_SHUTDOWN_SCRIPT),
"21",
"20",
"8",
]
def test_with_restore(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test command with restore flag."""
locker = create_locker(mock_tk, tmp_path)
cmd = locker._build_shutdown_cmd(21, 20, 8, restore=True)
assert cmd == [
"/usr/bin/sudo",
str(ADJUST_SHUTDOWN_SCRIPT),
"--restore",
"21",
"20",
"8",
]
class TestWriteShutdownConfig:
"""Tests for _write_shutdown_config method."""
def test_returns_false_when_script_missing(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False when adjust script doesn't exist."""
locker = create_locker(mock_tk, tmp_path)
mock_script = MagicMock()
mock_script.exists.return_value = False
with patch(
"python_pkg.screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT",
mock_script,
):
result = locker._write_shutdown_config(21, 20, 8)
assert result is False
def test_success_calls_run_shutdown_cmd(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test successful config write delegates to _run_shutdown_cmd."""
locker = create_locker(mock_tk, tmp_path)
mock_script = MagicMock()
mock_script.exists.return_value = True
with (
patch(
"python_pkg.screen_locker._shutdown.ADJUST_SHUTDOWN_SCRIPT",
mock_script,
),
patch.object(locker, "_run_shutdown_cmd", return_value=True) as mock_run,
):
result = locker._write_shutdown_config(21, 20, 8)
assert result is True
mock_run.assert_called_once()
class TestRunShutdownCmd:
"""Tests for _run_shutdown_cmd method."""
def test_success(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test successful command execution."""
locker = create_locker(mock_tk, tmp_path)
mock_result = MagicMock(stdout="OK\n")
with patch(
"python_pkg.screen_locker._shutdown.subprocess.run",
return_value=mock_result,
):
result = locker._run_shutdown_cmd(["cmd"], 21, 20)
assert result is True
def test_returns_false_on_subprocess_error(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test returns False on SubprocessError."""
locker = create_locker(mock_tk, tmp_path)
with patch(
"python_pkg.screen_locker._shutdown.subprocess.run",
side_effect=subprocess.CalledProcessError(1, "cmd"),
):
result = locker._run_shutdown_cmd(["cmd"], 21, 20)
assert result is False

View File

@ -416,3 +416,75 @@ class TestAskWorkoutDone:
locker.clear_container.assert_called_once() locker.clear_container.assert_called_once()
mock_tk.Label.assert_called() mock_tk.Label.assert_called()
mock_tk.Button.assert_called() mock_tk.Button.assert_called()
class TestAskIfSick:
"""Tests for ask_if_sick method."""
def test_ask_if_sick_displays_dialog(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test ask_if_sick shows sick day question."""
locker = create_locker(mock_tk, tmp_path)
object.__setattr__(locker, "clear_container", MagicMock())
locker.ask_if_sick()
locker.clear_container.assert_called_once()
mock_tk.Label.assert_called()
class TestSickQuestionButtons:
"""Tests for _sick_question_buttons method."""
def test_creates_buttons(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test _sick_question_buttons creates yes/no buttons."""
locker = create_locker(mock_tk, tmp_path)
locker._sick_question_buttons()
mock_tk.Button.assert_called()
class TestGetSickDayStatus:
"""Tests for _get_sick_day_status method."""
def test_already_adjusted_today(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test status when sick mode already used today."""
locker = create_locker(mock_tk, tmp_path)
object.__setattr__(
locker, "_sick_mode_used_today", MagicMock(return_value=True)
)
text, color = locker._get_sick_day_status()
assert "already adjusted" in text
assert color == "#ffaa00"
def test_adjustment_success(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test status when shutdown time adjusted successfully."""
locker = create_locker(mock_tk, tmp_path)
object.__setattr__(
locker, "_sick_mode_used_today", MagicMock(return_value=False)
)
object.__setattr__(
locker, "_adjust_shutdown_time_earlier", MagicMock(return_value=True)
)
text, color = locker._get_sick_day_status()
assert "earlier" in text
assert color == "#00aa00"
def test_adjustment_failure(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test status when adjustment fails."""
locker = create_locker(mock_tk, tmp_path)
object.__setattr__(
locker, "_sick_mode_used_today", MagicMock(return_value=False)
)
object.__setattr__(
locker, "_adjust_shutdown_time_earlier", MagicMock(return_value=False)
)
text, color = locker._get_sick_day_status()
assert "Could not adjust" in text
assert color == "#ff4444"

View File

@ -0,0 +1,47 @@
"""Tests for handle_sick_day and sick day UI."""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
from python_pkg.screen_locker.screen_lock import (
SICK_LOCKOUT_SECONDS,
)
from python_pkg.screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
class TestHandleSickDay:
"""Tests for handle_sick_day method."""
def test_sets_up_countdown(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test handle_sick_day initializes sick day flow."""
locker = create_locker(mock_tk, tmp_path)
object.__setattr__(locker, "clear_container", MagicMock())
object.__setattr__(
locker, "_sick_mode_used_today", MagicMock(return_value=False)
)
object.__setattr__(
locker, "_adjust_shutdown_time_earlier", MagicMock(return_value=True)
)
locker.handle_sick_day()
locker.clear_container.assert_called_once()
assert locker.sick_remaining_time == SICK_LOCKOUT_SECONDS - 1
class TestShowSickDayUi:
"""Tests for _show_sick_day_ui method."""
def test_displays_ui(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test _show_sick_day_ui displays labels."""
locker = create_locker(mock_tk, tmp_path)
locker._show_sick_day_ui("Test status", "#00aa00")
mock_tk.Label.assert_called()
assert hasattr(locker, "sick_countdown_label")

View File

@ -0,0 +1,35 @@
"""Tests for UI flows coverage gaps (part 2)."""
from __future__ import annotations
from typing import TYPE_CHECKING
from unittest.mock import MagicMock
from python_pkg.screen_locker.tests.conftest import create_locker
if TYPE_CHECKING:
from pathlib import Path
class TestUpdateSickCountdownAtZero:
"""Tests for _update_sick_countdown at zero remaining."""
def test_records_sick_day_and_unlocks_at_zero(
self,
mock_tk: MagicMock,
_mock_sys_exit: MagicMock,
tmp_path: Path,
) -> None:
"""Test countdown at zero records sick day and calls unlock."""
locker = create_locker(mock_tk, tmp_path)
locker.sick_remaining_time = 0
locker.sick_countdown_label = MagicMock()
locker.workout_data = {}
locker.log_file = tmp_path / "workout_log.json"
object.__setattr__(locker, "unlock_screen", MagicMock())
locker._update_sick_countdown()
assert locker.workout_data["type"] == "sick_day"
assert locker.workout_data["note"] == "Sick day - shutdown moved earlier"
locker.unlock_screen.assert_called_once()

View File

@ -369,3 +369,37 @@ class TestVerifyStrengthData:
locker.show_error.assert_called_once() locker.show_error.assert_called_once()
assert "valid data" in locker.show_error.call_args[0][0] assert "valid data" in locker.show_error.call_args[0][0]
class TestVariableReps:
"""Tests for variable reps format in strength verification."""
def test_valid_variable_reps(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test valid variable reps with + separator."""
locker = create_locker(mock_tk, tmp_path)
# 3 sets, reps 12+11+12 (3 variable values matching 3 sets), weight 50
# Total = (12+11+12) * 50 = 1750
setup_strength_entries(
locker, StrengthData("Squat", "3", "12+11+12", "50", "1750")
)
locker.log_file = tmp_path / "workout_log.json"
locker.workout_data = {"type": "strength"}
object.__setattr__(locker, "_attempt_unlock", MagicMock())
locker.verify_strength_data()
locker._attempt_unlock.assert_called_once()
def test_variable_reps_count_mismatch(
self, mock_tk: MagicMock, _mock_sys_exit: MagicMock, tmp_path: Path
) -> None:
"""Test variable reps count not matching sets."""
locker = create_locker(mock_tk, tmp_path)
# 5 sets but only 3 variable reps
setup_strength_entries(
locker, StrengthData("Squat", "5", "12+11+12", "50", "1750")
)
object.__setattr__(locker, "show_error", MagicMock())
locker.verify_strength_data()
locker.show_error.assert_called_once()
assert "variable reps count" in locker.show_error.call_args[0][0]