Files
Stride/tests/test_fitness.py
T

127 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import UTC, date, datetime
from math import exp
from types import SimpleNamespace
from test_app import login
from app.extensions import db
from app.fitness import effort, history
from app.heart_rate import zones
from app.models import Activity, User
pytest_plugins = ("test_app",)
def run(seconds=600, histogram=None):
return SimpleNamespace(
sport="running",
duration_s=seconds,
analysis_summary={"intervals": [{"hr_seconds": histogram or {}}]},
)
def test_effort_boundaries_weights_and_coverage():
scored = effort(run(histogram={"120": 200, "140": 200, "160": 200}), [140, 160])
assert scored["score"] == 20
assert scored["coverage"] == 100
assert effort(run(histogram={"140": 479}), [140, 160])["score"] is None
assert effort(run(histogram={"140": 480}), [140, 160])["score"] == 16
assert effort(run(histogram={"140": 600}), None)["score"] is None
assert effort(run(histogram={"140": 600}), None)["coverage"] == 100
assert effort(run(histogram={"250": 600}), [140, 160])["score"] is None
five = effort(
run(histogram={"121": 120, "122": 120, "152": 120, "167": 120, "181": 120}),
[122, 152, 167, 181],
)
assert five["score"] == 30
assert five["zone_minutes"] == [2, 2, 2, 2, 2]
def test_automatic_zones_update_on_birthday_manual_overrides():
user = SimpleNamespace(
timezone="Australia/Sydney", hr_zones={"mode": "auto", "birthday": "1990-10-22"}
)
assert zones(user, date(2026, 10, 21))["max_hr"] == 185
assert zones(user, date(2026, 10, 22))["max_hr"] == 184
user.hr_zones = {"mode": "max", "max_hr": 195, "birthday": "1990-10-22"}
assert zones(user, date(2026, 10, 22))["max_hr"] == 195
user.hr_zones = None
assert zones(user)["max_hr"] == 190
user.hr_zones = {"mode": "custom", "boundaries": [122, 152, 167, 181]}
assert zones(user)["labels"] == ["0–121", "122–151", "152–166", "167–180", "181+"]
def test_history_local_days_sum_decay_missing_and_owner_scope(app):
with app.app_context():
db.session.delete(db.session.get(Activity, 1))
user = db.session.get(User, 1)
user.hr_zones = {"mode": "custom", "boundaries": [140, 160, 180, 200]}
for key, owner, hour, histogram in [
("a", 1, 13, {"140": 600}),
("b", 1, 14, {"140": 600}),
("other", 2, 14, {"160": 600}),
("missing", 1, 15, {}),
]:
row = Activity(
owner_id=owner,
source_key=key,
title=key,
sport="running",
started_at=datetime(2026, 1, 1, hour, tzinfo=UTC),
duration_s=600,
analysis_summary=run(histogram=histogram).analysis_summary,
)
db.session.add(row)
db.session.commit()
data = history(user, date(2026, 1, 3))
assert len(data["runs"]) == 3
assert data["series"][0]["day"] == "2026-01-02"
first, second = data["series"]
assert first["load"] is None
assert first["missing"] == 1
assert first["fitness"] == round(40 * (1 - exp(-1 / 42)), 2)
assert first["fatigue"] == round(40 * (1 - exp(-1 / 7)), 2)
assert first["form"] == round(40 * (exp(-1 / 7) - exp(-1 / 42)), 2)
assert second["load"] == 0
assert second["fitness"] == round(40 * (1 - exp(-1 / 42)) * exp(-1 / 42), 2)
assert second["provisional"]
assert history(user, date(2026, 1, 3)) == data
user.hr_zones = {"mode": "custom", "boundaries": [160, 180, 200, 220]}
recalculated = history(user, date(2026, 1, 3))
assert recalculated["series"][0]["fitness"] == round(20 * (1 - exp(-1 / 42)), 2)
assert history(user, date(2026, 1, 1))["series"] == []
def test_fitness_page_private_and_renders_history(app):
client = app.test_client()
assert client.get("/fitness").status_code == 302
assert client.get("/fitness/data").status_code == 302
login(client)
response = client.get("/fitness")
assert response.status_code == 200
assert len(client.get("/fitness/data").json["runs"]) == 1
assert b"A quiet morning run" in response.data
with app.app_context():
db.session.get(User, 1).hr_bands = [140, 160]
db.session.commit()
assert b"fitness-chart" in client.get("/fitness").data
assert b"Run effort:" in client.get("/activities/1").data
def test_zone_settings_validation_persistence_and_privacy(app):
client = app.test_client()
assert client.post("/training/zones", data={"mode": "auto"}).status_code == 302
login(client)
valid = {"mode": "custom", "z2": "122", "z3": "152", "z4": "167", "z5": "181"}
assert client.post("/training/zones", data=valid).status_code == 302
assert client.post("/training/zones", data={**valid, "z3": "100"}).status_code == 400
assert client.post("/training/zones", data={"mode": "max", "max_hr": "999"}).status_code == 400
assert (
client.post("/training/zones", data={"mode": "auto", "birthday": "2099-01-01"}).status_code
== 400
)
with app.app_context():
assert db.session.get(User, 1).hr_zones["boundaries"] == [122, 152, 167, 181]
assert db.session.get(User, 2).hr_zones is None
assert b"Manual zone boundaries" in client.get("/settings").data