mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 22:54:00 +01:00
`sickdays gui` opens a Tk window modelled on the phone's Sick days screen: a Monday-first month calendar (cat-2 fill, a dot for kept lockers, past days faded and inert), a five-switch panel for the clicked day, and the "Marked sick" list with "Kept: ..." lines. A free day is refused with the phone's wording. Each write starts sync_quietly() on a non-daemon thread. The window class is "sickdays" and Escape closes it. sickdays.gui.KeepPanel is the panel on its own, a plain tk.Frame with no root, mainloop, grab or wm_* call, so screen-locker can embed it under its lock-screen grab. `import sickdays` and the rest of the CLI never import tkinter: the CLI loads the gui module by name only for `gui`, and a subprocess test checks this. The tests drive real Tk on a withdrawn root, and CI now runs them under xvfb-run, as gatelock's CI does. Adds a sickdays.desktop launcher, which install.sh installs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
90 lines
2.7 KiB
Python
90 lines
2.7 KiB
Python
# Copyright (c) 2026 Krzysztof Rudnicki
|
|
"""Redirect every on-disk path, and the free-day pool, away from real state.
|
|
|
|
Every writer resolves its paths through :func:`sickdays._paths.resolve_paths`,
|
|
so redirecting that one function in each module that imports it moves all of
|
|
them, including any file added later.
|
|
|
|
The GUI modules are in the list too, and their background sync is stubbed:
|
|
a test thread must never find the real crdt-sync credential.
|
|
|
|
The cross-pool check would otherwise import the real freedays and read the
|
|
real ``~/.local/share/freedays`` pool, so it is stubbed to "nothing is free"
|
|
for every test; tests of the check itself pass their own predicate.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
import pytest
|
|
|
|
from sickdays._paths import Paths
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Iterator
|
|
from datetime import date
|
|
from pathlib import Path
|
|
import tkinter as tk
|
|
|
|
_MODULES = (
|
|
"sickdays._api",
|
|
"sickdays._audit",
|
|
"sickdays._conflict",
|
|
"sickdays._keep_api",
|
|
"sickdays._sync",
|
|
"sickdays.gui._panel",
|
|
"sickdays.gui._window",
|
|
)
|
|
|
|
|
|
def _never_free(_day: date) -> bool:
|
|
return False
|
|
|
|
|
|
def _no_sync(*, paths: Paths | None = None) -> bool:
|
|
"""The GUI's background sync, stubbed: no test may reach Firebase."""
|
|
del paths
|
|
return False
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _redirect_state(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
"""Point every default path at ``tmp_path`` before any test runs."""
|
|
state = tmp_path / "state"
|
|
state.mkdir()
|
|
redirected = Paths.under(state)
|
|
|
|
def _resolve(paths: Paths | None) -> Paths:
|
|
return paths if paths is not None else redirected
|
|
|
|
for module in _MODULES:
|
|
monkeypatch.setattr(f"{module}.resolve_paths", _resolve)
|
|
monkeypatch.setattr("sickdays._api.free_day_predicate", lambda: _never_free)
|
|
monkeypatch.setattr("sickdays.gui._text.free_day_predicate", lambda: _never_free)
|
|
monkeypatch.setattr("sickdays.gui._panel.sync_quietly", _no_sync)
|
|
|
|
|
|
@pytest.fixture
|
|
def state_dir(tmp_path: Path) -> Path:
|
|
"""The redirected state directory, for tests that assert on files."""
|
|
return tmp_path / "state"
|
|
|
|
|
|
@pytest.fixture
|
|
def tk_root() -> Iterator[tk.Tk]:
|
|
"""A real, never-mapped Tk root; skips when there is no X display.
|
|
|
|
Real Tk rather than a fake: option names and Checkbutton/variable wiring
|
|
are Tk's own behaviour. CI runs the suite under ``xvfb-run``.
|
|
"""
|
|
import tkinter as tk
|
|
|
|
try:
|
|
root = tk.Tk(className="sickdays-test")
|
|
except tk.TclError as exc:
|
|
pytest.skip(f"no X display ({exc}); run the suite under xvfb-run")
|
|
root.withdraw()
|
|
yield root
|
|
root.destroy()
|