utils/sickdays/sickdays/tests/conftest.py
Krzysztof kuhy Rudnicki bd64b27109 sickdays 0.3.0: desktop sick-day picker and embeddable KeepPanel
`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
2026-10-17 13:47:26 +02:00

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()