summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/test_lms_ordo.ml220
1 files changed, 207 insertions, 13 deletions
diff --git a/test/test_lms_ordo.ml b/test/test_lms_ordo.ml
index c5cae1a..cff4b3d 100644
--- a/test/test_lms_ordo.ml
+++ b/test/test_lms_ordo.ml
@@ -95,6 +95,7 @@ module LD = Colitur_kernel.Liturgical_day
module Date = Colitur_kernel.Date
module MF = Colitur_kernel.Mass_formulary
module V = Rite_ef.Vocab_ef
+module TE = Rite_ef.Temporal_ef
let sanctoral_path = "../data/ef/sanctoral.sexp"
let adjustments_path = "../data/ef/adjustments.sexp"
@@ -314,7 +315,16 @@ let test_creed_matches_or_is_explained () =
let colitur = colitur_rows () in
let allow_list = load_allow_list () in
let by_id = List.map (fun e -> (e.id, e)) allow_list in
- let l1_dates = [ "2025-11-03" ] in
+ (* No known Creed divergence is adjudicated any more: L1 (RG 476(f), All
+ Souls' Day) was CLOSED by fixing {!Rite_ef.Rubrics_ef.creed} itself
+ (whole-branch review finding 1), not by re-adjudicating it -- see
+ expected-divergences-lms.sexp's own closure note for the citation and
+ the measured blast radius. [allow_list] is therefore expected to be
+ empty today; the [by_id]/[explained_counts] wiring below is kept
+ rather than deleted, because the "every declared entry must have
+ fired" check at the end is a real invariant worth keeping live even
+ while the list itself is empty -- exactly the shape a FUTURE entry of
+ this kind would need. *)
let unexplained = ref [] in
let explained_counts = Hashtbl.create 8 in
List.iter2
@@ -323,10 +333,7 @@ let test_creed_matches_or_is_explained () =
match o.creed with
| None -> () (* Good Friday, see test_creed_coverage *)
| Some ocreed ->
- if Bool.equal ocreed c.c_creed then ()
- else if List.mem o.date l1_dates then
- Hashtbl.replace explained_counts "L1" (1 + try Hashtbl.find explained_counts "L1" with Not_found -> 0)
- else unexplained := describe_creed_mismatch o c :: !unexplained)
+ if Bool.equal ocreed c.c_creed then () else unexplained := describe_creed_mismatch o c :: !unexplained)
ordo colitur;
Alcotest.(check (list string)) "every Creed mismatch is named in the allow-list -- none unexplained" []
(List.rev !unexplained);
@@ -403,15 +410,168 @@ let test_bvm_numeral_implies_votive () =
(* Formulary override, restricted to Proper/Preceding_sunday/Common -- *)
(* see this file's own header for why Own_slug is excluded, and Votive is *)
(* covered by the BVM checks above instead. *)
+(* *)
+(* Whole-branch review finding 2: the ORIGINAL version of this test only *)
+(* checked WHETHER a Preceding_sunday day showed an override line, never *)
+(* WHICH Sunday it named -- so colitur could resume the WRONG Sunday on *)
+(* every one of the 66 such days and this layer would stay green (step 3 *)
+(* of the reading chain, {!Rite_ef.Lectionary_ef.readings}, is exactly the *)
+(* code this was supposed to be testing). Fixed by deriving the EXPECTED *)
+(* prose from {!MF.t.said}'s own resolved SLUG -- not by calling colitur's *)
+(* own [sunday_slug] a second time (which would make the check tautological, *)
+(* the same reasoning this file's own header already gives for *)
+(* re-deriving the BVM numeral rather than calling {!Rite_ef. *)
+(* Lectionary_ef.bvm_saturday_citations}). The FIRST version of this fix *)
+(* instead recomputed the preceding Sunday's DATE and re-derived (season, *)
+(* week) via {!Rite_ef.Temporal_ef.temporal} independently -- and was WRONG, caught by its *)
+(* own assertion on the very first run: {!Rite_ef.Temporal_ef.temporal}'s raw [week] field *)
+(* and the SLUG's own embedded number genuinely diverge for the last *)
+(* Sunday before Advent (always slug-numbered "24", whatever the raw week *)
+(* count that year -- temporal_ef.ml's own [sunday_slug], the *)
+(* [same d last_sunday] branch) and for the "surplus Sundays" that resume *)
+(* Epiphany's own numbering when Septuagesima cut it short (same function, *)
+(* the [n > 23] branch) -- exactly the two hard cases that function's own *)
+(* comment names. Parsing the number back OUT of the already-resolved slug *)
+(* sidesteps both: it is not tautological with the wrong-Sunday failure *)
+(* mode this whole fix exists to catch (a wrong slug still parses to a *)
+(* wrong number, which still fails to match the Ordo's real text), and it *)
+(* is simpler and more robust than re-deriving the two special cases a *)
+(* second time by hand. *)
(* ---------------------------------------------------------------------- *)
-let test_formulary_override_by_source () =
+(* English ordinal suffix: 1st, 2nd, 3rd, 4th, 11th, 12th, 13th, 21st... --
+ the "teens" exception applies to every multiple of 100 plus 11-13, not
+ just 11-13 themselves, but nothing in this fixture's own domain (weeks
+ 1-24) ever reaches three digits, so the simpler [n mod 100] test below
+ is exercised fully by 11-13 alone and is not a latent bug for values
+ this function is actually ever called with. *)
+let ordinal n =
+ let suffix =
+ if n mod 100 >= 11 && n mod 100 <= 13 then "th"
+ else
+ match n mod 10 with
+ | 1 -> "st"
+ | 2 -> "nd"
+ | 3 -> "rd"
+ | _ -> "th"
+ in
+ Printf.sprintf "%d%s" n suffix
+
+(* [MF.t.said]'s own slug shape for every Preceding_sunday day observed in
+ the real fixture is [ef-<season-word>-sunday-<n>] ({!Rite_ef.Temporal_ef.sunday_slug}'s
+ own generic branch -- the ONLY branch that ever reaches a weekday's
+ [Preceding_sunday] fallback; the specially-NAMED Sundays [ef-low-sunday],
+ [ef-palm-sunday] etc. are caught by {!Rite_ef.Temporal_ef.named} first and never fall
+ through to it). Manual parsing, not [Str]/regex (deps frozen): finds the
+ last "-sunday-" marker and splits on it, since a season word can itself
+ contain hyphens ("time-after-pentecost") but never contains "-sunday-". *)
+let parse_sunday_slug slug =
+ let prefix = "ef-" in
+ let plen = String.length prefix in
+ if String.length slug <= plen || String.sub slug 0 plen <> prefix then None
+ else
+ let body = String.sub slug plen (String.length slug - plen) in
+ let marker = "-sunday-" in
+ let mlen = String.length marker and blen = String.length body in
+ let rec find_last i best = if i + mlen > blen then best
+ else find_last (i + 1) (if String.sub body i mlen = marker then Some i else best)
+ in
+ match find_last 0 None with
+ | None -> None
+ | Some i -> (
+ let word = String.sub body 0 i in
+ let num_str = String.sub body (i + mlen) (blen - i - mlen) in
+ match int_of_string_opt num_str with Some n -> Some (word, n) | None -> None)
+
+(* The Ordo's own prose, derived from [MF.t.said]'s own (season-word,
+ number) -- verified against every one of the 66 real Preceding_sunday
+ days in the fixture (test_formulary_override_matches below), not
+ invented from the naming convention alone. [None] means "this pair is
+ EXCLUDED from the mapping", handled explicitly by the caller, never
+ silently -- see [ascension_week_override] just below for the one real
+ population that lands there. *)
+let expected_preceding_sunday_override (word : string) (n : int) =
+ match (word, n) with
+ | "advent", 1 -> Some "Advent Sunday"
+ | "advent", 2 -> Some "2nd Sunday of Advent"
+ | "advent", 3 -> Some "3rd Sunday of Advent (Gaudete Sunday)"
+ | "advent", 4 -> Some "4th Sunday of Advent"
+ | "septuagesima", 1 -> Some "Septuagesima Sunday"
+ | "septuagesima", 2 -> Some "Sexagesima Sunday"
+ | "septuagesima", 3 -> Some "Quinquagesima Sunday"
+ | "time-after-epiphany", n -> Some (Printf.sprintf "%s Sunday after the Epiphany" (ordinal n))
+ | "time-after-pentecost", 24 -> Some "24th & Last Sunday After Pentecost"
+ | "time-after-pentecost", n -> Some (Printf.sprintf "%s Sunday after Pentecost" (ordinal n))
+ (* [ef-easter-sunday-<n>]: Paschaltide's own numbering counts Easter
+ Sunday itself as 1 (Low Sunday, named separately via {!Rite_ef.Temporal_ef.named} and
+ so never reaching this generic branch at all, is 2), so the ordinal
+ PRINTED is [n - 1] -- confirmed against every one of n=3..5 in the
+ fixture ("2nd" through "4th" Sunday after Easter). n=6,7 are
+ deliberately NOT given here: see [ascension_week_override]. *)
+ | "easter", n when n >= 3 && n <= 5 -> Some (Printf.sprintf "%s Sunday after Easter" (ordinal (n - 1)))
+ | _ -> None
+
+(* One NAMED Sunday (not a generic "ef-<season>-sunday-<n>" slug at all)
+ reaches [Preceding_sunday] in the real 66-day population: Christ the
+ King ({!Rite_ef.Temporal_ef.christ_the_king}, "the last Sunday of
+ October"), whose own ferias resume it the same way any other Sunday's
+ would. Unlike the Septuagesima trio's fixed proper names, its ORDINAL
+ position among the Sundays after Pentecost is not fixed -- it varies
+ year to year with Easter's own date -- so there is no string this
+ function could hard-code the way [expected_preceding_sunday_override]
+ does for the others. [word]/[n] cannot express it EITHER, because
+ {!Rite_ef.Temporal_ef.named} overrides the SLUG for that Sunday but,
+ confirmed directly in temporal_ef.ml ([build ~season ~slug ~colour
+ ~rank ~week:(week d) ()], called identically whether or not [named]
+ matched), never the raw [week] FIELD -- so re-deriving that one field
+ for this one named exception is not the same mistake the first version
+ of this fix made re-deriving [week] WHOLESALE (that failed on the
+ season's own two special-numbering cases, both of which this function
+ sidesteps by reading the slug directly); here there is no slug number
+ to read in the first place, so [week] is the only source of truth,
+ narrowly applied to the single slug that needs it. *)
+let expected_named_sunday_override slug ~year =
+ if String.equal slug "ef-christ-the-king" then
+ match TE.week (TE.christ_the_king year) with
+ | Some n -> Some (Printf.sprintf "%s Sunday after Pentecost" (ordinal n))
+ | None -> None
+ else None
+
+(* [n] = 6 or 7 (Paschaltide's own numbering, see the comment above -- the
+ calendar Sundays traditionally called the 5th and 6th Sunday after
+ Easter) are STRUCTURALLY, not coincidentally, always Ascension-adjacent:
+ the Ascension is a fixed Easter+39 (a Thursday inside week 6), so EVERY
+ feria whose own [Preceding_sunday] fallback would otherwise resolve to
+ one of these two weeks falls between Ascension Thursday and the
+ following Saturday, every year, not merely in this fixture's own
+ 2024-2025 window. A REAL finding, not a fixture artefact: the Ordo
+ shows "Mass of the Ascension" on all three such days this window
+ contains (2025-05-30, 2025-06-02, 2025-06-03 -- Friday and the
+ following Monday/Tuesday, the days between Ascension and the Sunday
+ after it that have no proper of their own), never "5th"/"6th Sunday
+ after Easter" -- the Missal keeps saying ASCENSION's own Mass through
+ this stretch, not the last numbered Sunday's. colitur's step 3 has no
+ notion of "inside Ascension's own after-feast period" -- it always
+ resumes the nearest PRECEDING SUNDAY, numbered or not -- so on these
+ three real days {!MF.t.said} is a plain "ef-easter-sunday-6"/"-7" slug
+ the Missal would never actually print as such. A genuine content gap in
+ colitur's reading chain, found by this fix, NOT fixed here (fixing it
+ means teaching step 3 or the lectionary data about Ascension's own
+ after-feast period, a lectionary-data change out of this fix round's
+ own scope) -- recorded honestly, the same way L1 in
+ expected-divergences-lms.sexp already records the 476(f) gap this same
+ branch closed, and counted below so a change to this population would
+ be caught rather than silently absorbed. *)
+let ascension_week_override = "Mass of the Ascension"
+
+let test_formulary_override_matches () =
let ordo = ordo_rows () in
let colitur = colitur_rows () in
let pairs = List.combine ordo colitur in
let counts = Hashtbl.create 8 in
let bump k = Hashtbl.replace counts k (1 + try Hashtbl.find counts k with Not_found -> 0) in
let bad = ref [] in
+ let ascension_week_count = ref 0 in
List.iter
(fun (o, c) ->
match c.c_formulary with
@@ -425,17 +585,51 @@ let test_formulary_override_by_source () =
if o.formulary_override <> None then
bad := Printf.sprintf "%s: Common day but Ordo shows override %s" o.date (Option.get o.formulary_override)
:: !bad
- | Some { MF.via = MF.Preceding_sunday; _ } ->
+ | Some { MF.via = MF.Preceding_sunday; said } -> (
bump "preceding_sunday";
- if o.formulary_override = None then bad := Printf.sprintf "%s: Preceding_sunday day but Ordo shows no override" o.date :: !bad
+ let slug = Colitur_kernel.Slug.to_string said in
+ let ordo_says = Printf.sprintf "Mass of %s" in
+ (* Try the generic [ef-<season>-sunday-<n>] shape first; fall back
+ to the one NAMED Sunday that also reaches this population
+ ([expected_named_sunday_override], see its own citation) when
+ the slug doesn't parse that way at all. *)
+ let year = int_of_string (String.sub o.date 0 4) in
+ let expected =
+ match parse_sunday_slug slug with
+ | Some (word, n) -> expected_preceding_sunday_override word n
+ | None -> expected_named_sunday_override slug ~year
+ in
+ match expected with
+ | Some expected -> (
+ let expected_line = ordo_says expected in
+ match o.formulary_override with
+ | Some got when String.equal got expected_line -> ()
+ | Some got -> bad := Printf.sprintf "%s: colitur resumed %S, Ordo says %S" o.date expected_line got :: !bad
+ | None -> bad := Printf.sprintf "%s: colitur resumed %S, Ordo shows no override" o.date expected_line :: !bad)
+ | None -> (
+ (* The Ascension-week exclusion above -- checked, not
+ assumed: fails loudly if the Ordo's own text ever stops
+ matching the one string this population is adjudicated to
+ carry, or if a slug outside every adjudicated case above
+ ever reaches here. *)
+ match o.formulary_override with
+ | Some got when String.equal got ascension_week_override -> incr ascension_week_count
+ | Some got ->
+ bad :=
+ Printf.sprintf "%s: unmapped (colitur resumed %S) and Ordo shows %S, not the adjudicated %S" o.date
+ slug got ascension_week_override
+ :: !bad
+ | None ->
+ bad := Printf.sprintf "%s: unmapped (colitur resumed %S) and Ordo shows no override" o.date slug :: !bad))
| Some { MF.via = MF.Votive; _ } | Some { MF.via = MF.Own_slug; _ } | None -> ())
pairs;
- Alcotest.(check (list string)) "no unexplained formulary-override mismatch on Proper/Common/Preceding_sunday days"
- [] (List.rev !bad);
+ Alcotest.(check (list string)) "every formulary-override mismatch is explained -- none unexplained" [] (List.rev !bad);
Alcotest.(check int) "179 Proper days in the window" 179 (try Hashtbl.find counts "proper" with Not_found -> 0);
Alcotest.(check int) "2 Common days in the window" 2 (try Hashtbl.find counts "common" with Not_found -> 0);
Alcotest.(check int) "66 Preceding_sunday days in the window" 66
- (try Hashtbl.find counts "preceding_sunday" with Not_found -> 0)
+ (try Hashtbl.find counts "preceding_sunday" with Not_found -> 0);
+ Alcotest.(check int) "3 Preceding_sunday days fall in the Ascension-week exclusion, no more, no fewer" 3
+ !ascension_week_count
let test_via_distribution_totals_400 () =
let colitur = colitur_rows () in
@@ -461,7 +655,7 @@ let suite =
Alcotest.test_case "every BVM-Saturday numeral matches its season" `Quick test_bvm_seasonal_selection;
Alcotest.test_case "every Ordo BVM numeral day is a colitur Votive day" `Quick
test_bvm_numeral_implies_votive;
- Alcotest.test_case "formulary override matches on Proper/Common/Preceding_sunday days" `Quick
- test_formulary_override_by_source;
+ Alcotest.test_case "formulary override STRING matches on Proper/Common/Preceding_sunday days" `Quick
+ test_formulary_override_matches;
Alcotest.test_case "the via distribution sums to the full window" `Quick test_via_distribution_totals_400
] )