[OCaml] High Intensity Training Online
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