(** Tests for the workout-import persistence operations on {!Test_repo}: per-trainee isolation, deterministic ordering, that deleting a provisional batch leaves promoted records, and that a promoted record is replaced by workout id. *) open Hito_app module WI = Workout_import let run = Lwt_main.run let get id = match Exercise.find_id id with | Some e -> e | None -> Alcotest.failf "catalog is missing %S" id let at s = Recovery.timestamp_of_unix_seconds s let ok = function Ok v -> v | Error _ -> Alcotest.fail "expected Ok" let wid = WI.workout_id (* A trainee id minted directly; the import operations never consult the account table. *) let trainee n = Trainee.id (Printf.sprintf "t%d" n) let set ~name ~kind ~kg ~reps : WI.source_set = { row = 1; exercise_name = name; set_index = 1; set_type = kind; weight_kg = kg; reps; superset_id = None; } let batch ~id ~imported_at = let sets = [ set ~name:"Barbell Squat" ~kind:WI.Failure ~kg:100. ~reps:8 ] in let w = ok (WI.make_workout ~id:(wid "w1") ~title:"Legs" ~started_at:(at 10) ~ended_at:(at 3_610) ~sets) in WI.make_batch ~id:(WI.batch_id id) ~source:"raw" ~fingerprint:"fp" ~imported_at ~workouts:[ w ] ~warnings:[] let record ~batch_id ~workout_id ~revision ~ended_at ~load : Repository.imported_record = let exercise = get "squats" in let effort = Result.get_ok (Evidence.Stimulus.Effort.make ~exercise ~load ~reps:8 ~outcome:Evidence.Stimulus.Positive_failure) in let obs = Evidence.Observation.make ~exercise ~effort ~performed_at:ended_at in { batch_id = WI.batch_id batch_id; workout_id = WI.workout_id workout_id; revision; title = "Legs"; started_at = at 10; ended_at; observations = [ obs ]; provenance = { WI.batch_id = WI.batch_id batch_id; workout_id = WI.workout_id workout_id; source_rows = [ 1 ]; normal_confirmation = None; revision; }; } let batch_ids bs = List.map (fun b -> WI.batch_id_to_string (WI.batch_identity b)) bs let repository_tests = [ ( "batches and records are isolated per trainee", `Quick, fun () -> let repo = Test_repo.create () in let a = trainee 1 and b = trainee 2 in run (Test_repo.save_import_batch repo a (batch ~id:"ba" ~imported_at:(at 100))); run (Test_repo.replace_imported_record repo a (record ~batch_id:"ba" ~workout_id:"w1" ~revision:0 ~ended_at:(at 3_610) ~load:100.)); Alcotest.(check int) "b has no batches" 0 (List.length (run (Test_repo.import_batches repo b))); Alcotest.(check int) "b has no records" 0 (List.length (run (Test_repo.imported_records repo b))); Alcotest.(check int) "a has one batch" 1 (List.length (run (Test_repo.import_batches repo a))) ); ( "batches list most recent imported_at first, ties by id descending", `Quick, fun () -> let repo = Test_repo.create () in let a = trainee 1 in run (Test_repo.save_import_batch repo a (batch ~id:"b1" ~imported_at:(at 100))); run (Test_repo.save_import_batch repo a (batch ~id:"b3" ~imported_at:(at 300))); run (Test_repo.save_import_batch repo a (batch ~id:"b2a" ~imported_at:(at 200))); run (Test_repo.save_import_batch repo a (batch ~id:"b2b" ~imported_at:(at 200))); Alcotest.(check (list string)) "newest first, id tie-break descending" [ "b3"; "b2b"; "b2a"; "b1" ] (batch_ids (run (Test_repo.import_batches repo a))) ); ( "saving a batch with the same id replaces it", `Quick, fun () -> let repo = Test_repo.create () in let a = trainee 1 in run (Test_repo.save_import_batch repo a (batch ~id:"b1" ~imported_at:(at 100))); run (Test_repo.save_import_batch repo a (batch ~id:"b1" ~imported_at:(at 500))); let bs = run (Test_repo.import_batches repo a) in Alcotest.(check int) "still one batch" 1 (List.length bs); Alcotest.(check int) "imported_at updated" 500 (Recovery.timestamp_to_unix_seconds (WI.batch_imported_at (List.hd bs))) ); ( "find_import_batch returns the stored batch or none", `Quick, fun () -> let repo = Test_repo.create () in let a = trainee 1 in run (Test_repo.save_import_batch repo a (batch ~id:"b1" ~imported_at:(at 100))); Alcotest.(check bool) "found" true (Option.is_some (run (Test_repo.find_import_batch repo a (WI.batch_id "b1")))); Alcotest.(check bool) "absent" false (Option.is_some (run (Test_repo.find_import_batch repo a (WI.batch_id "nope")))) ); ( "deleting a batch leaves promoted records untouched", `Quick, fun () -> let repo = Test_repo.create () in let a = trainee 1 in run (Test_repo.save_import_batch repo a (batch ~id:"b1" ~imported_at:(at 100))); run (Test_repo.replace_imported_record repo a (record ~batch_id:"b1" ~workout_id:"w1" ~revision:0 ~ended_at:(at 3_610) ~load:100.)); let deleted = run (Test_repo.delete_import_batch repo a (WI.batch_id "b1")) in Alcotest.(check bool) "batch deleted" true deleted; Alcotest.(check int) "no batches remain" 0 (List.length (run (Test_repo.import_batches repo a))); Alcotest.(check int) "record survives" 1 (List.length (run (Test_repo.imported_records repo a))) ); ( "deleting an absent batch reports false", `Quick, fun () -> let repo = Test_repo.create () in let a = trainee 1 in Alcotest.(check bool) "nothing to delete" false (run (Test_repo.delete_import_batch repo a (WI.batch_id "ghost"))) ); ( "a promoted record is replaced by workout id", `Quick, fun () -> let repo = Test_repo.create () in let a = trainee 1 in run (Test_repo.replace_imported_record repo a (record ~batch_id:"b1" ~workout_id:"w1" ~revision:0 ~ended_at:(at 3_610) ~load:100.)); run (Test_repo.replace_imported_record repo a (record ~batch_id:"b1" ~workout_id:"w1" ~revision:1 ~ended_at:(at 3_610) ~load:110.)); let records = run (Test_repo.imported_records repo a) in Alcotest.(check int) "still one record" 1 (List.length records); Alcotest.(check int) "revision advanced" 1 (List.hd records).Repository.revision; let load = Evidence.Stimulus.Effort.load (Evidence.Observation.effort (List.hd (List.hd records).Repository.observations)) in Alcotest.(check (float 0.001)) "load replaced" 110. load ); ( "records list most recent ended_at first", `Quick, fun () -> let repo = Test_repo.create () in let a = trainee 1 in run (Test_repo.replace_imported_record repo a (record ~batch_id:"b1" ~workout_id:"w1" ~revision:0 ~ended_at:(at 1_000) ~load:100.)); run (Test_repo.replace_imported_record repo a (record ~batch_id:"b1" ~workout_id:"w2" ~revision:0 ~ended_at:(at 9_000) ~load:100.)); run (Test_repo.replace_imported_record repo a (record ~batch_id:"b1" ~workout_id:"w3" ~revision:0 ~ended_at:(at 5_000) ~load:100.)); let ended = List.map (fun r -> Recovery.timestamp_to_unix_seconds r.Repository.ended_at) (run (Test_repo.imported_records repo a)) in Alcotest.(check (list int)) "newest ended_at first" [ 9_000; 5_000; 1_000 ] ended ); ] let suite = [ ("import_repository", repository_tests) ]