View raw

1 (** Tests for the workout-import persistence operations on {!Test_repo}: 2 per-trainee isolation, deterministic ordering, that deleting a provisional 3 batch leaves promoted records, and that a promoted record is replaced by 4 workout id. *) 5 6 open Hito_app 7 module WI = Workout_import 8 9 let run = Lwt_main.run 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 wid = WI.workout_id 19 20 (* A trainee id minted directly; the import operations never consult the 21 account table. *) 22 let trainee n = Trainee.id (Printf.sprintf "t%d" n) 23 24 let set ~name ~kind ~kg ~reps : WI.source_set = 25 { 26 row = 1; 27 exercise_name = name; 28 set_index = 1; 29 set_type = kind; 30 weight_kg = kg; 31 reps; 32 superset_id = None; 33 } 34 35 let batch ~id ~imported_at = 36 let sets = [ set ~name:"Barbell Squat" ~kind:WI.Failure ~kg:100. ~reps:8 ] in 37 let w = 38 ok 39 (WI.make_workout ~id:(wid "w1") ~title:"Legs" ~started_at:(at 10) 40 ~ended_at:(at 3_610) ~sets) 41 in 42 WI.make_batch ~id:(WI.batch_id id) ~source:"raw" ~fingerprint:"fp" 43 ~imported_at ~workouts:[ w ] ~warnings:[] 44 45 let record ~batch_id ~workout_id ~revision ~ended_at ~load : 46 Repository.imported_record = 47 let exercise = get "squats" in 48 let effort = 49 Result.get_ok 50 (Evidence.Stimulus.Effort.make ~exercise ~load ~reps:8 51 ~outcome:Evidence.Stimulus.Positive_failure) 52 in 53 let obs = 54 Evidence.Observation.make ~exercise ~effort ~performed_at:ended_at 55 in 56 { 57 batch_id = WI.batch_id batch_id; 58 workout_id = WI.workout_id workout_id; 59 revision; 60 title = "Legs"; 61 started_at = at 10; 62 ended_at; 63 observations = [ obs ]; 64 provenance = 65 { 66 WI.batch_id = WI.batch_id batch_id; 67 workout_id = WI.workout_id workout_id; 68 source_rows = [ 1 ]; 69 normal_confirmation = None; 70 revision; 71 }; 72 } 73 74 let batch_ids bs = 75 List.map (fun b -> WI.batch_id_to_string (WI.batch_identity b)) bs 76 77 let repository_tests = 78 [ 79 ( "batches and records are isolated per trainee", 80 `Quick, 81 fun () -> 82 let repo = Test_repo.create () in 83 let a = trainee 1 and b = trainee 2 in 84 run 85 (Test_repo.save_import_batch repo a 86 (batch ~id:"ba" ~imported_at:(at 100))); 87 run 88 (Test_repo.replace_imported_record repo a 89 (record ~batch_id:"ba" ~workout_id:"w1" ~revision:0 90 ~ended_at:(at 3_610) ~load:100.)); 91 Alcotest.(check int) 92 "b has no batches" 0 93 (List.length (run (Test_repo.import_batches repo b))); 94 Alcotest.(check int) 95 "b has no records" 0 96 (List.length (run (Test_repo.imported_records repo b))); 97 Alcotest.(check int) 98 "a has one batch" 1 99 (List.length (run (Test_repo.import_batches repo a))) ); 100 ( "batches list most recent imported_at first, ties by id descending", 101 `Quick, 102 fun () -> 103 let repo = Test_repo.create () in 104 let a = trainee 1 in 105 run 106 (Test_repo.save_import_batch repo a 107 (batch ~id:"b1" ~imported_at:(at 100))); 108 run 109 (Test_repo.save_import_batch repo a 110 (batch ~id:"b3" ~imported_at:(at 300))); 111 run 112 (Test_repo.save_import_batch repo a 113 (batch ~id:"b2a" ~imported_at:(at 200))); 114 run 115 (Test_repo.save_import_batch repo a 116 (batch ~id:"b2b" ~imported_at:(at 200))); 117 Alcotest.(check (list string)) 118 "newest first, id tie-break descending" 119 [ "b3"; "b2b"; "b2a"; "b1" ] 120 (batch_ids (run (Test_repo.import_batches repo a))) ); 121 ( "saving a batch with the same id replaces it", 122 `Quick, 123 fun () -> 124 let repo = Test_repo.create () in 125 let a = trainee 1 in 126 run 127 (Test_repo.save_import_batch repo a 128 (batch ~id:"b1" ~imported_at:(at 100))); 129 run 130 (Test_repo.save_import_batch repo a 131 (batch ~id:"b1" ~imported_at:(at 500))); 132 let bs = run (Test_repo.import_batches repo a) in 133 Alcotest.(check int) "still one batch" 1 (List.length bs); 134 Alcotest.(check int) 135 "imported_at updated" 500 136 (Recovery.timestamp_to_unix_seconds 137 (WI.batch_imported_at (List.hd bs))) ); 138 ( "find_import_batch returns the stored batch or none", 139 `Quick, 140 fun () -> 141 let repo = Test_repo.create () in 142 let a = trainee 1 in 143 run 144 (Test_repo.save_import_batch repo a 145 (batch ~id:"b1" ~imported_at:(at 100))); 146 Alcotest.(check bool) 147 "found" true 148 (Option.is_some 149 (run (Test_repo.find_import_batch repo a (WI.batch_id "b1")))); 150 Alcotest.(check bool) 151 "absent" false 152 (Option.is_some 153 (run (Test_repo.find_import_batch repo a (WI.batch_id "nope")))) ); 154 ( "deleting a batch leaves promoted records untouched", 155 `Quick, 156 fun () -> 157 let repo = Test_repo.create () in 158 let a = trainee 1 in 159 run 160 (Test_repo.save_import_batch repo a 161 (batch ~id:"b1" ~imported_at:(at 100))); 162 run 163 (Test_repo.replace_imported_record repo a 164 (record ~batch_id:"b1" ~workout_id:"w1" ~revision:0 165 ~ended_at:(at 3_610) ~load:100.)); 166 let deleted = 167 run (Test_repo.delete_import_batch repo a (WI.batch_id "b1")) 168 in 169 Alcotest.(check bool) "batch deleted" true deleted; 170 Alcotest.(check int) 171 "no batches remain" 0 172 (List.length (run (Test_repo.import_batches repo a))); 173 Alcotest.(check int) 174 "record survives" 1 175 (List.length (run (Test_repo.imported_records repo a))) ); 176 ( "deleting an absent batch reports false", 177 `Quick, 178 fun () -> 179 let repo = Test_repo.create () in 180 let a = trainee 1 in 181 Alcotest.(check bool) 182 "nothing to delete" false 183 (run (Test_repo.delete_import_batch repo a (WI.batch_id "ghost"))) ); 184 ( "a promoted record is replaced by workout id", 185 `Quick, 186 fun () -> 187 let repo = Test_repo.create () in 188 let a = trainee 1 in 189 run 190 (Test_repo.replace_imported_record repo a 191 (record ~batch_id:"b1" ~workout_id:"w1" ~revision:0 192 ~ended_at:(at 3_610) ~load:100.)); 193 run 194 (Test_repo.replace_imported_record repo a 195 (record ~batch_id:"b1" ~workout_id:"w1" ~revision:1 196 ~ended_at:(at 3_610) ~load:110.)); 197 let records = run (Test_repo.imported_records repo a) in 198 Alcotest.(check int) "still one record" 1 (List.length records); 199 Alcotest.(check int) 200 "revision advanced" 1 (List.hd records).Repository.revision; 201 let load = 202 Evidence.Stimulus.Effort.load 203 (Evidence.Observation.effort 204 (List.hd (List.hd records).Repository.observations)) 205 in 206 Alcotest.(check (float 0.001)) "load replaced" 110. load ); 207 ( "records list most recent ended_at first", 208 `Quick, 209 fun () -> 210 let repo = Test_repo.create () in 211 let a = trainee 1 in 212 run 213 (Test_repo.replace_imported_record repo a 214 (record ~batch_id:"b1" ~workout_id:"w1" ~revision:0 215 ~ended_at:(at 1_000) ~load:100.)); 216 run 217 (Test_repo.replace_imported_record repo a 218 (record ~batch_id:"b1" ~workout_id:"w2" ~revision:0 219 ~ended_at:(at 9_000) ~load:100.)); 220 run 221 (Test_repo.replace_imported_record repo a 222 (record ~batch_id:"b1" ~workout_id:"w3" ~revision:0 223 ~ended_at:(at 5_000) ~load:100.)); 224 let ended = 225 List.map 226 (fun r -> Recovery.timestamp_to_unix_seconds r.Repository.ended_at) 227 (run (Test_repo.imported_records repo a)) 228 in 229 Alcotest.(check (list int)) 230 "newest ended_at first" [ 9_000; 5_000; 1_000 ] ended ); 231 ] 232 233 let suite = [ ("import_repository", repository_tests) ] 234