diff options
| author | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 11:38:35 +0200 |
|---|---|---|
| committer | Lukasz Kasprzak <lukas@labunix.xyz> | 2026-08-12 11:38:35 +0200 |
| commit | e19adb9c7ea369cafe76e7eff50e9248a4a954d0 (patch) | |
| tree | 57824e121606286ebdf8fff68439986dd3300eae /test/test_sanctoral_ef.ml | |
| parent | 0506388da160a15ceddb0cea1a697d737103d5c4 (diff) | |
| parent | 36df2bd0af9a555a09334e14b0d89118be1dbbc0 (diff) | |
| download | colitur-e19adb9c7ea369cafe76e7eff50e9248a4a954d0.tar.gz colitur-e19adb9c7ea369cafe76e7eff50e9248a4a954d0.zip | |
Merge branch 'ef-plan3': Plan 3, the EF resolution engine
Adds the rite-parameterised Precedence resolver, Liturgical_day, Rite,
and Calendar (year as the primitive, because transfers need whole-year
knowledge), the full EF precedence ruleset (RG 91's 28-entry table,
occurrence RG 92-95, commemorations RG 108-111, transfers RG 96-98),
322 bootstrapped sanctoral entries, colitur day <year>, and validation
layers 3-5.
Layer 3 diffs 16801 days against lectio; layer 4 diffs 730 days against
missalemeum; layer 5 pins ~30 dates on the known-tricky years. Both
comparison layers carry cited allow-lists that name the governing RG
paragraph and which engine is right.
The oracle layer earned its place immediately: Holy Thursday was violet
in colitur and lectio alike, because colitur's data was bootstrapped
from lectio and both carried the same error. Only an independent source
could see it. RG 128(b) and RG 122 name it white.
Diffstat (limited to 'test/test_sanctoral_ef.ml')
| -rw-r--r-- | test/test_sanctoral_ef.ml | 115 |
1 files changed, 115 insertions, 0 deletions
diff --git a/test/test_sanctoral_ef.ml b/test/test_sanctoral_ef.ml new file mode 100644 index 0000000..8fb178f --- /dev/null +++ b/test/test_sanctoral_ef.ml @@ -0,0 +1,115 @@ +(* 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 ] ) |
