mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 15:33:57 +01:00
Ninth batch of the 250-line sweep. Every Python file in the repo is now under cap (over-cap count 25 -> 18; the 18 remaining are all Dart). _foodbank.py 365 -> 210 (+ _foodbank_search, _foodbank_rebuild) test_averages.py 351 -> 222 (+ test_averages_report.py) _estimator.py 325 -> 137 (+ _estimator_off.py) _budget.py 323 -> 227 (+ _budget_biometrics, _budget_derived) test_foodbank.py 311 -> 117 (+ test_foodbank_rebuild.py) _averages.py 298 -> 174 (+ _averages_periods, _averages_report) test_budget_history.py 288 -> 162 (+ test_budget_history_io.py) The split boundaries are constrained by which module owns each conftest-patched state path, not by line count. BUDGET_FILE stays in _budget, FOOD_BANK_FILE in _foodbank: the derived/search/rebuild halves reach the file only through their parent's private helpers. Two of the new modules had copied FOOD_BANK_FILE into their headers as an incidental import, which is exactly the leak the guard is for -- both cleaned. test_state_redirect.py now covers all five redirected paths (FOOD_LOG_FILE, BUDGET_FILE, BUDGET_HISTORY_FILE, FOOD_BANK_FILE, MANUAL_BANK_FILE) rather than just the food log, in both directions: only the owning module may name each constant, and each redirect demonstrably moves a real write off the live path. Two rewires fail-closed rather than silently: `_budget.budget_weight` and `_estimator.requests` were module-attribute references no import rewrite touches, and both raised AttributeError immediately. The latter appeared only after ruff removed the now-unused `requests` import from _estimator -- the import was load-bearing solely as a patch target. Verified: 707 Python tests / 100% branch coverage, 5 layout-fits tests under xvfb, check_patch_targets 0, pre-commit clean, ~/.local/share/diet_guard byte-identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RA9ynjqXPnexRQnR5oAuCC
164 lines
5.4 KiB
Python
164 lines
5.4 KiB
Python
"""Tests for diet_guard._averages: period edges, bands, and coverage counts."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date, timedelta
|
|
|
|
from diet_guard import _averages_report
|
|
from diet_guard._averages import AverageBand
|
|
from diet_guard._budget_history import EPOCH_DAY, BudgetEntry, BudgetSchedule
|
|
|
|
|
|
def _entry(kcal: float) -> dict[str, object]:
|
|
return {"kcal": kcal}
|
|
|
|
|
|
def _flat(budget: int) -> BudgetSchedule:
|
|
"""A schedule where one budget has applied since the beginning of time."""
|
|
return BudgetSchedule(
|
|
(BudgetEntry(EPOCH_DAY, budget, "1970-01-01T00:00:00+00:00"),),
|
|
default=budget,
|
|
)
|
|
|
|
|
|
def _days(start: str, kcals: list[float]) -> dict[str, list[dict[str, object]]]:
|
|
"""A log of consecutive days from ``start``, one entry each."""
|
|
first = date.fromisoformat(start)
|
|
return {
|
|
(first + timedelta(days=offset)).isoformat(): [_entry(kcal)]
|
|
for offset, kcal in enumerate(kcals)
|
|
}
|
|
|
|
|
|
class TestWeeklyAverage:
|
|
def test_current_week_stops_at_yesterday(self) -> None:
|
|
# Today is Thursday 2026-01-08 and today's 100 kcal must NOT count:
|
|
# the mean is over Mon-Wed only.
|
|
log = _days("2026-01-05", [3000, 3000, 3000]) | {"2026-01-08": [_entry(100)]}
|
|
result = _averages_report.weekly_average(
|
|
log,
|
|
schedule=_flat(2000),
|
|
today=date(2026, 1, 8),
|
|
)
|
|
assert result.avg_kcal == 3000
|
|
assert result.end == "2026-01-07"
|
|
assert result.band is AverageBand.VERY_OVER
|
|
|
|
def test_monday_has_no_complete_days_this_week(self) -> None:
|
|
log = _days("2026-01-05", [3000])
|
|
result = _averages_report.weekly_average(
|
|
log,
|
|
schedule=_flat(2000),
|
|
today=date(2026, 1, 5),
|
|
)
|
|
assert result.elapsed_days == 0
|
|
assert result.avg_kcal is None
|
|
assert result.band is None
|
|
|
|
def test_previous_week_is_whole(self) -> None:
|
|
log = _days("2026-01-05", [2100, 2100, 2100, 2100, 2100, 2100, 2100])
|
|
result = _averages_report.weekly_average(
|
|
log,
|
|
schedule=_flat(2000),
|
|
weeks_ago=1,
|
|
today=date(2026, 1, 14),
|
|
)
|
|
assert (result.start, result.end) == ("2026-01-05", "2026-01-11")
|
|
assert result.elapsed_days == 7
|
|
assert result.logged_days == 7
|
|
assert result.band is AverageBand.SLIGHTLY_OVER
|
|
|
|
def test_defaults_to_the_real_today(self) -> None:
|
|
# No explicit `today`: the call must still resolve a real date range
|
|
# rather than raising or returning an unbounded period.
|
|
result = _averages_report.weekly_average({}, schedule=_flat(2000))
|
|
assert result.elapsed_days >= 0
|
|
|
|
|
|
class TestMonthlyAverage:
|
|
def test_current_month_stops_at_yesterday(self) -> None:
|
|
log = _days("2026-03-01", [1000, 1000, 1000]) | {"2026-03-04": [_entry(9000)]}
|
|
result = _averages_report.monthly_average(
|
|
log,
|
|
schedule=_flat(2000),
|
|
today=date(2026, 3, 4),
|
|
)
|
|
assert result.avg_kcal == 1000
|
|
assert (result.start, result.end) == ("2026-03-01", "2026-03-03")
|
|
assert result.band is AverageBand.UNDER
|
|
|
|
def test_first_of_the_month_has_no_complete_days(self) -> None:
|
|
result = _averages_report.monthly_average(
|
|
{},
|
|
schedule=_flat(2000),
|
|
today=date(2026, 3, 1),
|
|
)
|
|
assert result.elapsed_days == 0
|
|
assert result.avg_kcal is None
|
|
|
|
def test_previous_month_is_whole(self) -> None:
|
|
log = _days("2026-02-01", [2500, 2500])
|
|
result = _averages_report.monthly_average(
|
|
log,
|
|
schedule=_flat(2000),
|
|
months_ago=1,
|
|
today=date(2026, 3, 10),
|
|
)
|
|
assert (result.start, result.end) == ("2026-02-01", "2026-02-28")
|
|
assert result.elapsed_days == 28
|
|
assert result.logged_days == 2
|
|
assert result.band is AverageBand.VERY_OVER
|
|
|
|
def test_defaults_to_the_real_today(self) -> None:
|
|
result = _averages_report.monthly_average({}, schedule=_flat(2000))
|
|
assert result.elapsed_days >= 0
|
|
|
|
|
|
class TestRecentPeriods:
|
|
def test_recent_weeks_are_newest_first(self) -> None:
|
|
weeks = _averages_report.recent_weeks(
|
|
{},
|
|
schedule=_flat(2000),
|
|
count=3,
|
|
today=date(2026, 1, 14),
|
|
)
|
|
assert [week.start for week in weeks] == [
|
|
"2026-01-12",
|
|
"2026-01-05",
|
|
"2025-12-29",
|
|
]
|
|
|
|
def test_recent_months_are_newest_first(self) -> None:
|
|
months = _averages_report.recent_months(
|
|
{},
|
|
schedule=_flat(2000),
|
|
count=3,
|
|
today=date(2026, 1, 14),
|
|
)
|
|
assert [month.start for month in months] == [
|
|
"2026-01-01",
|
|
"2025-12-01",
|
|
"2025-11-01",
|
|
]
|
|
|
|
def test_zero_count_is_empty(self) -> None:
|
|
assert (
|
|
_averages_report.recent_weeks(
|
|
{},
|
|
schedule=_flat(2000),
|
|
count=0,
|
|
today=date(2026, 1, 14),
|
|
)
|
|
== ()
|
|
)
|
|
|
|
def test_recent_weeks_defaults_to_the_real_today(self) -> None:
|
|
assert (
|
|
len(_averages_report.recent_weeks({}, schedule=_flat(2000), count=2)) == 2
|
|
)
|
|
|
|
def test_recent_months_defaults_to_the_real_today(self) -> None:
|
|
assert (
|
|
len(_averages_report.recent_months({}, schedule=_flat(2000), count=2)) == 2
|
|
)
|