diet-guard/diet_guard/tests/test_averages_report.py
Krzysztof kuhy Rudnicki 3e51e614b1 Split the last seven Python files; extend the redirect guard to all five paths
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
2026-08-15 20:37:05 +02:00

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
)