testsAndMisc/python_pkg/wallpaper_rotator/tests/test_style_data.py
Krzysztof kuhy Rudnicki d502580e3f Dark wallpaper rotator: new style every 3 h
Six text-free styles (gradient, flow/Voronoi, habit heatmap, shutdown
clock, Earth day/night, plasma) replace the white i3 root. A 15-minute
systemd user timer picks a new random style per monitor every 3 hours,
unlike its previous one and the other monitor's; live styles refresh in
between. Also syncs i3/config with the live config and prunes the
oldest evidence artifact for the 60-file cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WzxdWX51FMD58yKUC1eYWf
2026-11-11 16:31:06 +01:00

126 lines
4.5 KiB
Python

"""Branches of the data-driven styles: clock conf, habit ledgers, Earth maps."""
from __future__ import annotations
from datetime import UTC, date, datetime
import json
from typing import TYPE_CHECKING
import numpy as np
from PIL import Image
from python_pkg.wallpaper_rotator import _style_clock as _clock
from python_pkg.wallpaper_rotator import _style_earth as _earth
from python_pkg.wallpaper_rotator import _style_heatmap as _heatmap
from python_pkg.wallpaper_rotator._context import RenderContext
if TYPE_CHECKING:
from pathlib import Path
def _ctx(tmp_path: Path, now: datetime, conf: Path | None = None) -> RenderContext:
return RenderContext(
now=now,
data_home=tmp_path / "share",
earth_dir=tmp_path / "earth",
shutdown_conf=conf or tmp_path / "missing.conf",
)
def test_schedule_defaults_when_conf_is_missing(tmp_path: Path) -> None:
assert _clock.schedule(tmp_path / "nope.conf", 0) == (300, 1380)
def test_schedule_reads_numbers_and_skips_junk_lines(tmp_path: Path) -> None:
conf = tmp_path / "s.conf"
conf.write_text(
"no equals sign\n"
"MON_WED_MINUTES=1200\n"
"THU_SUN_MINUTES = 1300 \n"
"MORNING_END_MINUTES=abc\n",
encoding="utf-8",
)
assert _clock.schedule(conf, 2) == (300, 1200)
assert _clock.schedule(conf, 3) == (300, 1300)
def test_clock_after_shutdown_draws_only_the_dim_ring(tmp_path: Path) -> None:
late = _ctx(tmp_path, datetime(2026, 11, 11, 23, 30, tzinfo=UTC))
image = _clock.render(late, (200, 120), np.random.default_rng(0))
assert image.size == (200, 120)
def test_clock_tone_is_calmer_hours_before_shutdown(tmp_path: Path) -> None:
early = _ctx(tmp_path, datetime(2026, 11, 11, 8, 0, tzinfo=UTC))
late = _ctx(tmp_path, datetime(2026, 11, 11, 22, 30, tzinfo=UTC))
rng = np.random.default_rng(0)
corner_early = _clock.render(early, (200, 120), rng).getpixel((100, 5))
corner_late = _clock.render(late, (200, 120), rng).getpixel((100, 5))
assert corner_early != corner_late
def _ledger(tmp_path: Path, text: str) -> Path:
path = tmp_path / "ledger.json"
path.write_text(text, encoding="utf-8")
return path
def test_credit_days_counts_real_credits_per_day(tmp_path: Path) -> None:
entries = [
{"kind": "credit", "amount": 5, "day": "2026-11-10"},
{"kind": "credit", "day": "2026-11-10"},
{"kind": "credit", "amount": 1, "day": "2026-11-09"},
]
days = _heatmap.credit_days(_ledger(tmp_path, json.dumps({"entries": entries})))
assert days == {date(2026, 11, 10): 2, date(2026, 11, 9): 1}
def test_credit_days_ignores_unusable_rows(tmp_path: Path) -> None:
entries = [
"not a dict",
{"kind": "debit", "amount": 3, "day": "2026-11-10"},
{"kind": "credit", "amount": 0, "day": "2026-11-10"},
{"kind": "credit", "amount": 1, "day": "yesterday"},
{"kind": "credit", "amount": 1},
]
days = _heatmap.credit_days(_ledger(tmp_path, json.dumps({"entries": entries})))
assert not days
def test_credit_days_survives_a_broken_ledger(tmp_path: Path) -> None:
assert not _heatmap.credit_days(tmp_path / "missing.json")
assert not _heatmap.credit_days(_ledger(tmp_path, "{not json"))
assert not _heatmap.credit_days(_ledger(tmp_path, "[1, 2]"))
assert not _heatmap.credit_days(_ledger(tmp_path, "{}"))
def test_heatmap_lights_cells_for_credit_days(tmp_path: Path) -> None:
now = datetime(2026, 11, 11, 12, 0, tzinfo=UTC)
ctx = _ctx(tmp_path, now)
folder = ctx.data_home / "workout_locker"
folder.mkdir(parents=True)
rows = [{"kind": "credit", "amount": 1, "day": "2026-11-11"}] * 3
(folder / "ledger.json").write_text(json.dumps({"entries": rows}), encoding="utf-8")
lit = _heatmap.render(ctx, (800, 480), np.random.default_rng(0))
blank = _heatmap.render(
_ctx(tmp_path / "x", now), (800, 480), np.random.default_rng(0)
)
assert lit.tobytes() != blank.tobytes()
def test_earth_available_needs_both_images(tmp_path: Path) -> None:
ctx = _ctx(tmp_path, datetime(2026, 11, 11, tzinfo=UTC))
assert not _earth.available(ctx)
ctx.earth_dir.mkdir()
Image.new("RGB", (4, 2)).save(ctx.earth_dir / "day.jpg")
assert not _earth.available(ctx)
Image.new("RGB", (4, 2)).save(ctx.earth_dir / "night.jpg")
assert _earth.available(ctx)
def test_sun_is_over_the_greenwich_meridian_at_noon_utc(tmp_path: Path) -> None:
ctx = _ctx(tmp_path, datetime(2026, 3, 20, 12, 0, tzinfo=UTC))
lat, lon = _earth.sun_position(ctx)
assert lon == 0.0
assert abs(lat) < np.radians(24)