(* 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 (* Counts re-derived 2026-08-12 (ef-rebootstrap) against the regenerated source INI directly (`grep -c '^\['` minus the [layer] header itself; `grep '^rank' | sort | uniq -c`) -- NOT copied from a `colitur day`/ bootstrap-tool run, per this project's own "derive the new value, don't transcribe it from output" pinning discipline. The source's SHA-256 moved 6a25e634... -> 1b303ef2..., correcting: 15 entries wrongly `rank = commemoration` that are really class-3 feasts (322 -> 327's own feast/ comm split moves 15 the other way before the net +5 below); 5 entries newly present (agnes-secundo, boniface-martyr, eusebius-confessor, evaristus, theodore -- all Commemoration_only), so 322 + 5 = 327 and 114 - 15 + 5 = 104. Class1 count (12) is untouched by this regeneration -- none of the 20 changed/added entries is Class1. *) let test_load_and_counts () = let l = load () in Alcotest.(check int) "327 entries" 327 (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) "104 Commemoration_only" 104 (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 ] )