Files
garmin-coach-to-cal-sync/tests/test_workouts.py

234 lines
7.9 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 __future__ import annotations
from datetime import date
from garmin_coach_clone.workouts import (
build_dummy_cycling_workout,
calendar_entry_date,
calendar_entry_id,
calendar_entry_name,
clone_workout_description,
clone_workout_name,
clone_workout_payload,
estimate_duration,
existing_clone_names,
find_generated_calendar_entry,
find_generated_workout,
generated_calendar_entries,
generated_clone_date,
generated_workouts,
generated_workouts_older_than,
validate_workout_payload,
workout_steps_equal,
)
def test_dummy_workout_has_expected_duration_and_steps() -> None:
workout = build_dummy_cycling_workout("Dummy")
assert workout["workoutName"] == "Dummy"
assert workout["sportType"]["sportTypeKey"] == "cycling"
assert estimate_duration(workout) == 14 * 60
assert validate_workout_payload(workout) == []
def test_clone_payload_strips_ids_and_sets_prefix() -> None:
source = build_dummy_cycling_workout("Coach Original")
source["workoutId"] = 123
source["ownerId"] = 456
source["workoutSegments"][0]["workoutSteps"][0]["stepId"] = 789
cloned = clone_workout_payload(source, date(2026, 6, 16), "GCClone")
assert cloned["workoutName"] == "Coach Original June 16, 2026 (gcclone)"
assert "workoutId" not in cloned
assert "ownerId" not in cloned
assert "stepId" not in cloned["workoutSegments"][0]["workoutSteps"][0]
assert validate_workout_payload(cloned) == []
def test_clone_title_and_description_use_source_details() -> None:
source = build_dummy_cycling_workout("Tempo")
source["description"] = "3x10:00@180W"
steps = source["workoutSegments"][0]["workoutSteps"]
steps[0]["targetType"] = {"workoutTargetTypeKey": "power.zone"}
steps[0]["targetValueOne"] = 98
steps[0]["targetValueTwo"] = 142
assert clone_workout_name(
"Tempo", date(2026, 7, 15), "GCClone", "{type} {date} ({clone})"
) == "Tempo July 15, 2026 (gcclone)"
assert clone_workout_description(source) == (
"3x10:00@180W\n"
"Intervals: Warm up 5:00 @ 98142 W | 1× (Interval 2:00 | Recover 2:00) | "
"Cool down 5:00\n"
"GCClone — cloned from Garmin Coach; completing it may not update the Coach plan."
)
def test_workout_steps_equal_ignores_ids_but_compares_steps() -> None:
left = build_dummy_cycling_workout("Left")
right = build_dummy_cycling_workout("Right")
left["workoutId"] = 1
right["workoutId"] = 2
left["workoutSegments"][0]["workoutSteps"][0]["stepId"] = 100
right["workoutSegments"][0]["workoutSteps"][0]["stepId"] = 200
assert workout_steps_equal(left, right)
right["workoutSegments"][0]["workoutSteps"][0]["endConditionValue"] = 123
assert not workout_steps_equal(left, right)
def test_workout_steps_equal_ignores_garmin_upload_defaults() -> None:
left = build_dummy_cycling_workout("Left")
right = build_dummy_cycling_workout("Right")
repeat = right["workoutSegments"][0]["workoutSteps"][1]
repeat["stepOrder"] = 2
repeat["childStepId"] = 1
repeat["smartRepeat"] = False
for idx, step in enumerate(right["workoutSegments"][0]["workoutSteps"], start=1):
step["stepId"] = 1000 + idx
step["stepOrder"] = idx
step["equipmentType"] = {
"displayOrder": 0,
"equipmentTypeId": 0,
"equipmentTypeKey": None,
}
step["strokeType"] = {
"displayOrder": 0,
"strokeTypeId": 0,
"strokeTypeKey": None,
}
for idx, step in enumerate(repeat["workoutSteps"], start=3):
step["childStepId"] = 1
step["stepId"] = 1000 + idx
step["stepOrder"] = idx
step["equipmentType"] = {
"displayOrder": 0,
"equipmentTypeId": 0,
"equipmentTypeKey": None,
}
step["strokeType"] = {
"displayOrder": 0,
"strokeTypeId": 0,
"strokeTypeKey": None,
}
assert workout_steps_equal(left, right)
def test_existing_clone_names_filters_by_prefix_and_date() -> None:
names = existing_clone_names(
[
{"workoutName": "GCClone 2026-06-16 Ride"},
{"workoutName": "GCClone 2026-06-17 Ride"},
{"workoutName": "Other 2026-06-16 Ride"},
],
date(2026, 6, 16),
"GCClone",
)
assert names == ["GCClone 2026-06-16 Ride"]
def test_generated_workouts_filters_by_prefix() -> None:
workouts = [
{"workoutId": 1, "workoutName": "GCClone 2026-06-16 Ride"},
{"workoutId": 2, "workoutName": "GCClone Probe Dummy 2026-06-16"},
{"workoutId": 3, "workoutName": "Real Workout"},
]
assert generated_workouts(workouts, "GCClone") == workouts[:2]
assert find_generated_workout(workouts, "2", "GCClone") == workouts[1]
assert find_generated_workout(workouts, "3", "GCClone") is None
def test_generated_clone_date_requires_prefix_and_iso_date() -> None:
assert generated_clone_date("GCClone 2026-06-16 Ride", "GCClone") == date(2026, 6, 16)
assert generated_clone_date("GCClone Probe Dummy 2026-06-16", "GCClone") is None
assert generated_clone_date("Other 2026-06-16 Ride", "GCClone") is None
def test_generated_workouts_older_than_filters_by_clone_date() -> None:
workouts = [
{"workoutId": 1, "workoutName": "GCClone 2026-06-10 Old"},
{"workoutId": 2, "workoutName": "GCClone 2026-06-11 Cutoff"},
{"workoutId": 3, "workoutName": "GCClone 2026-06-12 New"},
{"workoutId": 4, "workoutName": "GCClone Probe Dummy 2026-06-01"},
]
assert generated_workouts_older_than(workouts, date(2026, 6, 11), "GCClone") == [
workouts[0]
]
def test_generated_calendar_entries_handle_nested_workout_names() -> None:
generated_entry = {
"scheduledWorkoutId": 99,
"scheduledDate": "2026-06-16",
"workout": {"workoutName": "GCClone 2026-06-16 Ride"},
}
data = {
"calendarItems": [
generated_entry,
{
"scheduledWorkoutId": 100,
"scheduledDate": "2026-06-16",
"workout": {"workoutName": "Real Workout"},
},
]
}
entries = generated_calendar_entries(data, "GCClone")
assert entries == [generated_entry]
assert find_generated_calendar_entry(data, "99", "GCClone") == generated_entry
assert find_generated_calendar_entry(data, "100", "GCClone") is None
assert calendar_entry_id(generated_entry) == "99"
assert calendar_entry_date(generated_entry) == "2026-06-16"
assert calendar_entry_name(generated_entry) == "GCClone 2026-06-16 Ride"
def test_generated_calendar_entries_accept_id_as_calendar_entry_id() -> None:
generated_entry = {
"id": 99,
"scheduledDate": "2026-06-16",
"workout": {"workoutName": "GCClone 2026-06-16 Ride"},
}
data = {"calendarItems": [generated_entry]}
assert generated_calendar_entries(data, "GCClone") == [generated_entry]
assert calendar_entry_id(generated_entry) == "99"
def test_validate_workout_payload_rejects_missing_segments() -> None:
errors = validate_workout_payload(
{
"workoutName": "Broken",
"sportType": {"sportTypeKey": "cycling"},
"workoutSegments": [],
}
)
assert "workoutSegments must be a non-empty list" in errors
def test_validate_workout_payload_rejects_non_cycling() -> None:
workout = build_dummy_cycling_workout("Broken")
workout["sportType"] = {"sportTypeKey": "running"}
errors = validate_workout_payload(workout)
assert "sportType must be cycling, got running" in errors
def test_validate_workout_payload_rejects_missing_step_target() -> None:
workout = build_dummy_cycling_workout("Broken")
workout["workoutSegments"][0]["workoutSteps"][0].pop("targetType")
errors = validate_workout_payload(workout)
assert "workoutSegments[1].workoutSteps[1].targetType is missing" in errors