View raw

1 (** Unit tests for {!Prescription}, authored against the prescription.mli 2 contract. *) 3 4 let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok" 5 6 let get id = 7 match Exercise.find_id id with 8 | Some e -> e 9 | None -> Alcotest.failf "catalog is missing %S" id 10 11 let six_to_ten = Prescription.Rep_range.make ~min:6 ~max:10 12 13 let prescribe ?(substitutes = []) delivery = 14 Prescription.Stimulus.make ~delivery ~rep_range:six_to_ten 15 ~allowed_substitutes:substitutes 16 17 let single id = prescribe (Prescription.Stimulus.Single (get id)) 18 19 let pair ~isolation ~compound = 20 prescribe 21 (Prescription.Stimulus.Pre_exhaust 22 { isolation = get isolation; compound = get compound }) 23 24 let pair_with_substitutes ~substitutes ~isolation ~compound = 25 prescribe ~substitutes:(List.map get substitutes) 26 (Prescription.Stimulus.Pre_exhaust 27 { isolation = get isolation; compound = get compound }) 28 29 (* {1 One prescribed stimulus} *) 30 31 let delivery_tests = 32 [ 33 ( "a single movement is prescribable", 34 `Quick, 35 fun () -> 36 let p = single "curls" in 37 match Prescription.Stimulus.delivery p with 38 | Prescription.Stimulus.Single e -> 39 Alcotest.(check string) "curls" "Curls" (Exercise.name e) 40 | Prescription.Stimulus.Pre_exhaust _ -> Alcotest.fail "expected Single" 41 ); 42 ( "HD1's pec pre-exhaust is prescribable", 43 `Quick, 44 fun () -> 45 let p = pair ~isolation:"dumbbell-flyes" ~compound:"incline-press" in 46 Alcotest.(check int) 47 "two movements, isolation first" 2 48 (List.length (Prescription.Stimulus.exercises p)); 49 Alcotest.(check string) 50 "isolation leads" "Dumbbell Flyes" 51 (Exercise.name (List.hd (Prescription.Stimulus.exercises p))) ); 52 ( "an invalid pairing raises Invalid", 53 `Quick, 54 fun () -> 55 match 56 try 57 ignore (pair ~isolation:"dumbbell-flyes" ~compound:"squats"); 58 None 59 with Prescription.Stimulus.Invalid error -> Some error 60 with 61 | Some (Prescription.Stimulus.Not_a_pre_exhaust _) -> () 62 | _ -> Alcotest.fail "expected Not_a_pre_exhaust" ); 63 ] 64 65 let rep_window_tests = 66 [ 67 ( "Rep_range limits are HD1's 6-12 stimulus window", 68 `Quick, 69 fun () -> 70 Alcotest.(check int) "min 6" 6 (fst Prescription.Rep_range.limits); 71 Alcotest.(check int) "max 12" 12 (snd Prescription.Rep_range.limits) ); 72 ( "a window inside the limits is a prescription value", 73 `Quick, 74 fun () -> 75 List.iter 76 (fun (min, max) -> ignore (Prescription.Rep_range.make ~min ~max)) 77 [ (6, 10); (6, 12); (8, 12); (8, 8) ] ); 78 ( "a malformed range is rejected before prescription construction", 79 `Quick, 80 fun () -> 81 match 82 try 83 ignore (Prescription.Rep_range.make ~min:8 ~max:6); 84 None 85 with Prescription.Rep_range.Invalid error -> Some error 86 with 87 | Some (Prescription.Rep_range.Invalid_order _) -> () 88 | _ -> Alcotest.fail "expected Invalid_order" ); 89 ( "a range outside HD1's limits is rejected", 90 `Quick, 91 fun () -> 92 List.iter 93 (fun (min, max) -> 94 match 95 try 96 ignore (Prescription.Rep_range.make ~min ~max); 97 None 98 with Prescription.Rep_range.Invalid error -> Some error 99 with 100 | Some (Prescription.Rep_range.Outside_limits _) -> () 101 | _ -> Alcotest.failf "%d-%d should be refused" min max) 102 [ (3, 5); (1, 3); (15, 20); (6, 20) ] ); 103 ] 104 105 let substitute_tests = 106 [ 107 ( "a whitelisted alternative may be allowed", 108 `Quick, 109 fun () -> 110 let p = 111 prescribe 112 ~substitutes:[ get "pec-deck" ] 113 (Prescription.Stimulus.Single (get "dumbbell-flyes")) 114 in 115 Alcotest.(check bool) 116 "pec deck permitted" true 117 (Prescription.Stimulus.permits p (get "pec-deck")); 118 Alcotest.(check bool) 119 "flyes permitted" true 120 (Prescription.Stimulus.permits p (get "dumbbell-flyes")); 121 Alcotest.(check bool) 122 "squats not permitted" false 123 (Prescription.Stimulus.permits p (get "squats")) ); 124 ( "a substitute off the catalog whitelist raises Invalid", 125 `Quick, 126 fun () -> 127 match 128 try 129 ignore 130 (prescribe 131 ~substitutes:[ get "squats" ] 132 (Prescription.Stimulus.Single (get "dumbbell-flyes"))); 133 None 134 with Prescription.Stimulus.Invalid error -> Some error 135 with 136 | Some (Prescription.Stimulus.Substitute_not_permitted id) -> 137 Alcotest.(check string) "squats" "squats" (id :> string) 138 | _ -> Alcotest.fail "expected Substitute_not_permitted" ); 139 ( "a substitute for either half of a pre-exhaust is allowed", 140 `Quick, 141 fun () -> 142 ignore 143 (prescribe 144 ~substitutes:[ get "cable-crossovers" ] 145 (Prescription.Stimulus.Pre_exhaust 146 { 147 isolation = get "dumbbell-flyes"; 148 compound = get "incline-press"; 149 })) ); 150 ] 151 152 (* {1 One prescribed workout} *) 153 154 (* HD1's Day 1: pecs pre-exhaust, two delt isolations, triceps pre-exhaust. *) 155 let day_one_stimuli = 156 [ 157 pair ~isolation:"dumbbell-flyes" ~compound:"incline-press"; 158 single "laterals"; 159 single "bent-over-laterals"; 160 pair ~isolation:"lying-french-press" ~compound:"dips"; 161 ] 162 163 let workout_tests = 164 [ 165 ( "HD1's Day 1 is prescribable, in order", 166 `Quick, 167 fun () -> 168 let w = 169 Prescription.Workout.make ~name:"Day 1" ~stimuli:day_one_stimuli 170 in 171 Alcotest.(check string) "name" "Day 1" (Prescription.Workout.name w); 172 Alcotest.(check int) 173 "four stimuli" 4 174 (List.length (Prescription.Workout.stimuli w)) ); 175 ( "an empty workout raises Invalid", 176 `Quick, 177 fun () -> 178 match 179 try 180 ignore (Prescription.Workout.make ~name:"Nothing" ~stimuli:[]); 181 false 182 with 183 | Prescription.Workout.Invalid Prescription.Workout.Empty_workout -> 184 true 185 with 186 | true -> () 187 | false -> Alcotest.fail "expected Empty_workout" ); 188 ] 189 190 (* {1 The routine} *) 191 192 let secs = Recovery.duration_to_seconds 193 194 let ideal_day_one = 195 Prescription.Workout.make ~name:"Day 1" 196 ~stimuli: 197 [ 198 pair_with_substitutes 199 ~substitutes:[ "cable-crossovers"; "pec-deck" ] 200 ~isolation:"dumbbell-flyes" ~compound:"incline-press"; 201 single "laterals"; 202 prescribe 203 ~substitutes:[ get "reverse-pec-deck" ] 204 (Prescription.Stimulus.Single (get "bent-over-laterals")); 205 pair_with_substitutes 206 ~substitutes:[ "pressdowns"; "triceps-machine" ] 207 ~isolation:"lying-french-press" ~compound:"dips"; 208 ] 209 210 let ideal_day_two = 211 Prescription.Workout.make ~name:"Day 2" 212 ~stimuli: 213 [ 214 pair_with_substitutes 215 ~substitutes:[ "straight-arm-pulldowns" ] 216 ~isolation:"pullovers" ~compound:"close-grip-pulldowns"; 217 single "bent-over-rows"; 218 single "shrugs"; 219 prescribe 220 ~substitutes:[ get "deadlifts" ] 221 (Prescription.Stimulus.Single (get "hyperextensions")); 222 prescribe 223 ~substitutes:[ get "preacher-curls" ] 224 (Prescription.Stimulus.Single (get "curls")); 225 ] 226 227 let ideal_day_three = 228 Prescription.Workout.make ~name:"Day 3" 229 ~stimuli: 230 [ 231 pair_with_substitutes ~substitutes:[ "squats" ] 232 ~isolation:"leg-extensions" ~compound:"leg-presses"; 233 single "leg-curls"; 234 single "calf-raises"; 235 single "sit-ups"; 236 ] 237 238 let ideal = 239 Prescription.Routine.make ~name:"Ideal Routine" 240 ~workouts:[ ideal_day_one; ideal_day_two; ideal_day_three ] 241 242 let days = Prescription.Routine.workouts ideal 243 244 let nth n = 245 match List.nth_opt days n with 246 | Some w -> w 247 | None -> Alcotest.failf "Ideal Routine has no workout %d" n 248 249 let routine_tests = 250 [ 251 ( "an empty routine raises Invalid", 252 `Quick, 253 fun () -> 254 match 255 try 256 ignore (Prescription.Routine.make ~name:"Nothing" ~workouts:[]); 257 false 258 with 259 | Prescription.Routine.Invalid Prescription.Routine.Empty_routine -> 260 true 261 with 262 | true -> () 263 | false -> Alcotest.fail "expected Empty_routine" ); 264 ( "the Ideal Routine is HD1's three days", 265 `Quick, 266 fun () -> 267 Alcotest.(check string) 268 "name" "Ideal Routine" 269 (Prescription.Routine.name ideal); 270 Alcotest.(check int) "three workouts" 3 (List.length days); 271 Alcotest.(check (list string)) 272 "in order" 273 [ "Day 1"; "Day 2"; "Day 3" ] 274 (List.map Prescription.Workout.name days) ); 275 ( "each day prescribes the movements HD1 lists", 276 `Quick, 277 fun () -> 278 Alcotest.(check (list int)) 279 "stimuli per day" [ 4; 5; 4 ] 280 (List.map 281 (fun w -> List.length (Prescription.Workout.stimuli w)) 282 days) ); 283 ( "Day 1 opens with the pec pre-exhaust and closes with the triceps one", 284 `Quick, 285 fun () -> 286 let ps = Prescription.Workout.stimuli (nth 0) in 287 let names p = 288 List.map Exercise.name (Prescription.Stimulus.exercises p) 289 in 290 Alcotest.(check (list string)) 291 "flyes into incline press" 292 [ "Dumbbell Flyes"; "Incline Presses" ] 293 (names (List.hd ps)); 294 Alcotest.(check (list string)) 295 "french press into dips" 296 [ "Lying French Presses"; "Dips" ] 297 (names (List.nth ps 3)) ); 298 ( "Day 2's superset is the lat pre-exhaust, not an antagonist pairing", 299 `Quick, 300 fun () -> 301 let ps = Prescription.Workout.stimuli (nth 1) in 302 match Prescription.Stimulus.delivery (List.hd ps) with 303 | Prescription.Stimulus.Pre_exhaust { isolation; compound } -> 304 Alcotest.(check string) 305 "isolation" "Pullovers" (Exercise.name isolation); 306 Alcotest.(check string) 307 "compound" "Close-grip, palms-up Pulldowns" 308 (Exercise.name compound) 309 | Prescription.Stimulus.Single _ -> 310 Alcotest.fail "expected a pre-exhaust" ); 311 ( "HD1's alternatives are permitted where it offers them", 312 `Quick, 313 fun () -> 314 let legs = List.hd (Prescription.Workout.stimuli (nth 2)) in 315 Alcotest.(check bool) 316 "squats may replace the leg press" true 317 (Prescription.Stimulus.permits legs (get "squats")); 318 let pecs = List.hd (Prescription.Workout.stimuli (nth 0)) in 319 Alcotest.(check bool) 320 "pec deck may replace flyes" true 321 (Prescription.Stimulus.permits pecs (get "pec-deck")) ); 322 ] 323 324 let rotation_tests = 325 [ 326 ( "the cycle advances and wraps", 327 `Quick, 328 fun () -> 329 let name w = Prescription.Workout.name w in 330 Alcotest.(check string) 331 "1 -> 2" "Day 2" 332 (name (Prescription.Routine.workout_after ideal (nth 0))); 333 Alcotest.(check string) 334 "2 -> 3" "Day 3" 335 (name (Prescription.Routine.workout_after ideal (nth 1))); 336 Alcotest.(check string) 337 "3 wraps to 1" "Day 1" 338 (name (Prescription.Routine.workout_after ideal (nth 2))) ); 339 ( "an unknown workout falls back to the start of the cycle", 340 `Quick, 341 fun () -> 342 let stranger = 343 Prescription.Workout.make ~name:"Elsewhere" 344 ~stimuli:(Prescription.Workout.stimuli (nth 0)) 345 in 346 Alcotest.(check string) 347 "falls back" "Day 1" 348 (Prescription.Workout.name 349 (Prescription.Routine.workout_after ideal stranger)) ); 350 ] 351 352 let rest_tests = 353 [ 354 ( "HD1's intervals are every other day, then two days off", 355 `Quick, 356 fun () -> 357 Alcotest.(check int) 358 "48h" 172_800 359 (secs Prescription.Routine.training_interval); 360 Alcotest.(check int) 361 "72h" 259_200 362 (secs Prescription.Routine.cycle_rest) ); 363 ( "rest within the cycle is 48h, and 72h once it completes", 364 `Quick, 365 fun () -> 366 Alcotest.(check int) 367 "after Day 1" 172_800 368 (secs (Prescription.Routine.recovery_after ideal (nth 0))); 369 Alcotest.(check int) 370 "after Day 2" 172_800 371 (secs (Prescription.Routine.recovery_after ideal (nth 1))); 372 Alcotest.(check int) 373 "after Day 3" 259_200 374 (secs (Prescription.Routine.recovery_after ideal (nth 2))) ); 375 ] 376 377 let suite = 378 [ 379 ("prescription.stimulus.delivery", delivery_tests); 380 ("prescription.stimulus.rep_window", rep_window_tests); 381 ("prescription.stimulus.substitutes", substitute_tests); 382 ("prescription.workout", workout_tests); 383 ("prescription.routine", routine_tests); 384 ("prescription.routine.rotation", rotation_tests); 385 ("prescription.routine.rest", rest_tests); 386 ] 387