(* Loads the bootstrapped data/ef/sanctoral.sexp (Task 10, tools/ bootstrap_sanctoral.ml) through Layer.load and checks it against counts independently derived from the source INI itself (grep -c '^\[', grep '^rank' | sort | uniq -c -- see the task report), plus two named spot-checks against the INI's own text so a passing count can't hide the wrong 322 entries having been converted (task brief's "live hazard"). *) module L = Colitur_kernel.Layer module Cel = Colitur_kernel.Celebration module S = Colitur_kernel.Slug module DS = Colitur_kernel.Date_spec module Col = Colitur_kernel.Colour module Sub = Colitur_kernel.Subject module Lang = Colitur_kernel.Lang module Names = Colitur_kernel.Names module V = Rite_ef.Vocab_ef module PE = Rite_ef.Precedence_ef (* Relative to this test's own build directory (_build/default/test/); made available there because test/dune declares it as a dep of the test stanza. *) let path = "../data/ef/sanctoral.sexp" let load () = match L.load V.rank_of_sexp path with | Ok l -> l | Error e -> Alcotest.failf "%s: failed to load: %s" path e let mkdate m d = match DS.fixed ~month:m ~day:d with Ok t -> t | Error e -> failwith e let test_load_and_counts () = let l = load () in Alcotest.(check int) "322 entries" 322 (List.length l.L.entries); let commemoration_only = List.filter (fun e -> e.L.cel.Cel.status = Cel.Commemoration_only) l.L.entries in Alcotest.(check int) "114 Commemoration_only" 114 (List.length commemoration_only); let class1 = List.filter (fun e -> e.L.cel.Cel.rank = V.Class1) l.L.entries in Alcotest.(check int) "12 Class1" 12 (List.length class1); let temporal_subjects = List.filter (fun e -> e.L.cel.Cel.subject = Sub.Temporal) l.L.entries in Alcotest.(check int) "no Subject.Temporal" 0 (List.length temporal_subjects); (* Every slug already had to pass Slug.of_string during load (Slug.t_of_sexp is the validating parser -- an invalid slug would have failed the whole Layer.load with Error, never landing here silently). Re-checking is still worth doing explicitly, rather than resting entirely on "load didn't error", so a future loader change that weakens that guarantee has a test that would catch it directly. *) List.iter (fun e -> match S.of_string (S.to_string e.L.cel.Cel.slug) with | Ok _ -> () | Error msg -> Alcotest.failf "slug %s failed re-validation: %s" (S.to_string e.L.cel.Cel.slug) msg) l.L.entries; (* All 322 dates are Date_spec.Fixed month/day pairs constructed via Date_spec.fixed, which already rejects an out-of-range day for that month against the LEAP-year maximum -- so every one must resolve in a leap year (2028: divisible by 4, not by 100). *) List.iter (fun e -> match DS.resolve e.L.date ~year:2028 with | Some _ -> () | None -> Alcotest.failf "slug %s: date does not resolve in leap year 2028" (S.to_string e.L.cel.Cel.slug)) l.L.entries let find l slug = match L.find l (S.of_string_exn slug) with | Some e -> e | None -> Alcotest.failf "slug %s not found in %s" slug path (* Named spot-check 1: one of the 6 entries carrying an explicit `class` field in the source (INI lines: date=02-02, rank=class-2, colour=white, class=lord, name.en/name.pl as below). Exercises decision 1's non-default branch -- an entry where subject must NOT fall back to Saint. *) let test_spot_check_explicit_class () = let l = load () in let e = find l "purification-of-the-blessed-virgin-mary" in let cel = e.L.cel in Alcotest.(check bool) "date 02-02" true (e.L.date = mkdate 2 2); Alcotest.(check bool) "rank Class2" true (cel.Cel.rank = V.Class2); Alcotest.(check bool) "status Feast" true (cel.Cel.status = Cel.Feast); Alcotest.(check bool) "colour White" true (cel.Cel.colour = Col.White); Alcotest.(check bool) "subject Lord (from class = lord)" true (cel.Cel.subject = Sub.Lord); Alcotest.(check (option string)) "name.en" (Some "Purification of the Blessed Virgin Mary") (Names.find cel.Cel.names (Lang.of_string_exn "en")); Alcotest.(check (option string)) "name.pl" (Some "Oczyszczenie N. M. P.") (Names.find cel.Cel.names (Lang.of_string_exn "pl")); Alcotest.(check string) "layer tagged universal (Precedence_ef.universal_layer)" PE.universal_layer cel.Cel.layer (* Named spot-check 2: a `rank = commemoration` entry (INI lines: date=01-14, rank=commemoration, colour=white, no class, name.en/name.pl as below). Exercises decision 2 (Commemoration_only + inferred Class3) AND decision 1's default branch (no class field -> Saint) on the SAME entry. *) let test_spot_check_commemoration () = let l = load () in let e = find l "maur-abbot" in let cel = e.L.cel in Alcotest.(check bool) "date 01-15" true (e.L.date = mkdate 1 15); Alcotest.(check bool) "rank inferred Class3" true (cel.Cel.rank = V.Class3); Alcotest.(check bool) "status Commemoration_only" true (cel.Cel.status = Cel.Commemoration_only); Alcotest.(check bool) "colour White" true (cel.Cel.colour = Col.White); Alcotest.(check bool) "subject defaults to Saint (no class field)" true (cel.Cel.subject = Sub.Saint); Alcotest.(check (option string)) "name.en" (Some "St. Maur, Abbot") (Names.find cel.Cel.names (Lang.of_string_exn "en")); Alcotest.(check (option string)) "name.pl" (Some "św. Maura, Opata") (Names.find cel.Cel.names (Lang.of_string_exn "pl")) let suite = ( "Sanctoral_ef (data/ef/sanctoral.sexp)", [ Alcotest.test_case "load succeeds and counts match the source INI" `Quick test_load_and_counts; Alcotest.test_case "spot-check: explicit class = lord (Purification of the BVM)" `Quick test_spot_check_explicit_class; Alcotest.test_case "spot-check: rank = commemoration (St. Maur, Abbot)" `Quick test_spot_check_commemoration ] )