(** Unit tests for {!Exercise}, authored against the exercise.mli contract. *) let get id = match Exercise.find_id id with | Some e -> e | None -> Alcotest.failf "catalog is missing %S" id let dumbbell_flyes = { Exercise.equipment = Exercise.Dumbbell; movement = Exercise.Fly; variation = Exercise.Standard; } let lookup_tests = [ ( "a structured key locates the curated exercise", `Quick, fun () -> match Exercise.find dumbbell_flyes with | Some exercise -> Alcotest.(check string) "name" "Dumbbell Flyes" (Exercise.name exercise); Alcotest.(check bool) "key round-trips" true (Exercise.key exercise = dumbbell_flyes); Alcotest.(check string) "stable id" "dumbbell-flyes" (Exercise.id exercise :> string) | None -> Alcotest.fail "Dumbbell Flyes key is absent" ); ( "an absent structured key is rejected", `Quick, fun () -> Alcotest.(check bool) "no dumbbell press in HD1 catalog" true (Option.is_none (Exercise.find { Exercise.equipment = Exercise.Dumbbell; movement = Exercise.Press; variation = Exercise.Standard; })) ); ( "a stable identifier remains an external lookup boundary", `Quick, fun () -> Alcotest.(check bool) "stable id" true (Option.is_some (Exercise.find_id "dumbbell-flyes")); Alcotest.(check bool) "unknown id" true (Option.is_none (Exercise.find_id "jefferson-curl")) ); ] let pre_exhaust_tests = [ ( "accepts HD1's pec pairing", `Quick, fun () -> Alcotest.(check bool) "flyes then incline press" true (Exercise.may_pre_exhaust ~isolation:(get "dumbbell-flyes") ~compound:(get "incline-press")) ); ( "accepts HD1's triceps pairing, where dips serves a second muscle", `Quick, fun () -> Alcotest.(check bool) "french press then dips" true (Exercise.may_pre_exhaust ~isolation:(get "lying-french-press") ~compound:(get "dips")); Alcotest.(check bool) "flyes then dips" true (Exercise.may_pre_exhaust ~isolation:(get "dumbbell-flyes") ~compound:(get "dips")) ); ( "accepts HD1's lat and leg pairings", `Quick, fun () -> Alcotest.(check bool) "pullovers then pulldowns" true (Exercise.may_pre_exhaust ~isolation:(get "pullovers") ~compound:(get "close-grip-pulldowns")); Alcotest.(check bool) "leg extensions then leg presses" true (Exercise.may_pre_exhaust ~isolation:(get "leg-extensions") ~compound:(get "leg-presses")) ); ( "rejects invalid pairings", `Quick, fun () -> Alcotest.(check bool) "compound then compound" false (Exercise.may_pre_exhaust ~isolation:(get "dips") ~compound:(get "incline-press")); Alcotest.(check bool) "isolation then isolation" false (Exercise.may_pre_exhaust ~isolation:(get "dumbbell-flyes") ~compound:(get "pec-deck")); Alcotest.(check bool) "unrelated targets" false (Exercise.may_pre_exhaust ~isolation:(get "dumbbell-flyes") ~compound:(get "squats")) ); ] let substitution_tests = [ ( "HD1's alternatives are substitutable", `Quick, fun () -> Alcotest.(check bool) "flyes for pec deck" true (Exercise.may_substitute ~original:(get "dumbbell-flyes") ~candidate:(get "pec-deck")); Alcotest.(check bool) "leg press for squat" true (Exercise.may_substitute ~original:(get "leg-presses") ~candidate:(get "squats")) ); ( "substitution is symmetric but excludes the original and unrelated \ movements", `Quick, fun () -> let flyes = get "dumbbell-flyes" in let pec_deck = get "pec-deck" in Alcotest.(check bool) "pec deck for flyes" true (Exercise.may_substitute ~original:pec_deck ~candidate:flyes); Alcotest.(check bool) "not self" false (Exercise.may_substitute ~original:flyes ~candidate:flyes); Alcotest.(check bool) "curls for squats" false (Exercise.may_substitute ~original:(get "curls") ~candidate:(get "squats")) ); ] let suite = [ ("exercise.lookup", lookup_tests); ("exercise.pre_exhaust", pre_exhaust_tests); ("exercise.substitution", substitution_tests); ]