(** Unit tests for {!Recovery}. *) let secs d = Recovery.duration_to_seconds d let at s = Recovery.timestamp_of_unix_seconds s let time_tests = [ ( "hours and days convert to seconds", `Quick, fun () -> Alcotest.(check int) "48h" 172_800 (secs (Recovery.hours 48)); Alcotest.(check int) "2d" 172_800 (secs (Recovery.days 2)) ); ( "timestamps round-trip", `Quick, fun () -> Alcotest.(check int) "unix seconds" 1_700_000_000 (Recovery.timestamp_to_unix_seconds (at 1_700_000_000)) ); ( "elapsed measures the gap between workouts", `Quick, fun () -> Alcotest.(check int) "48h apart" 172_800 (secs (Recovery.elapsed ~since:(at 0) ~now:(at 172_800))) ); ( "elapsed never goes negative", `Quick, fun () -> Alcotest.(check int) "clamped" 0 (secs (Recovery.elapsed ~since:(at 172_800) ~now:(at 0))) ); ] let readiness_tests = [ ( "the recommended window having passed means ready", `Quick, fun () -> let r = Recovery.evaluate_readiness ~elapsed:(Recovery.hours 48) ~recommended:(Recovery.hours 48) in Alcotest.(check bool) "ready" true (Recovery.is_ready r) ); ( "short of the window means still recovering", `Quick, fun () -> match Recovery.evaluate_readiness ~elapsed:(Recovery.hours 24) ~recommended:(Recovery.hours 48) with | Recovery.Ready -> Alcotest.fail "expected Recovering" | Recovery.Recovering { rested; recommended } -> Alcotest.(check int) "rested" 86_400 (secs rested); Alcotest.(check int) "recommended" 172_800 (secs recommended) ); ] let clearance_tests = [ ( "recovery earns a clearance", `Quick, fun () -> match Recovery.clear Recovery.Ready with | Some c -> ( match Recovery.basis c with | Recovery.Recovered -> () | Recovery.Overridden _ -> Alcotest.fail "expected Recovered") | None -> Alcotest.fail "expected a clearance" ); ( "an incomplete recovery earns none", `Quick, fun () -> let recovering = Recovery.evaluate_readiness ~elapsed:(Recovery.hours 12) ~recommended:(Recovery.hours 48) in Alcotest.(check bool) "refused" true (Option.is_none (Recovery.clear recovering)) ); ( "an override is granted and records the recovery shortfall", `Quick, fun () -> let recovering = Recovery.evaluate_readiness ~elapsed:(Recovery.hours 12) ~recommended:(Recovery.hours 48) in let c = Recovery.override recovering in match Recovery.basis c with | Recovery.Overridden { rested; recommended } -> Alcotest.(check int) "rested" 43_200 (secs rested); Alcotest.(check int) "recommended" 172_800 (secs recommended) | Recovery.Recovered -> Alcotest.fail "expected Overridden" ); ( "overriding when already recovered is simply recovered", `Quick, fun () -> match Recovery.basis (Recovery.override Recovery.Ready) with | Recovery.Recovered -> () | Recovery.Overridden _ -> Alcotest.fail "no recovery was outstanding to override" ); ] let suite = [ ("recovery.time", time_tests); ("recovery.readiness", readiness_tests); ("recovery.clearance", clearance_tests); ]