116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
from datetime import date
|
|
|
|
import pytest
|
|
|
|
from app.core.dto.training import ApproachDTO, ExerciseSessionDTO
|
|
from app.core.metrics import estimate_1rm, session_metrics, summarize
|
|
|
|
|
|
def make_session(day: int, *sets: tuple, month: int = 1) -> ExerciseSessionDTO:
|
|
return ExerciseSessionDTO(
|
|
date=date(2025, month, day),
|
|
approaches=[ApproachDTO(weight=weight, reps=reps) for weight, reps in sets],
|
|
)
|
|
|
|
|
|
def test_estimate_1rm_uses_epley_formula():
|
|
assert estimate_1rm(140, 12) == pytest.approx(196)
|
|
assert estimate_1rm(100, 10) == pytest.approx(133.333, rel=1e-4)
|
|
|
|
|
|
def test_single_rep_is_the_weight_itself():
|
|
assert estimate_1rm(100, 1) == 100
|
|
|
|
|
|
def test_session_metrics_of_weighted_sets():
|
|
metrics = session_metrics(make_session(1, (100, 12), (110, 10)).approaches)
|
|
|
|
assert metrics.max_weight == 110
|
|
assert metrics.e1rm == pytest.approx(max(140.0, 110 * (1 + 10 / 30)))
|
|
assert metrics.volume == 100 * 12 + 110 * 10
|
|
assert metrics.total_reps == 22
|
|
assert metrics.best_reps == 12
|
|
|
|
|
|
def test_sets_without_weight_count_only_as_reps():
|
|
metrics = session_metrics(make_session(1, (0, 7), (47, 12)).approaches)
|
|
|
|
assert metrics.e1rm == pytest.approx(estimate_1rm(47, 12))
|
|
assert metrics.volume == 47 * 12
|
|
assert metrics.total_reps == 19
|
|
|
|
|
|
def test_session_without_weight_has_no_e1rm():
|
|
metrics = session_metrics(make_session(1, (0, 11), (0, 8)).approaches)
|
|
|
|
assert metrics.e1rm is None
|
|
assert metrics.volume == 0
|
|
assert metrics.total_reps == 19
|
|
assert metrics.best_reps == 11
|
|
|
|
|
|
def test_weighted_progress_records_and_change():
|
|
# newest first, like the repository returns
|
|
history = [
|
|
make_session(20, (100, 12)), # e1RM 140
|
|
make_session(10, (110, 12), (100, 12)), # e1RM 154 (record)
|
|
make_session(1, (80, 10)), # e1RM 106.67
|
|
]
|
|
|
|
progress = summarize(history)
|
|
|
|
assert progress.weighted is True
|
|
assert progress.max_weight == 110
|
|
assert progress.best_primary.value == pytest.approx(154)
|
|
assert progress.best_primary.date == date(2025, 1, 10)
|
|
assert progress.best_secondary.value == 110 * 12 + 100 * 12
|
|
assert progress.first_primary == pytest.approx(80 * (1 + 10 / 30))
|
|
assert progress.last_primary == pytest.approx(140)
|
|
assert progress.primary_change == pytest.approx(140 - 80 * (1 + 10 / 30))
|
|
assert progress.primary_change_percent == pytest.approx(
|
|
(140 - 80 * (1 + 10 / 30)) / (80 * (1 + 10 / 30)) * 100
|
|
)
|
|
|
|
|
|
def test_record_keeps_the_first_date_it_was_reached():
|
|
history = [make_session(20, (100, 12)), make_session(10, (100, 12))]
|
|
|
|
assert summarize(history).best_primary.date == date(2025, 1, 10)
|
|
|
|
|
|
def test_exercise_without_weight_is_tracked_by_reps():
|
|
history = [
|
|
make_session(20, (0, 8), (0, 7)), # best 8, total 15
|
|
make_session(10, (0, 10), (0, 5)), # best 10, total 15
|
|
make_session(1, (0, 6)), # best 6, total 6
|
|
]
|
|
|
|
progress = summarize(history)
|
|
|
|
assert progress.weighted is False
|
|
assert progress.max_weight == 0
|
|
assert progress.best_primary.value == 10
|
|
assert progress.best_primary.date == date(2025, 1, 10)
|
|
assert progress.best_secondary.value == 15
|
|
assert progress.best_secondary.date == date(2025, 1, 10)
|
|
assert (progress.first_primary, progress.last_primary) == (6, 8)
|
|
assert progress.primary_change == 2
|
|
|
|
|
|
def test_weighted_exercise_ignores_sessions_without_weight_for_primary_metric():
|
|
history = [
|
|
make_session(20, (0, 12)), # no e1RM
|
|
make_session(10, (100, 10)),
|
|
]
|
|
|
|
progress = summarize(history)
|
|
|
|
assert progress.weighted is True
|
|
assert progress.last_primary == pytest.approx(estimate_1rm(100, 10))
|
|
assert progress.primary_change == 0
|
|
|
|
|
|
def test_percent_change_is_undefined_when_the_first_value_is_zero():
|
|
progress = summarize([make_session(1, (0, 0))])
|
|
|
|
assert progress.primary_change_percent is None
|