106 lines
3.5 KiB
Python
106 lines
3.5 KiB
Python
"""Tests for `write_config` — TOML serialiser used by the first-run wizard.
|
|
|
|
The load-bearing invariant: write_config → load_config produces an equivalent
|
|
Config. If that holds, the wizard can write whatever the user picked and
|
|
trust the rest of the codebase to read it back the same way.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
from openrun.config import (
|
|
BanisterParams,
|
|
Config,
|
|
HRZones,
|
|
UserProfile,
|
|
load_config,
|
|
write_config,
|
|
)
|
|
|
|
|
|
def _sample_config(tmp_path: Path) -> Config:
|
|
return Config(
|
|
user=UserProfile(
|
|
name="Test Runner",
|
|
height_cm=180.0,
|
|
weight_kg=72.5,
|
|
hr_max=200,
|
|
lthr=178,
|
|
resting_hr=48,
|
|
zones=HRZones(
|
|
z1=(100, 120), z2=(121, 140), z3=(141, 160),
|
|
z4=(161, 180), z5=(181, 200),
|
|
),
|
|
),
|
|
db_path=tmp_path / "data" / "garmin.db",
|
|
banister=BanisterParams(ctl_tau_days=42.0, atl_tau_days=7.0, race_day_tl_per_km=7.0),
|
|
races=(("wk 4 — 30K", "2026-06-13"), ("wk 17 — 50 mile", "2026-09-12")),
|
|
)
|
|
|
|
|
|
def test_write_then_load_roundtrip(tmp_path: Path) -> None:
|
|
cfg = _sample_config(tmp_path)
|
|
out = tmp_path / "openrun.toml"
|
|
write_config(cfg, out)
|
|
|
|
loaded = load_config(out)
|
|
assert loaded.user.name == cfg.user.name
|
|
assert loaded.user.hr_max == cfg.user.hr_max
|
|
assert loaded.user.lthr == cfg.user.lthr
|
|
assert loaded.user.resting_hr == cfg.user.resting_hr
|
|
assert loaded.user.weight_kg == cfg.user.weight_kg
|
|
assert loaded.user.height_cm == cfg.user.height_cm
|
|
assert loaded.user.zones == cfg.user.zones
|
|
assert loaded.banister == cfg.banister
|
|
assert loaded.races == cfg.races
|
|
|
|
|
|
def test_write_handles_missing_optional_fields(tmp_path: Path) -> None:
|
|
"""Height/weight/lthr/rhr are optional — omitted entirely from output."""
|
|
cfg = Config(
|
|
user=UserProfile(name="Minimal", hr_max=190),
|
|
db_path=tmp_path / "minimal.db",
|
|
)
|
|
out = tmp_path / "openrun.toml"
|
|
write_config(cfg, out)
|
|
body = out.read_text()
|
|
assert "height_cm" not in body
|
|
assert "weight_kg" not in body
|
|
assert "lthr" not in body
|
|
assert "resting_hr" not in body
|
|
|
|
loaded = load_config(out)
|
|
assert loaded.user.hr_max == 190
|
|
assert loaded.user.height_cm is None
|
|
|
|
|
|
def test_write_quotes_strings_with_special_chars(tmp_path: Path) -> None:
|
|
"""Race labels and athlete names commonly contain Unicode em-dashes and quotes;
|
|
the serialiser must escape these so the file parses back."""
|
|
cfg = Config(
|
|
user=UserProfile(name='O\'Brien — "Speedy"', hr_max=200),
|
|
db_path=tmp_path / "x.db",
|
|
races=(('wk 17 — Cascade Crest "50M"', "2026-09-12"),),
|
|
)
|
|
out = tmp_path / "openrun.toml"
|
|
write_config(cfg, out)
|
|
loaded = load_config(out)
|
|
assert loaded.user.name == 'O\'Brien — "Speedy"'
|
|
assert loaded.races == (('wk 17 — Cascade Crest "50M"', "2026-09-12"),)
|
|
|
|
|
|
def test_write_creates_parent_dir(tmp_path: Path) -> None:
|
|
cfg = Config(user=UserProfile(name="x", hr_max=200), db_path=tmp_path / "x.db")
|
|
nested = tmp_path / "a" / "b" / "openrun.toml"
|
|
write_config(cfg, nested)
|
|
assert nested.exists()
|
|
|
|
|
|
def test_no_races_omits_section(tmp_path: Path) -> None:
|
|
cfg = Config(user=UserProfile(name="x", hr_max=200), db_path=tmp_path / "x.db", races=())
|
|
out = tmp_path / "openrun.toml"
|
|
write_config(cfg, out)
|
|
assert "[[races]]" not in out.read_text()
|
|
assert load_config(out).races == ()
|