from types import SimpleNamespace import pytest from app.analytics import analyse, enrich, running_cost def run(grade=0): return SimpleNamespace( sport="running", distance_m=600, samples=[ { "distance": i * 3, "time": i, "elevation": 100 + i * 3 * grade, "speed": 3, "heart_rate": 150, "cadence": 170, } for i in range(201) ], laps=[{"distance": 301}, {"distance": 299}], ) def test_flat_gap_and_laps_use_distance_weighted_time(): result = analyse(run()) assert result["summary"]["gap"] == pytest.approx(1000 / 3) assert result["summary"]["gap_coverage"] == 100 assert [lap["gap"] for lap in result["laps"]] == pytest.approx([1000 / 3] * 2) assert sum(lap["gap_time"] for lap in result["laps"]) == pytest.approx(200) assert result["terrain"][1]["heart_rate"] == 150 @pytest.mark.parametrize("grade", [-0.1, 0.1]) def test_slope_adjusts_pace_using_cost_ratio(grade): activity = run(grade) row = enrich(activity.samples)[100] assert row["grade"] == pytest.approx(grade * 100) assert row["gap"] == pytest.approx(1000 / 3 * 3.6 / running_cost(grade)) gap = analyse(activity)["summary"]["gap"] assert (gap < 1000 / 3) if grade > 0 else (gap > 1000 / 3) def test_missing_data_and_nonrunning_do_not_invent_gap(): activity = run() for row in activity.samples: row["elevation"] = None assert analyse(activity)["summary"]["gap"] is None activity = run() activity.sport = "cycling" assert analyse(activity)["summary"]["gap"] is None activity = run() for row in activity.samples: row["time"] = None assert analyse(activity)["summary"]["gap"] is None def test_recording_gap_excluded_from_time_and_distance(): activity = run() for row in activity.samples[100:]: row["time"] += 100 result = analyse(activity) assert result["summary"]["gap"] == pytest.approx(1000 / 3) assert result["summary"]["gap_coverage"] == pytest.approx(99.5) def test_extreme_slopes_and_stationary_records_have_no_gap(): assert analyse(run(0.5))["summary"]["gap"] is None activity = run() for row in activity.samples: row["distance"] = 0 assert analyse(activity)["summary"]["gap"] is None