(** Unit tests for {!Prescription}, authored against the prescription.mli contract. *) let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok" let get id = match Exercise.find_id id with | Some e -> e | None -> Alcotest.failf "catalog is missing %S" id let six_to_ten = Prescription.Rep_range.make ~min:6 ~max:10 let prescribe ?(substitutes = []) delivery = Prescription.Stimulus.make ~delivery ~rep_range:six_to_ten ~allowed_substitutes:substitutes let single id = prescribe (Prescription.Stimulus.Single (get id)) let pair ~isolation ~compound = prescribe (Prescription.Stimulus.Pre_exhaust { isolation = get isolation; compound = get compound }) let pair_with_substitutes ~substitutes ~isolation ~compound = prescribe ~substitutes:(List.map get substitutes) (Prescription.Stimulus.Pre_exhaust { isolation = get isolation; compound = get compound }) (* {1 One prescribed stimulus} *) let delivery_tests = [ ( "a single movement is prescribable", `Quick, fun () -> let p = single "curls" in match Prescription.Stimulus.delivery p with | Prescription.Stimulus.Single e -> Alcotest.(check string) "curls" "Curls" (Exercise.name e) | Prescription.Stimulus.Pre_exhaust _ -> Alcotest.fail "expected Single" ); ( "HD1's pec pre-exhaust is prescribable", `Quick, fun () -> let p = pair ~isolation:"dumbbell-flyes" ~compound:"incline-press" in Alcotest.(check int) "two movements, isolation first" 2 (List.length (Prescription.Stimulus.exercises p)); Alcotest.(check string) "isolation leads" "Dumbbell Flyes" (Exercise.name (List.hd (Prescription.Stimulus.exercises p))) ); ( "an invalid pairing raises Invalid", `Quick, fun () -> match try ignore (pair ~isolation:"dumbbell-flyes" ~compound:"squats"); None with Prescription.Stimulus.Invalid error -> Some error with | Some (Prescription.Stimulus.Not_a_pre_exhaust _) -> () | _ -> Alcotest.fail "expected Not_a_pre_exhaust" ); ] let rep_window_tests = [ ( "Rep_range limits are HD1's 6-12 stimulus window", `Quick, fun () -> Alcotest.(check int) "min 6" 6 (fst Prescription.Rep_range.limits); Alcotest.(check int) "max 12" 12 (snd Prescription.Rep_range.limits) ); ( "a window inside the limits is a prescription value", `Quick, fun () -> List.iter (fun (min, max) -> ignore (Prescription.Rep_range.make ~min ~max)) [ (6, 10); (6, 12); (8, 12); (8, 8) ] ); ( "a malformed range is rejected before prescription construction", `Quick, fun () -> match try ignore (Prescription.Rep_range.make ~min:8 ~max:6); None with Prescription.Rep_range.Invalid error -> Some error with | Some (Prescription.Rep_range.Invalid_order _) -> () | _ -> Alcotest.fail "expected Invalid_order" ); ( "a range outside HD1's limits is rejected", `Quick, fun () -> List.iter (fun (min, max) -> match try ignore (Prescription.Rep_range.make ~min ~max); None with Prescription.Rep_range.Invalid error -> Some error with | Some (Prescription.Rep_range.Outside_limits _) -> () | _ -> Alcotest.failf "%d-%d should be refused" min max) [ (3, 5); (1, 3); (15, 20); (6, 20) ] ); ] let substitute_tests = [ ( "a whitelisted alternative may be allowed", `Quick, fun () -> let p = prescribe ~substitutes:[ get "pec-deck" ] (Prescription.Stimulus.Single (get "dumbbell-flyes")) in Alcotest.(check bool) "pec deck permitted" true (Prescription.Stimulus.permits p (get "pec-deck")); Alcotest.(check bool) "flyes permitted" true (Prescription.Stimulus.permits p (get "dumbbell-flyes")); Alcotest.(check bool) "squats not permitted" false (Prescription.Stimulus.permits p (get "squats")) ); ( "a substitute off the catalog whitelist raises Invalid", `Quick, fun () -> match try ignore (prescribe ~substitutes:[ get "squats" ] (Prescription.Stimulus.Single (get "dumbbell-flyes"))); None with Prescription.Stimulus.Invalid error -> Some error with | Some (Prescription.Stimulus.Substitute_not_permitted id) -> Alcotest.(check string) "squats" "squats" (id :> string) | _ -> Alcotest.fail "expected Substitute_not_permitted" ); ( "a substitute for either half of a pre-exhaust is allowed", `Quick, fun () -> ignore (prescribe ~substitutes:[ get "cable-crossovers" ] (Prescription.Stimulus.Pre_exhaust { isolation = get "dumbbell-flyes"; compound = get "incline-press"; })) ); ] (* {1 One prescribed workout} *) (* HD1's Day 1: pecs pre-exhaust, two delt isolations, triceps pre-exhaust. *) let day_one_stimuli = [ pair ~isolation:"dumbbell-flyes" ~compound:"incline-press"; single "laterals"; single "bent-over-laterals"; pair ~isolation:"lying-french-press" ~compound:"dips"; ] let workout_tests = [ ( "HD1's Day 1 is prescribable, in order", `Quick, fun () -> let w = Prescription.Workout.make ~name:"Day 1" ~stimuli:day_one_stimuli in Alcotest.(check string) "name" "Day 1" (Prescription.Workout.name w); Alcotest.(check int) "four stimuli" 4 (List.length (Prescription.Workout.stimuli w)) ); ( "an empty workout raises Invalid", `Quick, fun () -> match try ignore (Prescription.Workout.make ~name:"Nothing" ~stimuli:[]); false with | Prescription.Workout.Invalid Prescription.Workout.Empty_workout -> true with | true -> () | false -> Alcotest.fail "expected Empty_workout" ); ] (* {1 The routine} *) let secs = Recovery.duration_to_seconds let ideal_day_one = Prescription.Workout.make ~name:"Day 1" ~stimuli: [ pair_with_substitutes ~substitutes:[ "cable-crossovers"; "pec-deck" ] ~isolation:"dumbbell-flyes" ~compound:"incline-press"; single "laterals"; prescribe ~substitutes:[ get "reverse-pec-deck" ] (Prescription.Stimulus.Single (get "bent-over-laterals")); pair_with_substitutes ~substitutes:[ "pressdowns"; "triceps-machine" ] ~isolation:"lying-french-press" ~compound:"dips"; ] let ideal_day_two = Prescription.Workout.make ~name:"Day 2" ~stimuli: [ pair_with_substitutes ~substitutes:[ "straight-arm-pulldowns" ] ~isolation:"pullovers" ~compound:"close-grip-pulldowns"; single "bent-over-rows"; single "shrugs"; prescribe ~substitutes:[ get "deadlifts" ] (Prescription.Stimulus.Single (get "hyperextensions")); prescribe ~substitutes:[ get "preacher-curls" ] (Prescription.Stimulus.Single (get "curls")); ] let ideal_day_three = Prescription.Workout.make ~name:"Day 3" ~stimuli: [ pair_with_substitutes ~substitutes:[ "squats" ] ~isolation:"leg-extensions" ~compound:"leg-presses"; single "leg-curls"; single "calf-raises"; single "sit-ups"; ] let ideal = Prescription.Routine.make ~name:"Ideal Routine" ~workouts:[ ideal_day_one; ideal_day_two; ideal_day_three ] let days = Prescription.Routine.workouts ideal let nth n = match List.nth_opt days n with | Some w -> w | None -> Alcotest.failf "Ideal Routine has no workout %d" n let routine_tests = [ ( "an empty routine raises Invalid", `Quick, fun () -> match try ignore (Prescription.Routine.make ~name:"Nothing" ~workouts:[]); false with | Prescription.Routine.Invalid Prescription.Routine.Empty_routine -> true with | true -> () | false -> Alcotest.fail "expected Empty_routine" ); ( "the Ideal Routine is HD1's three days", `Quick, fun () -> Alcotest.(check string) "name" "Ideal Routine" (Prescription.Routine.name ideal); Alcotest.(check int) "three workouts" 3 (List.length days); Alcotest.(check (list string)) "in order" [ "Day 1"; "Day 2"; "Day 3" ] (List.map Prescription.Workout.name days) ); ( "each day prescribes the movements HD1 lists", `Quick, fun () -> Alcotest.(check (list int)) "stimuli per day" [ 4; 5; 4 ] (List.map (fun w -> List.length (Prescription.Workout.stimuli w)) days) ); ( "Day 1 opens with the pec pre-exhaust and closes with the triceps one", `Quick, fun () -> let ps = Prescription.Workout.stimuli (nth 0) in let names p = List.map Exercise.name (Prescription.Stimulus.exercises p) in Alcotest.(check (list string)) "flyes into incline press" [ "Dumbbell Flyes"; "Incline Presses" ] (names (List.hd ps)); Alcotest.(check (list string)) "french press into dips" [ "Lying French Presses"; "Dips" ] (names (List.nth ps 3)) ); ( "Day 2's superset is the lat pre-exhaust, not an antagonist pairing", `Quick, fun () -> let ps = Prescription.Workout.stimuli (nth 1) in match Prescription.Stimulus.delivery (List.hd ps) with | Prescription.Stimulus.Pre_exhaust { isolation; compound } -> Alcotest.(check string) "isolation" "Pullovers" (Exercise.name isolation); Alcotest.(check string) "compound" "Close-grip, palms-up Pulldowns" (Exercise.name compound) | Prescription.Stimulus.Single _ -> Alcotest.fail "expected a pre-exhaust" ); ( "HD1's alternatives are permitted where it offers them", `Quick, fun () -> let legs = List.hd (Prescription.Workout.stimuli (nth 2)) in Alcotest.(check bool) "squats may replace the leg press" true (Prescription.Stimulus.permits legs (get "squats")); let pecs = List.hd (Prescription.Workout.stimuli (nth 0)) in Alcotest.(check bool) "pec deck may replace flyes" true (Prescription.Stimulus.permits pecs (get "pec-deck")) ); ] let rotation_tests = [ ( "the cycle advances and wraps", `Quick, fun () -> let name w = Prescription.Workout.name w in Alcotest.(check string) "1 -> 2" "Day 2" (name (Prescription.Routine.workout_after ideal (nth 0))); Alcotest.(check string) "2 -> 3" "Day 3" (name (Prescription.Routine.workout_after ideal (nth 1))); Alcotest.(check string) "3 wraps to 1" "Day 1" (name (Prescription.Routine.workout_after ideal (nth 2))) ); ( "an unknown workout falls back to the start of the cycle", `Quick, fun () -> let stranger = Prescription.Workout.make ~name:"Elsewhere" ~stimuli:(Prescription.Workout.stimuli (nth 0)) in Alcotest.(check string) "falls back" "Day 1" (Prescription.Workout.name (Prescription.Routine.workout_after ideal stranger)) ); ] let rest_tests = [ ( "HD1's intervals are every other day, then two days off", `Quick, fun () -> Alcotest.(check int) "48h" 172_800 (secs Prescription.Routine.training_interval); Alcotest.(check int) "72h" 259_200 (secs Prescription.Routine.cycle_rest) ); ( "rest within the cycle is 48h, and 72h once it completes", `Quick, fun () -> Alcotest.(check int) "after Day 1" 172_800 (secs (Prescription.Routine.recovery_after ideal (nth 0))); Alcotest.(check int) "after Day 2" 172_800 (secs (Prescription.Routine.recovery_after ideal (nth 1))); Alcotest.(check int) "after Day 3" 259_200 (secs (Prescription.Routine.recovery_after ideal (nth 2))) ); ] let suite = [ ("prescription.stimulus.delivery", delivery_tests); ("prescription.stimulus.rep_window", rep_window_tests); ("prescription.stimulus.substitutes", substitute_tests); ("prescription.workout", workout_tests); ("prescription.routine", routine_tests); ("prescription.routine.rotation", rotation_tests); ("prescription.routine.rest", rest_tests); ]