Files
Stride/tests/test_stats.py
T

167 lines
5.9 KiB
Python

from datetime import UTC, datetime
import pytest
from sqlalchemy import event
from test_app import add_history, login
from app.extensions import db
from app.models import Activity
from app.stats import explore, parse_query, query_text
pytest_plugins = ("test_app",)
def test_query_round_trip_and_validation():
query = parse_query(
{
"metric": "cadence",
"secondary": "none",
"weeks": "26",
"minimum": "8",
"maximum": "20",
"hr_min": "140",
"hr_max": "150",
"end": "2026-10-02",
"compare": "no",
}
)
assert parse_query({"query": query_text(query)}) == query
tiny = parse_query({"minimum": "0.0000001"})
assert parse_query({"query": query_text(tiny)}) == tiny
for args in (
{"weeks": "105"},
{"query": ""},
{"minimum": "nan"},
{"minimum": "12", "maximum": "4"},
{"metric": "password"},
{"end": "2026-02-30"},
{"query": "runs\nwhere owner_id = 2"},
{"query": "runs\nsummarize pace\nby week\nby month"},
):
with pytest.raises(ValueError):
parse_query(args)
def test_weighting_missing_data_and_private_scope(app):
with app.app_context():
db.session.add_all(
[
Activity(
owner_id=1,
source_key="stats:long",
title="Long",
sport="running",
started_at=datetime(2026, 9, 29, tzinfo=UTC),
distance_m=15000,
duration_s=4500,
avg_hr=None,
),
Activity(
owner_id=2,
source_key="stats:private",
title="Secret",
sport="running",
started_at=datetime(2026, 9, 29, tzinfo=UTC),
distance_m=100000,
duration_s=10000,
avg_hr=200,
),
Activity(
owner_id=1,
source_key="stats:bike",
title="Bike",
sport="cycling",
started_at=datetime(2026, 9, 29, tzinfo=UTC),
distance_m=100000,
duration_s=10000,
),
]
)
db.session.commit()
statements = []
def record(_conn, _cursor, statement, _parameters, _context, _many):
statements.append(statement)
event.listen(db.engine, "before_cursor_execute", record)
try:
result = explore(1, "Australia/Sydney", parse_query({"end": "2026-10-02"}))
finally:
event.remove(db.engine, "before_cursor_execute", record)
assert result["count"] == 2
assert result["cards"][0]["value"] == "5:15"
assert result["cards"][1]["value"] == "140.0"
assert result["cards"][1]["coverage"] == 1
assert all("samples" not in sql and "route_geometry" not in sql for sql in statements)
def test_stats_routes_and_drillthrough(app):
client = app.test_client()
assert client.get("/stats").status_code == 302
assert client.get("/stats/runs").status_code == 302
login(client)
response = client.get("/stats?end=2026-10-02")
assert response.status_code == 200
assert b"A quiet morning run" in response.data
assert b"5:00" not in response.data # recorded pace is 6:00/km
response = client.get("/stats/runs?end=2026-10-02&bucket=28+Sep+2026")
assert response.status_code == 200
assert response.json["count"] == 1
assert response.json["rows"][0]["values"] == ["6:00", "140.0"]
assert client.get("/stats/runs?bucket=bad").status_code == 400
assert client.get("/stats?metric=unknown").status_code == 400
assert b"No runs match" in client.get("/stats?minimum=1000").data
client.post("/logout")
login(client, "other")
assert b"A quiet morning run" not in client.get("/stats?end=2026-10-02").data
assert client.get("/stats/runs?end=2026-10-02&bucket=28+Sep+2026").json["rows"] == []
def test_local_date_boundary_and_previous_alignment(app):
with app.app_context():
db.session.add(
Activity(
owner_id=1,
source_key="stats:boundary",
title="Local midnight",
sport="running",
started_at=datetime(2026, 9, 24, 14, tzinfo=UTC),
distance_m=1000,
duration_s=300,
)
)
db.session.add(
Activity(
owner_id=1,
source_key="stats:prior",
title="Prior",
sport="running",
started_at=datetime(2026, 9, 20, tzinfo=UTC),
distance_m=2000,
duration_s=800,
)
)
db.session.commit()
result = explore(1, "Australia/Sydney", parse_query({"weeks": "1", "end": "2026-10-01"}))
assert result["start"].isoformat() == "2026-09-25"
assert result["count"] == 2
assert result["prior_count"] == 1
assert 400 in result["chart"]["series"][0]["previous"]
def test_evidence_pagination(app):
with app.app_context():
add_history(40)
db.session.commit()
client = app.test_client()
login(client)
args = {"end": "2026-10-15", "group": "month", "bucket": "01 Sep 2026"}
response = client.get("/stats/runs", query_string=args)
assert response.status_code == 200
assert response.json["count"] == 31
assert len(response.json["rows"]) == 25
second = client.get("/stats/runs", query_string={**args, "page": 2})
assert len(second.json["rows"]) == 6
assert not {r["id"] for r in response.json["rows"]} & {r["id"] for r in second.json["rows"]}
assert client.get("/stats/runs", query_string={**args, "page": 3}).status_code == 400