1
-
Removed:
(** Unit tests for {!Progression}, authored against progression.mli.
1
+
Added:
(** Unit tests for {!Progression}. *)
2
2
3
-
Removed:
NOTE: Not yet registered in the main runner; wired in during implementation
4
-
Removed:
(Task 8). progression.mli is the source of truth over these assertions.
3
+
Added:
let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok"
5
4
6
-
Removed:
Under test: an improving history is [Progressing]; a flat/declining history
7
-
Removed:
is [Stalled]; the [beats] relation captures progressive overload. *)
5
+
Added:
let get id =
6
+
Added:
match Exercise.find id with
7
+
Added:
| Some e -> e
8
+
Added:
| None -> Alcotest.failf "catalog is missing %S" id
8
9
9
-
Removed:
let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok"
10
+
Added:
let kg n = ok (Units.Weight.of_kg n)
11
+
Added:
let reps n = ok (Units.Reps.of_int n)
12
+
Added:
let at s = Recovery.timestamp_of_unix_seconds s
13
+
Added:
let day n = at (n * 86_400)
14
+
Added:
let secs = Recovery.duration_to_seconds
15
+
Added:
let range lo hi = ok (Units.Rep_range.make ~min:(reps lo) ~max:(reps hi))
16
+
Added:
let six_to_ten = range 6 10
10
17
11
-
Removed:
let sample kg reps : Progression.sample =
12
-
Removed:
{ load = ok (Units.Weight.of_kg kg); reps = ok (Units.Reps.of_int reps) }
18
+
Added:
let move ?(outcome = Stimulus.Positive_failure) ?(id = "laterals") load r =
19
+
Added:
Stimulus.Movement.make ~exercise:(get id) ~load:(kg load) ~reps:(reps r)
20
+
Added:
~outcome
13
21
14
-
Removed:
let progression_tests =
22
+
Added:
(* An observation is a public record, so evidence can be written directly. *)
23
+
Added:
let seen ~on load r : Logbook.observation =
24
+
Added:
{ exercise = get "laterals"; movement = move load r; performed_at = day on }
25
+
Added:
26
+
Added:
let beats_tests =
15
27
[
16
-
Removed:
( "increasing weight progresses",
28
+
Added:
( "one more rep at the same load is progress",
17
29
`Quick,
18
30
fun () ->
19
-
Removed:
let history = [ sample 80.0 6; sample 82.5 6 ] in
20
31
Alcotest.(check bool)
21
-
Removed:
"progressing" true
22
-
Removed:
(Progression.evaluate ~history = Ok Progressing) );
23
-
Removed:
( "more reps at same weight progresses",
32
+
Added:
"8 -> 9" true
33
+
Added:
(Progression.beats ~previous:(move 12. 8) ~current:(move 12. 9)) );
34
+
Added:
( "more load is progress",
24
35
`Quick,
25
36
fun () ->
26
37
Alcotest.(check bool)
27
-
Removed:
"beats" true
28
-
Removed:
(Progression.beats ~previous:(sample 80.0 6) ~current:(sample 80.0 7))
29
-
Removed:
);
30
-
Removed:
( "flat performance stalls",
38
+
Added:
"12 -> 14kg" true
39
+
Added:
(Progression.beats ~previous:(move 12. 8) ~current:(move 14. 8)) );
40
+
Added:
( "more load at fewer reps is still progress",
31
41
`Quick,
32
42
fun () ->
33
-
Removed:
let history = [ sample 80.0 6; sample 80.0 6 ] in
43
+
Added:
(* Raising the load is meant to force you back down the rep range. *)
34
44
Alcotest.(check bool)
35
-
Removed:
"stalled" true
36
-
Removed:
(Progression.evaluate ~history = Ok Stalled) );
37
-
Removed:
( "insufficient data is an error, not a status",
45
+
Added:
"12kg x 12 -> 14kg x 7" true
46
+
Added:
(Progression.beats ~previous:(move 12. 12) ~current:(move 14. 7)) );
47
+
Added:
( "repeating a performance is not progress",
38
48
`Quick,
39
49
fun () ->
40
50
Alcotest.(check bool)
41
-
Removed:
"single sample errors" true
42
-
Removed:
(Progression.evaluate ~history:[ sample 80.0 6 ]
43
-
Removed:
= Error Progression.Insufficient_data) );
51
+
Added:
"identical" false
52
+
Added:
(Progression.beats ~previous:(move 12. 8) ~current:(move 12. 8)) );
53
+
Added:
( "going backwards is not progress",
54
+
Added:
`Quick,
55
+
Added:
fun () ->
56
+
Added:
Alcotest.(check bool)
57
+
Added:
"fewer reps" false
58
+
Added:
(Progression.beats ~previous:(move 12. 8) ~current:(move 12. 7));
59
+
Added:
Alcotest.(check bool)
60
+
Added:
"more reps at a lighter load" false
61
+
Added:
(Progression.beats ~previous:(move 12. 8) ~current:(move 10. 12)) );
44
62
]
45
63
46
-
Removed:
let suite = [ ("progression.evaluate", progression_tests) ]
47
-
Removed:
48
-
Removed:
let band_tests =
49
-
Removed:
let target_reps =
50
-
Removed:
Units.Rep_range.make
51
-
Removed:
~min:(ok (Units.Reps.of_int 6))
52
-
Removed:
~max:(ok (Units.Reps.of_int 8))
53
-
Removed:
|> ok
54
-
Removed:
in
64
+
Added:
let assess_tests =
55
65
[
56
-
Removed:
( "below the band is Below_range",
66
+
Added:
( "the stall window is two weeks",
57
67
`Quick,
58
68
fun () ->
69
+
Added:
Alcotest.(check int) "14 days" 1_209_600 (secs Progression.stall_window)
70
+
Added:
);
71
+
Added:
( "too little record to judge",
72
+
Added:
`Quick,
73
+
Added:
fun () ->
59
74
Alcotest.(check bool)
60
-
Removed:
"below" true
61
-
Removed:
(Progression.classify ~target_reps (sample 80.0 4)
62
-
Removed:
= Progression.Below_range) );
63
-
Removed:
( "within the band is In_range",
75
+
Added:
"nothing" true
76
+
Added:
(Result.is_error (Progression.assess []));
77
+
Added:
Alcotest.(check bool)
78
+
Added:
"a single session" true
79
+
Added:
(Result.is_error (Progression.assess [ seen ~on:1 12. 8 ]));
80
+
Added:
Alcotest.(check bool)
81
+
Added:
"flat but recent" true
82
+
Added:
(Result.is_error
83
+
Added:
(Progression.assess [ seen ~on:1 12. 8; seen ~on:3 12. 8 ])) );
84
+
Added:
( "a recent advance is progress",
64
85
`Quick,
65
86
fun () ->
66
87
Alcotest.(check bool)
67
-
Removed:
"in range" true
68
-
Removed:
(Progression.classify ~target_reps (sample 80.0 7)
69
-
Removed:
= Progression.In_range) );
70
-
Removed:
( "above the band is Above_range",
88
+
Added:
"progressing" true
89
+
Added:
(Progression.assess
90
+
Added:
[ seen ~on:1 12. 8; seen ~on:3 12. 9; seen ~on:5 12. 9 ]
91
+
Added:
= Ok Progression.Progressing) );
92
+
Added:
( "no advance for two weeks is a stall",
71
93
`Quick,
72
94
fun () ->
73
95
Alcotest.(check bool)
74
-
Removed:
"above" true
75
-
Removed:
(Progression.classify ~target_reps (sample 80.0 9)
76
-
Removed:
= Progression.Above_range) );
96
+
Added:
"stalled" true
97
+
Added:
(Progression.assess
98
+
Added:
[ seen ~on:1 12. 8; seen ~on:8 12. 8; seen ~on:16 12. 8 ]
99
+
Added:
= Ok Progression.Stalled) );
100
+
Added:
( "an advance more than two weeks ago is also a stall",
101
+
Added:
`Quick,
102
+
Added:
fun () ->
103
+
Added:
Alcotest.(check bool)
104
+
Added:
"stalled" true
105
+
Added:
(Progression.assess
106
+
Added:
[ seen ~on:1 12. 8; seen ~on:3 12. 9; seen ~on:20 12. 9 ]
107
+
Added:
= Ok Progression.Stalled) );
77
108
]
78
109
79
-
Removed:
let guide_tests =
80
-
Removed:
let target_reps =
81
-
Removed:
Units.Rep_range.make
82
-
Removed:
~min:(ok (Units.Reps.of_int 6))
83
-
Removed:
~max:(ok (Units.Reps.of_int 8))
84
-
Removed:
|> ok
85
-
Removed:
in
110
+
Added:
let remedy_tests =
86
111
[
87
-
Removed:
( "under the band top: next target adds a rep at the same load",
112
+
Added:
( "progress calls for no change",
88
113
`Quick,
89
114
fun () ->
90
-
Removed:
let g = Progression.guide ~target_reps (sample 80.0 6) in
91
-
Removed:
match g.next with
92
-
Removed:
| Progression.Add_reps { load; min_reps } ->
93
-
Removed:
Alcotest.(check (float 0.0001))
94
-
Removed:
"same load" 80.0 (Units.Weight.to_kg load);
95
-
Removed:
Alcotest.(check int) "one more rep" 7 (Units.Reps.to_int min_reps)
96
-
Removed:
| Add_load _ -> Alcotest.fail "expected Add_reps" );
97
-
Removed:
( "at the band top: next target adds load and resets to the bottom",
115
+
Added:
Alcotest.(check bool)
116
+
Added:
"none" true
117
+
Added:
(Option.is_none (Progression.remedy Progression.Progressing)) );
118
+
Added:
( "a stall calls for a lay-off, then less work and more rest",
98
119
`Quick,
99
120
fun () ->
100
-
Removed:
let g = Progression.guide ~target_reps (sample 80.0 8) in
101
-
Removed:
match g.next with
102
-
Removed:
| Progression.Add_load { min_load; reps } ->
103
-
Removed:
Alcotest.(check bool)
104
-
Removed:
"load increased" true
105
-
Removed:
(Units.Weight.to_kg min_load > 80.0);
121
+
Added:
match Progression.remedy Progression.Stalled with
122
+
Added:
| Some
123
+
Added:
(Progression.Lay_off_then_reduce
124
+
Added:
{ lay_off; drop_stimuli_per_workout; extra_rest }) ->
125
+
Added:
Alcotest.(check int) "one week off" 604_800 (secs lay_off);
106
126
Alcotest.(check int)
107
-
Removed:
"resets to band bottom" 6 (Units.Reps.to_int reps)
108
-
Removed:
| Add_reps _ -> Alcotest.fail "expected Add_load" );
109
-
Removed:
( "past the band: next target adds load too",
127
+
Added:
"one fewer stimulus per workout" 1 drop_stimuli_per_workout;
128
+
Added:
Alcotest.(check int) "one extra rest day" 86_400 (secs extra_rest)
129
+
Added:
| None -> Alcotest.fail "a stall must have a remedy" );
130
+
Added:
( "no remedy ever adds work",
110
131
`Quick,
111
132
fun () ->
112
-
Removed:
let g = Progression.guide ~target_reps (sample 80.0 9) in
113
-
Removed:
match g.next with
114
-
Removed:
| Progression.Add_load _ -> ()
115
-
Removed:
| Add_reps _ -> Alcotest.fail "expected Add_load" );
133
+
Added:
(* Structural: the only remedy subtracts. This asserts the intent. *)
134
+
Added:
match Progression.remedy Progression.Stalled with
135
+
Added:
| Some (Progression.Lay_off_then_reduce { drop_stimuli_per_workout; _ })
136
+
Added:
->
137
+
Added:
Alcotest.(check bool)
138
+
Added:
"subtracts volume" true
139
+
Added:
(drop_stimuli_per_workout > 0)
140
+
Added:
| None -> Alcotest.fail "expected a remedy" );
116
141
]
117
142
118
-
Removed:
let prescribe_tests =
119
-
Removed:
let target_reps =
120
-
Removed:
Units.Rep_range.make
121
-
Removed:
~min:(ok (Units.Reps.of_int 6))
122
-
Removed:
~max:(ok (Units.Reps.of_int 8))
123
-
Removed:
|> ok
124
-
Removed:
in
143
+
Added:
let load_tests =
125
144
[
126
-
Removed:
( "prescribe carries the evidence it was derived from",
145
+
Added:
( "the load rises at twelve reps",
127
146
`Quick,
128
147
fun () ->
129
-
Removed:
let evidence = [ sample 80.0 6; sample 82.5 6 ] in
130
-
Removed:
let p = Progression.prescribe ~target_reps ~evidence in
131
148
Alcotest.(check int)
132
-
Removed:
"evidence retained" 2
133
-
Removed:
(List.length p.Progression.evidence) );
134
-
Removed:
( "prescribe's status matches evaluate",
149
+
Added:
"trigger" 12
150
+
Added:
(Units.Reps.to_int Progression.load_increase_trigger) );
151
+
Added:
( "the increase is a 10-20% window",
135
152
`Quick,
136
153
fun () ->
137
-
Removed:
let evidence = [ sample 80.0 6; sample 82.5 6 ] in
138
-
Removed:
let p = Progression.prescribe ~target_reps ~evidence in
154
+
Added:
let low, high = Progression.load_increase ~current:(kg 100.) in
155
+
Added:
Alcotest.(check (float 0.001)) "110kg" 110. (Units.Weight.to_kg low);
156
+
Added:
Alcotest.(check (float 0.001)) "120kg" 120. (Units.Weight.to_kg high) );
157
+
Added:
( "inside the window the load holds",
158
+
Added:
`Quick,
159
+
Added:
fun () ->
139
160
Alcotest.(check bool)
140
-
Removed:
"progressing" true
141
-
Removed:
(p.Progression.status = Progression.Progressing) );
161
+
Added:
"8 reps of 6-10" true
162
+
Added:
(Progression.judge_load ~rep_range:six_to_ten (move 12. 8)
163
+
Added:
= Progression.Hold) );
164
+
Added:
( "eleven reps is slack, not yet a load increase",
165
+
Added:
`Quick,
166
+
Added:
fun () ->
167
+
Added:
(* HD1 raises the load at twelve, not merely above the window. *)
168
+
Added:
Alcotest.(check bool)
169
+
Added:
"11 reps of 6-10 holds" true
170
+
Added:
(Progression.judge_load ~rep_range:six_to_ten (move 12. 11)
171
+
Added:
= Progression.Hold) );
172
+
Added:
( "twelve reps calls for more load",
173
+
Added:
`Quick,
174
+
Added:
fun () ->
175
+
Added:
match Progression.judge_load ~rep_range:six_to_ten (move 100. 12) with
176
+
Added:
| Progression.Increase (low, high) ->
177
+
Added:
Alcotest.(check (float 0.001)) "110kg" 110. (Units.Weight.to_kg low);
178
+
Added:
Alcotest.(check (float 0.001))
179
+
Added:
"120kg" 120. (Units.Weight.to_kg high)
180
+
Added:
| _ -> Alcotest.fail "expected Increase" );
181
+
Added:
( "failing below the window means the load is too heavy",
182
+
Added:
`Quick,
183
+
Added:
fun () ->
184
+
Added:
Alcotest.(check bool)
185
+
Added:
"4 reps of 6-10" true
186
+
Added:
(Progression.judge_load ~rep_range:six_to_ten (move 12. 4)
187
+
Added:
= Progression.Too_heavy) );
142
188
]
143
189
144
-
Removed:
let volume_tests =
190
+
Added:
(* Entries, for the diagnostics. *)
191
+
Added:
let cleared = Option.get (Recovery.clear Recovery.Ready)
192
+
Added:
let workout = List.hd (Routine.workouts Routine.ideal_routine)
193
+
Added:
194
+
Added:
let entry ~clearance ~stimuli =
195
+
Added:
List.fold_left
196
+
Added:
(fun e s -> ok (Entry.add_stimulus e s))
197
+
Added:
(Entry.start workout ~clearance ~started_at:(day 1))
198
+
Added:
stimuli
199
+
Added:
200
+
Added:
let laterals ?outcome load r =
201
+
Added:
ok (Stimulus.make (Stimulus.Single (move ?outcome ~id:"laterals" load r)))
202
+
Added:
203
+
Added:
let extended =
204
+
Added:
Stimulus.Beyond_failure (Stimulus.Forced_reps, [ Stimulus.Negatives ])
205
+
Added:
206
+
Added:
let diagnostic_tests =
145
207
[
146
-
Removed:
( "volume sums load times reps",
208
+
Added:
( "a clean record yields no diagnostics",
147
209
`Quick,
148
210
fun () ->
149
-
Removed:
let v = Progression.volume [ sample 80.0 6; sample 20.0 12 ] in
150
-
Removed:
Alcotest.(check (float 0.0001)) "volume" 720.0 v );
151
-
Removed:
( "empty history has zero volume",
211
+
Added:
let e = entry ~clearance:cleared ~stimuli:[ laterals 12. 8 ] in
212
+
Added:
Alcotest.(check int) "none" 0 (List.length (Progression.diagnose [ e ]))
213
+
Added:
);
214
+
Added:
( "extending every stimulus is flagged",
152
215
`Quick,
153
216
fun () ->
154
-
Removed:
Alcotest.(check (float 0.0001)) "zero" 0.0 (Progression.volume []) );
217
+
Added:
let e =
218
+
Added:
entry ~clearance:cleared ~stimuli:[ laterals ~outcome:extended 12. 8 ]
219
+
Added:
in
220
+
Added:
match Progression.diagnose [ e ] with
221
+
Added:
| [ Progression.Extensions_on_every_stimulus n ] ->
222
+
Added:
Alcotest.(check int) "one workout" 1 n
223
+
Added:
| _ -> Alcotest.fail "expected the extension diagnostic" );
224
+
Added:
( "extending only some stimuli is not flagged",
225
+
Added:
`Quick,
226
+
Added:
fun () ->
227
+
Added:
let e =
228
+
Added:
entry ~clearance:cleared
229
+
Added:
~stimuli:[ laterals ~outcome:extended 12. 8; laterals 10. 9 ]
230
+
Added:
in
231
+
Added:
Alcotest.(check int) "none" 0 (List.length (Progression.diagnose [ e ]))
232
+
Added:
);
233
+
Added:
( "training on an override is flagged",
234
+
Added:
`Quick,
235
+
Added:
fun () ->
236
+
Added:
let recovering =
237
+
Added:
Recovery.evaluate_readiness ~elapsed:(Recovery.hours 12)
238
+
Added:
~recommended:Routine.training_interval
239
+
Added:
in
240
+
Added:
let e =
241
+
Added:
entry
242
+
Added:
~clearance:(Recovery.override recovering ~reason:"impatient")
243
+
Added:
~stimuli:[ laterals 12. 8 ]
244
+
Added:
in
245
+
Added:
match Progression.diagnose [ e ] with
246
+
Added:
| [ Progression.Trained_under_recovered n ] ->
247
+
Added:
Alcotest.(check int) "one workout" 1 n
248
+
Added:
| _ -> Alcotest.fail "expected the under-recovery diagnostic" );
249
+
Added:
( "a stall alongside both habits names both as suspects",
250
+
Added:
`Quick,
251
+
Added:
fun () ->
252
+
Added:
let recovering =
253
+
Added:
Recovery.evaluate_readiness ~elapsed:(Recovery.hours 12)
254
+
Added:
~recommended:Routine.training_interval
255
+
Added:
in
256
+
Added:
let e =
257
+
Added:
entry
258
+
Added:
~clearance:(Recovery.override recovering ~reason:"impatient")
259
+
Added:
~stimuli:[ laterals ~outcome:extended 12. 8 ]
260
+
Added:
in
261
+
Added:
Alcotest.(check int) "both" 2 (List.length (Progression.diagnose [ e ]));
262
+
Added:
Alcotest.(check bool)
263
+
Added:
"and the routine is stalled" true
264
+
Added:
(Progression.assess
265
+
Added:
[ seen ~on:1 12. 8; seen ~on:8 12. 8; seen ~on:16 12. 8 ]
266
+
Added:
= Ok Progression.Stalled) );
155
267
]
156
268
157
269
let suite =
158
-
Removed:
suite
159
-
Removed:
@ [
160
-
Removed:
("progression.band", band_tests);
161
-
Removed:
("progression.guide", guide_tests);
162
-
Removed:
("progression.prescribe", prescribe_tests);
163
-
Removed:
("progression.volume", volume_tests);
164
-
Removed:
]
270
+
Added:
[
271
+
Added:
("progression.beats", beats_tests);
272
+
Added:
("progression.assess", assess_tests);
273
+
Added:
("progression.remedy", remedy_tests);
274
+
Added:
("progression.load", load_tests);
275
+
Added:
("progression.diagnostics", diagnostic_tests);
276
+
Added:
]