[OCaml] High Intensity Training Online
1
(** Tests for {!Hito_persistence.Import_codec} — the length-prefixed framing for
2
import batches and promoted records. The framing must preserve arbitrary
3
bytes in every free-text field, and a decode must rebuild the batch and its
4
promotion exactly, so {!Already_promoted} and edited re-promotion survive a
5
reconnect. *)
6
7
open Hito_app
8
open Hito_persistence
9
module WI = Workout_import
10
11
let get id =
12
match Exercise.find_id id with
13
| Some e -> e
14
| None -> Alcotest.failf "catalog is missing %S" id
15
16
let at s = Recovery.timestamp_of_unix_seconds s
17
let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok"
18
let find_exercise (id : Exercise.id) = Exercise.find_id (id :> string)
19
let wid = WI.workout_id
20
21
(* Awkward text: commas, newlines, colons, and non-ASCII UTF-8. A length-prefixed
22
field must carry every byte verbatim. *)
23
let nasty = "a,b\nc:d\t\"quote\" — café 🏋️, 12:corrupt\n99:end"
24
25
let set ?(row = 0) ?(set_index = 1) ?(superset_id = None) ~name ~kind ~kg ~reps
26
() : WI.source_set =
27
{
28
row;
29
exercise_name = name;
30
set_index;
31
set_type = kind;
32
weight_kg = kg;
33
reps;
34
superset_id;
35
}
36
37
(* A batch with a failure set and a normal set, both under one awkward exercise
38
name, plus an awkward warning and awkward source text. *)
39
let sample_batch () =
40
let sets =
41
[
42
set ~row:1 ~name:nasty ~kind:WI.Failure ~kg:102.5 ~reps:8 ();
43
set ~row:2 ~name:nasty ~kind:WI.Normal ~kg:60.0 ~reps:10
44
~superset_id:(Some 3) ();
45
]
46
in
47
let w =
48
ok
49
(WI.make_workout ~id:(wid "w1") ~title:nasty ~started_at:(at 1_000)
50
~ended_at:(at 4_600) ~sets)
51
in
52
let warnings =
53
[
54
{
55
WI.row = 7;
56
exercise_name = Some nasty;
57
kind = WI.Unsupported_row nasty;
58
};
59
]
60
in
61
WI.make_batch ~id:(WI.batch_id "b1") ~source:nasty ~fingerprint:"fp-🔒"
62
~imported_at:(at 5_000) ~workouts:[ w ] ~warnings
63
64
(* Map, acknowledge, and promote, so the snapshot carries a promotion. *)
65
let promoted_batch () =
66
let b = sample_batch () in
67
let b =
68
ok (WI.map_exercise b ~workout:(wid "w1") ~source_name:nasty (get "squats"))
69
in
70
let b =
71
ok (WI.confirm_normal_sets b ~workout:(wid "w1") ~acknowledgement:nasty)
72
in
73
let b, _ = ok (WI.promote_workout b (wid "w1")) in
74
b
75
76
let roundtrip_batch b =
77
let encoded = Import_codec.encode_snapshot (WI.snapshot b) in
78
match Import_codec.decode_snapshot ~find_exercise encoded with
79
| Ok b' -> b'
80
| Error e -> Alcotest.failf "decode failed: %a" Import_codec.pp_error e
81
82
let codec_tests =
83
[
84
( "an unpromoted batch round-trips, preserving special characters",
85
`Quick,
86
fun () ->
87
let b = sample_batch () in
88
let d = roundtrip_batch b in
89
Alcotest.(check string)
90
"source verbatim" (WI.batch_source b) (WI.batch_source d);
91
Alcotest.(check string)
92
"fingerprint verbatim" (WI.batch_fingerprint b)
93
(WI.batch_fingerprint d);
94
Alcotest.(check int)
95
"imported_at seconds"
96
(Recovery.timestamp_to_unix_seconds (WI.batch_imported_at b))
97
(Recovery.timestamp_to_unix_seconds (WI.batch_imported_at d));
98
let w = Option.get (WI.find_workout d (wid "w1")) in
99
Alcotest.(check string) "title verbatim" nasty (WI.workout_title w);
100
Alcotest.(check int) "two sets" 2 (List.length (WI.workout_sets w));
101
match WI.batch_warnings d with
102
| [ { kind = WI.Unsupported_row detail; exercise_name = Some name; _ } ]
103
->
104
Alcotest.(check string) "warning detail verbatim" nasty detail;
105
Alcotest.(check string) "warning name verbatim" nasty name
106
| _ -> Alcotest.fail "expected one unsupported-row warning" );
107
( "a mapping round-trips and stays confirmed",
108
`Quick,
109
fun () ->
110
let b = sample_batch () in
111
let b =
112
ok
113
(WI.map_exercise b ~workout:(wid "w1") ~source_name:nasty
114
(get "squats"))
115
in
116
let d = roundtrip_batch b in
117
let w = Option.get (WI.find_workout d (wid "w1")) in
118
match WI.workout_mapping w ~source_name:nasty with
119
| Some e ->
120
Alcotest.(check bool)
121
"mapped to squats" true
122
(Exercise.equal e (get "squats"))
123
| None -> Alcotest.fail "expected the mapping to survive" );
124
( "a promotion round-trips, preserving acknowledgement and revision",
125
`Quick,
126
fun () ->
127
let b = promoted_batch () in
128
let d = roundtrip_batch b in
129
let w = Option.get (WI.find_workout d (wid "w1")) in
130
Alcotest.(check string)
131
"acknowledgement verbatim" nasty
132
(Option.get (WI.workout_acknowledgement w));
133
(* Re-promoting the decoded batch at the same revision is idempotent:
134
the promotion survived the round-trip. *)
135
let _, result = ok (WI.promote_workout d (wid "w1")) in
136
match result with
137
| WI.Already_promoted p ->
138
Alcotest.(check int)
139
"two observations" 2
140
(List.length p.observations);
141
Alcotest.(check (option string))
142
"normal confirmation verbatim" (Some nasty)
143
p.provenance.normal_confirmation
144
| WI.Promoted _ ->
145
Alcotest.fail "expected Already_promoted, not a fresh promotion"
146
| WI.Blocked _ -> Alcotest.fail "expected Already_promoted, not Blocked"
147
);
148
( "an unknown exercise id in a snapshot is reported, not raised",
149
`Quick,
150
fun () ->
151
let b = promoted_batch () in
152
let encoded = Import_codec.encode_snapshot (WI.snapshot b) in
153
(* Resolve nothing: every id is unknown to this catalog view. *)
154
match
155
Import_codec.decode_snapshot ~find_exercise:(fun _ -> None) encoded
156
with
157
| Error (Import_codec.Unknown_exercise _) -> ()
158
| _ -> Alcotest.fail "expected Unknown_exercise" );
159
( "truncated framing is reported as malformed",
160
`Quick,
161
fun () ->
162
match
163
Import_codec.decode_snapshot ~find_exercise "100:not-that-long"
164
with
165
| Error (Import_codec.Malformed _) -> ()
166
| _ -> Alcotest.fail "expected Malformed" );
167
]
168
169
(* A promoted record built straight from a promotion, for the record codec. *)
170
let sample_record () : Repository.imported_record =
171
let b = promoted_batch () in
172
match WI.snapshot b with
173
| { workouts = [ { promotion = Some p; id; _ } ]; batch_id; _ } ->
174
let observations =
175
List.map
176
(fun (o : WI.observation_snapshot) ->
177
let exercise = get (o.exercise_id :> string) in
178
let effort =
179
Result.get_ok
180
(Evidence.Stimulus.Effort.make ~exercise ~load:o.load
181
~reps:o.reps ~outcome:Evidence.Stimulus.Positive_failure)
182
in
183
Evidence.Observation.make ~exercise ~effort
184
~performed_at:o.performed_at)
185
p.observations
186
in
187
{
188
batch_id;
189
workout_id = id;
190
revision = p.revision;
191
title = p.title;
192
started_at = p.started_at;
193
ended_at = p.ended_at;
194
observations;
195
provenance =
196
{
197
WI.batch_id;
198
workout_id = id;
199
source_rows = p.source_rows;
200
normal_confirmation = p.normal_confirmation;
201
revision = p.revision;
202
};
203
}
204
| _ -> Alcotest.fail "expected one promoted workout in the snapshot"
205
206
let record_tests =
207
[
208
( "a promoted record round-trips, preserving special characters",
209
`Quick,
210
fun () ->
211
let r = sample_record () in
212
let encoded = Import_codec.encode_record r in
213
match Import_codec.decode_record encoded with
214
| Ok d ->
215
Alcotest.(check string) "title verbatim" nasty d.Repository.title;
216
Alcotest.(check int) "revision" r.revision d.revision;
217
Alcotest.(check int)
218
"same observation count"
219
(List.length r.observations)
220
(List.length d.observations);
221
Alcotest.(check (option string))
222
"normal confirmation verbatim" (Some nasty)
223
d.provenance.normal_confirmation;
224
Alcotest.(check (list int))
225
"source rows" r.provenance.source_rows d.provenance.source_rows
226
| Error e -> Alcotest.failf "decode failed: %a" Import_codec.pp_error e
227
);
228
( "truncated record framing is reported, not raised",
229
`Quick,
230
fun () ->
231
let r = sample_record () in
232
let encoded = Import_codec.encode_record r in
233
(* Drop the final byte: the framing no longer lines up. *)
234
let truncated = String.sub encoded 0 (String.length encoded - 1) in
235
match Import_codec.decode_record truncated with
236
| Error _ -> ()
237
| Ok _ -> Alcotest.fail "expected a decode error on truncated text" );
238
]
239
240
let suite = [ ("import_codec", codec_tests); ("import_record", record_tests) ]
241