diet-guard/diet_guard/tests/test_gatelock_body.py
Krzysztof kuhy Rudnicki 43e4b4070d Replace PC gate text with glyphs
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-09 15:35:34 +01:00

162 lines
5.7 KiB
Python

"""The gate's Body tab: its handlers, input parsing and weight graph.
Built on the in-memory Tk fakes (``fake_tk``), so no window ever opens.
"""
from __future__ import annotations
from datetime import date
from typing import TYPE_CHECKING
from unittest.mock import patch
import pytest
from diet_guard import _gatelock_body
from diet_guard import _gatelock_glyphs as g
from diet_guard._body_fat_store import body_fats
from diet_guard._body_prefs import saved_goal
from diet_guard._body_store import profile, set_profile, set_weight, weights
from diet_guard._budget import read_raw_record, write_budget
from diet_guard._gatelock_body import BodyTab
from diet_guard._gatelock_body_graph import (
GraphBox,
draw_weight_graph,
graph_points,
in_range,
value_span,
)
from diet_guard._gatelock_body_input import parse_day, parse_float
from diet_guard.tests._gate_fixtures import fake_tk
from diet_guard.tests._tk_fakes import FakeCanvas, FakeNotebook
if TYPE_CHECKING:
from collections.abc import Iterator
@pytest.fixture
def tab() -> Iterator[BodyTab]:
with (
fake_tk(),
patch.object(_gatelock_body, "publish_after_log_detached") as publish,
):
body = BodyTab(FakeNotebook())
body.publish = publish # the test can assert it
yield body
def _person() -> None:
set_profile(birth="2000-05-22", height_cm=169.0, sex="m")
set_weight("2026-09-27", 72.6)
class TestInput:
def test_parsing(self) -> None:
assert parse_float(" 72,6 ") == 72.6
assert parse_float("x") is None
assert parse_day(" 2026-09-28") == "2026-09-28"
assert parse_day("today") is None
class TestProfile:
def test_rejects_then_saves(self, tab: BodyTab) -> None:
tab.vars.profile.height.set("9")
tab.save_profile()
assert tab.vars.status.get().startswith(g.ERROR)
tab.vars.profile.birth.set("2000-05-22")
tab.vars.profile.height.set("169")
tab.vars.profile.sex.set("m")
tab.save_profile()
assert profile().complete
assert tab.vars.status.get() == g.OK
def test_complete_profile_loads_into_the_inputs(self) -> None:
_person()
with fake_tk():
body = BodyTab(FakeNotebook())
assert body.vars.profile.height.get() == "169"
assert body.vars.goal.answer.get().startswith("→ ")
class TestWeights:
def test_save_reject_delete(self, tab: BodyTab) -> None:
write_budget(2000, weight_kg=90.0)
tab.vars.logs.weight.set("5")
tab.save_weight()
assert tab.vars.status.get().startswith(g.ERROR)
tab.vars.logs.weight.set("80")
tab.vars.logs.weight_day.set("2026-09-10")
tab.save_weight()
assert weights() == {"2026-09-10": 80.0}
assert (read_raw_record() or {})["w"] == 80.0
tab.vars.logs.weight_day.set("nope")
tab.delete_weight()
assert tab.vars.status.get() == g.ERROR + " " + g.DELETE + " 0000-00-00"
tab.vars.logs.weight_day.set("2026-09-10")
tab.delete_weight()
assert weights() == {}
def test_body_fat_save_reject_delete(self, tab: BodyTab) -> None:
tab.vars.logs.body_fat.set("90")
tab.save_body_fat()
assert tab.vars.status.get().startswith(g.ERROR)
tab.vars.logs.body_fat.set("17")
tab.save_body_fat()
assert body_fats() == {tab.vars.logs.fat_day.get(): 17.0}
tab.vars.logs.fat_day.set("x")
tab.delete_body_fat()
assert tab.vars.status.get() == g.ERROR + " " + g.DELETE + " 0000-00-00"
tab.vars.logs.fat_day.set(next(iter(body_fats())))
tab.delete_body_fat()
assert body_fats() == {}
class TestGoalAndGraph:
def test_goal_change_saves_and_reanswers(self, tab: BodyTab) -> None:
_person()
tab.vars.goal.weight_unit.set("lb")
tab.vars.goal.amount.set("oops")
tab.goal_changed()
assert saved_goal().weight_unit == "lb"
assert saved_goal().amount == 0.5 # unreadable amount kept
assert tab.vars.goal.amount.get() == "0.5"
assert tab.vars.goal.answer.get().startswith("→ ")
def test_range_and_body_fat_graph(self, tab: BodyTab) -> None:
_person()
tab.vars.plan.days[0].set("2500")
tab.vars.plan.target.set("")
tab.set_range(None)
tab.vars.logs.graph.set("fat")
tab.refresh()
assert f"2500 {g.EDIT}" in tab.vars.blocks["plan"].get()
class TestGraph:
def test_in_range_and_span(self) -> None:
log = {"2026-01-01": 80.0, "2026-09-27": 72.0}
assert in_range(log, date(2026, 9, 28), None) == log
assert in_range(log, date(2026, 9, 28), 30) == {"2026-09-27": 72.0}
low, high = value_span([72.0], (50.0, 71.0))
assert low < 71.0 < high
low, high = value_span([72.0], (40.0, 45.0))
assert low > 45.0
def test_single_point_is_centred(self) -> None:
box = GraphBox(700, 300)
((x, _),) = graph_points({"2026-09-27": 72.0}, box, (71.0, 73.0))
assert x == pytest.approx(box.x(0.5))
def test_draws_band_line_and_labels_or_a_hint(self) -> None:
canvas = FakeCanvas()
box = GraphBox(700, 300)
draw_weight_graph(canvas, {}, box, None)
assert canvas.drawn[0][0] == "text"
assert canvas.drawn[0][2]["text"] == g.EMPTY
log = {"2026-09-20": 74.0, "2026-09-27": 72.0}
draw_weight_graph(canvas, log, box, (52.8, 71.1))
kinds = [kind for kind, _, _ in canvas.drawn]
assert "rectangle" in kinds
assert kinds.count("oval") == 2
draw_weight_graph(canvas, log, box, (10.0, 20.0)) # band far off-axis
assert "rectangle" not in [kind for kind, _, _ in canvas.drawn]