pytest + pytest-asyncio in flake.nix. 53 tests covering parser (all formats, error cases) and db (CRUD, soft delete, update, stats, pagination). Pre-commit hook runs tests when fitness bot files are staged. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
7 KiB
Python
187 lines
7 KiB
Python
from parser import parse_exercise_line, parse_workout, format_workout, _fmt_weight
|
|
|
|
|
|
# ── parse_exercise_line ──────────────────────────────────────────
|
|
|
|
|
|
class TestParseExerciseLine:
|
|
def test_classic_format(self):
|
|
ex = parse_exercise_line("Bench press: 4x8x35")
|
|
assert ex.name == "Bench press"
|
|
assert ex.sets == 4
|
|
assert ex.reps == 8
|
|
assert ex.weight_kg == 35.0
|
|
assert len(ex.sets_detail) == 4
|
|
assert all(s.reps == 8 and s.weight_kg == 35.0 for s in ex.sets_detail)
|
|
|
|
def test_bodyweight(self):
|
|
ex = parse_exercise_line("Pull-ups: 3x10")
|
|
assert ex.sets == 3
|
|
assert ex.reps == 10
|
|
assert ex.weight_kg == 0.0
|
|
|
|
def test_per_set_varying(self):
|
|
ex = parse_exercise_line("Shoulder press: 8x25, 5x35, 6x40")
|
|
assert ex.sets == 3
|
|
assert ex.sets_detail[0].reps == 8
|
|
assert ex.sets_detail[0].weight_kg == 25.0
|
|
assert ex.sets_detail[1].reps == 5
|
|
assert ex.sets_detail[1].weight_kg == 35.0
|
|
assert ex.sets_detail[2].reps == 6
|
|
assert ex.sets_detail[2].weight_kg == 40.0
|
|
|
|
def test_per_set_bodyweight(self):
|
|
ex = parse_exercise_line("Pull-ups: 12, 10, 8")
|
|
assert ex.sets == 3
|
|
assert [s.reps for s in ex.sets_detail] == [12, 10, 8]
|
|
assert all(s.weight_kg == 0.0 for s in ex.sets_detail)
|
|
|
|
def test_machine_id(self):
|
|
ex = parse_exercise_line("Shoulder press (3032): 4x8x25")
|
|
assert ex.machine_id == "3032"
|
|
assert ex.name == "Shoulder press"
|
|
|
|
def test_machine_id_with_per_set(self):
|
|
ex = parse_exercise_line("Butterfly chest (5014): 7x40, 7x40, 5x45")
|
|
assert ex.machine_id == "5014"
|
|
assert ex.sets == 3
|
|
|
|
def test_asterisk_separator(self):
|
|
ex = parse_exercise_line("Deadlift: 3*5*100")
|
|
assert ex.sets == 3
|
|
assert ex.reps == 5
|
|
assert ex.weight_kg == 100.0
|
|
|
|
def test_decimal_weight(self):
|
|
ex = parse_exercise_line("Bench: 3x8x22.5")
|
|
assert ex.weight_kg == 22.5
|
|
|
|
def test_no_colon_returns_none(self):
|
|
assert parse_exercise_line("just text") is None
|
|
|
|
def test_bad_format_after_colon_returns_none(self):
|
|
assert parse_exercise_line("Foo: abc") is None
|
|
|
|
def test_empty_string_returns_none(self):
|
|
assert parse_exercise_line("") is None
|
|
|
|
def test_whitespace_only_returns_none(self):
|
|
assert parse_exercise_line(" ") is None
|
|
|
|
|
|
# ── parse_workout ────────────────────────────────────────────────
|
|
|
|
|
|
class TestParseWorkout:
|
|
def test_single_line(self):
|
|
groups, errors = parse_workout("Bench: 4x8x35")
|
|
assert len(groups) == 1
|
|
assert len(groups[0]) == 1
|
|
assert groups[0][0].name == "Bench"
|
|
|
|
def test_superset_consecutive_lines(self):
|
|
groups, errors = parse_workout("Bench: 4x8x35\nCurls: 3x10x15")
|
|
assert len(groups) == 1
|
|
assert len(groups[0]) == 2
|
|
|
|
def test_blank_line_separates_groups(self):
|
|
groups, errors = parse_workout("Bench: 4x8x35\n\nSquats: 5x5x60")
|
|
assert len(groups) == 2
|
|
assert len(groups[0]) == 1
|
|
assert len(groups[1]) == 1
|
|
|
|
def test_errors_collected(self):
|
|
groups, errors = parse_workout("Bench: 4x8x35\nBad: nope\nSquats: 5x5x60")
|
|
assert len(groups) == 1 # bench and squats in one group (no blank line)
|
|
assert len(groups[0]) == 2
|
|
assert len(errors) == 1
|
|
assert "Bad: nope" in errors[0].line
|
|
|
|
def test_all_lines_fail(self):
|
|
groups, errors = parse_workout("Bad: nope\nAlso bad: xyz")
|
|
assert len(groups) == 0
|
|
assert len(errors) == 2
|
|
|
|
def test_lines_without_colon_skipped_silently(self):
|
|
groups, errors = parse_workout("Note: this is a header\nBench: 4x8x35")
|
|
# "Note: this is a header" has a colon but fails parse → error
|
|
# Actually let me check: it tries to parse "this is a header" as sets
|
|
# which fails, so it IS an error
|
|
assert len(groups) == 1
|
|
assert len(errors) == 1
|
|
|
|
def test_plain_text_no_colon_no_error(self):
|
|
groups, errors = parse_workout("just a note\nBench: 4x8x35")
|
|
assert len(groups) == 1
|
|
assert len(errors) == 0
|
|
|
|
def test_empty_text(self):
|
|
groups, errors = parse_workout("")
|
|
assert groups == []
|
|
assert errors == []
|
|
|
|
def test_full_workout(self):
|
|
text = """Shoulder press (3032): 8x25, 5x35, 6x40, 6x40
|
|
Butterfly chest (5014): 7x40, 7x40, 5x45
|
|
Bicep curl machine: 10x20, 5x25, 4x25
|
|
Ab curls: 3x7x70"""
|
|
groups, errors = parse_workout(text)
|
|
assert len(errors) == 0
|
|
assert len(groups) == 1 # all consecutive = one superset group
|
|
assert len(groups[0]) == 4
|
|
|
|
|
|
# ── format_workout ───────────────────────────────────────────────
|
|
|
|
|
|
class TestFormatWorkout:
|
|
def test_uniform_sets_compact(self):
|
|
data = [[{"name": "Bench", "machine_id": None, "sets": 4, "reps": 8,
|
|
"weight_kg": 35, "sets_detail": [{"reps": 8, "weight_kg": 35}] * 4}]]
|
|
result = format_workout(data)
|
|
assert "4x8x35kg" in result
|
|
|
|
def test_varying_sets_listed(self):
|
|
data = [[{"name": "Press", "machine_id": None, "sets": 2, "reps": 8,
|
|
"weight_kg": 25, "sets_detail": [
|
|
{"reps": 8, "weight_kg": 25}, {"reps": 5, "weight_kg": 35}]}]]
|
|
result = format_workout(data)
|
|
assert "8x25kg" in result
|
|
assert "5x35kg" in result
|
|
|
|
def test_machine_id_shown(self):
|
|
data = [[{"name": "Press", "machine_id": "3032", "sets": 1, "reps": 8,
|
|
"weight_kg": 25, "sets_detail": [{"reps": 8, "weight_kg": 25}]}]]
|
|
result = format_workout(data)
|
|
assert "(3032)" in result
|
|
|
|
def test_bodyweight_omits_kg(self):
|
|
data = [[{"name": "Pull-ups", "machine_id": None, "sets": 3, "reps": 10,
|
|
"weight_kg": 0, "sets_detail": [{"reps": 10, "weight_kg": 0}] * 3}]]
|
|
result = format_workout(data)
|
|
assert "kg" not in result
|
|
assert "3x10" in result
|
|
|
|
def test_superset_label(self):
|
|
data = [[
|
|
{"name": "A", "machine_id": None, "sets": 1, "reps": 10, "weight_kg": 20,
|
|
"sets_detail": [{"reps": 10, "weight_kg": 20}]},
|
|
{"name": "B", "machine_id": None, "sets": 1, "reps": 10, "weight_kg": 20,
|
|
"sets_detail": [{"reps": 10, "weight_kg": 20}]},
|
|
]]
|
|
result = format_workout(data)
|
|
assert "Superset" in result
|
|
|
|
|
|
# ── _fmt_weight ──────────────────────────────────────────────────
|
|
|
|
|
|
class TestFmtWeight:
|
|
def test_integer(self):
|
|
assert _fmt_weight(70.0) == "70"
|
|
|
|
def test_fractional(self):
|
|
assert _fmt_weight(22.5) == "22.5"
|
|
|
|
def test_zero(self):
|
|
assert _fmt_weight(0.0) == "0"
|