diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/cli.t | 2 | ||||
| -rw-r--r-- | test/test_calendar.ml | 7 | ||||
| -rw-r--r-- | test/test_lectionary_ef.ml | 51 | ||||
| -rw-r--r-- | test/test_validate.ml | 109 |
4 files changed, 152 insertions, 17 deletions
@@ -452,7 +452,7 @@ CSV run rather than one per year: sexp and xml are also available: $ colitur emit --format sexp --from 2027 --to 2027 | wc -l - 8472 + 8881 $ colitur emit --format xml --from 2027 --to 2027 | head -2 <?xml version="1.0" encoding="UTF-8"?> diff --git a/test/test_calendar.ml b/test/test_calendar.ml index 132a466..599dfb2 100644 --- a/test/test_calendar.ml +++ b/test/test_calendar.ml @@ -99,9 +99,10 @@ module Fixture = struct let rec search d = if (occupant d).Cel.rank = Lo then d else search (D.add_days d 1) in search (D.add_days origin 1) - (* No fixture here exercises citations -- readings is a harmless constant - [], the same role [empty_layer] plays for the sanctoral side. *) - let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = [] + (* No fixture here exercises citations or the formulary -- readings is a + harmless constant [(None, [])], the same role [empty_layer] plays for + the sanctoral side. *) + let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, []) let rite : (season, rank) Rite.t = { Rite.id = "synthetic-calendar"; vocab; year_start; temporal; anchors = (fun _ -> []); diff --git a/test/test_lectionary_ef.ml b/test/test_lectionary_ef.ml index d56d187..bc2be8c 100644 --- a/test/test_lectionary_ef.ml +++ b/test/test_lectionary_ef.ml @@ -467,7 +467,7 @@ let test_step4_unreachable_commons_still_resolve () = let for_saint s = Lectionary_ef.commons_for ~commons (Slug.of_string_exn s) in let refs_of = function | None -> [ "<no common>" ] - | Some cs -> List.map (fun c -> c.Citation.reference) cs + | Some (_id, cs) -> List.map (fun c -> c.Citation.reference) cs in Alcotest.(check (list string)) "St Benedict (21 March), Common of Abbots" @@ -580,6 +580,51 @@ let test_commons_load_rejects_bad_data () = (Slug.of_string_exn "benedict") = None) +(* ---------------------------------------------------------------------- *) +(* The formulary itself (Task 2): each step of the chain now reports HOW *) +(* it resolved, not only what it resolved. One day per step -- the same *) +(* dates this file already uses (and hand-verifies) elsewhere for the *) +(* citations those days carry, so no new date needs independent checking. *) +(* ---------------------------------------------------------------------- *) + +let formulary_cases = + [ (* step 1: a saint with his own proper -- same date as + [test_step1_proper_beats_any_common_john_of_god]. *) + (2038, 3, 8, "john-of-god", Colitur_kernel.Mass_formulary.Proper); + (* step 2: the day's own temporal slug -- same date as + [test_step2_lenten_feria_has_its_own], Monday of Lent I. *) + (2026, 2, 23, "ef-lent-1-monday", Colitur_kernel.Mass_formulary.Own_slug); + (* step 3: a feria resuming the preceding Sunday. The task brief's own + snippet pinned this date against week 9 ("ef-time-after-pentecost- + sunday-9"); running the real resolver against 2026 shows 3 August + 2026 is Monday of week 10, resuming 2 August's "...sunday-10" -- + corrected per the brief's own "find the dates by running the current + binary if they drift" instruction. *) + (2026, 8, 3, "ef-time-after-pentecost-sunday-10", + Colitur_kernel.Mass_formulary.Preceding_sunday); + (* step 4: a saint sent to a Common -- same date as + [test_step4_commons_perpetua_and_felicity]; [said] is the Common's + OWN id (data/ef/commons.sexp), not the saint's slug. *) + (2038, 3, 6, "common-of-non-virgins-1", Colitur_kernel.Mass_formulary.Common) ] + +let test_formulary_reports_its_source () = + List.iter + (fun (y, m, d, expected_slug, expected_via) -> + let day = day y m d in + match day.Colitur_kernel.Liturgical_day.formulary with + | None -> Alcotest.failf "%04d-%02d-%02d: no formulary" y m d + | Some f -> + Alcotest.(check string) + (Printf.sprintf "%04d-%02d-%02d slug" y m d) + expected_slug + (Colitur_kernel.Slug.to_string f.Colitur_kernel.Mass_formulary.said); + Alcotest.(check string) + (Printf.sprintf "%04d-%02d-%02d source" y m d) + (Colitur_kernel.Mass_formulary.source_to_string expected_via) + (Colitur_kernel.Mass_formulary.source_to_string + f.Colitur_kernel.Mass_formulary.via)) + formulary_cases + let suite = [ ("step 1: sanctoral proper", `Quick, test_step1_sanctoral_proper); ("step 2: own temporal proper", `Quick, test_step2_lenten_feria_has_its_own); @@ -618,4 +663,6 @@ let suite = ("the five propers no real year reaches are present", `Quick, test_step4_unreachable_propers_are_present); ("Commons.load rejects the four silent-degradation defects", `Quick, - test_commons_load_rejects_bad_data) ] + test_commons_load_rejects_bad_data); + ("the formulary reports its own source, one day per step", `Quick, + test_formulary_reports_its_source) ] diff --git a/test/test_validate.ml b/test/test_validate.ml index 8b3014a..7387971 100644 --- a/test/test_validate.ml +++ b/test/test_validate.ml @@ -300,15 +300,16 @@ module Synthetic = struct defaults against the default empty [layer], since nothing ever contests the temporal office there) so the resolution fixtures further down can override them without duplicating every other field. *) - (* Most fixtures here exercise no citations -- [readings] is a harmless - constant [], the same role the other placeholder defaults above play, - and {!Validate}'s own citation checks are gated on a rite producing SOME - citation somewhere, so a constant [] leaves them entirely dormant. Task + (* Most fixtures here exercise no citations and no formulary -- [readings] + is a harmless constant [(None, [])], the same role the other + placeholder defaults above play, and {!Validate}'s own citation and + formulary checks are gated on a rite producing SOME citation/formulary + somewhere, so a constant [(None, [])] leaves them entirely dormant. Task 10 makes it overridable ([?readings] below) so the citation fixtures at the end of this file can drive those checks directly, exactly as every - other check here is driven -- rather than leaving two kernel checks with - no committed proof that they can fire at all. *) - let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = [] + other check here is driven -- rather than leaving kernel checks with no + committed proof that they can fire at all. *) + let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, []) (* The shape {!Validate} accepts: exactly one First and one Gospel. The references are deliberately nonsense -- these fixtures assert SHAPE, @@ -317,6 +318,12 @@ module Synthetic = struct [ { Citation.part = Citation.First; reference = "Synth 1:1" }; { Citation.part = Citation.Gospel; reference = "Synth 2:2" } ] + (* The formulary equivalent of [well_formed_citations] above -- shape only, + never rubrically meaningful content. *) + let well_formed_formulary = + { Colitur_kernel.Mass_formulary.said = Slug.of_string_exn "syn-formulary"; + via = Colitur_kernel.Mass_formulary.Own_slug } + let rite ?(vocab = vocab) ?(anchors = fun _ -> []) ?(season_runs = [ A; B ]) ?(rules = rules) ?(transfer_target = fun _ origin _ -> origin) ?(readings = readings) temporal : (season, rank) Rite.t = @@ -759,7 +766,7 @@ let test_citations_silent_without_a_lectionary () = (* The positive: well-formed citations on every day report nothing. *) let test_citations_clean_when_well_formed () = - let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = well_formed_citations in + let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, well_formed_citations) in let fs = run ~readings good in Alcotest.(check bool) "neither citation check fires when every day carries First + Gospel" false (has_check "citations" fs || has_check "citations-unresolved" fs) @@ -769,7 +776,7 @@ let test_citations_clean_when_well_formed () = plausibly produce -- half a lookup succeeding. *) let test_citations_fires_on_a_lone_epistle () = let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = - [ { Citation.part = Citation.First; reference = "Synth 1:1" } ] + (None, [ { Citation.part = Citation.First; reference = "Synth 1:1" } ]) in Alcotest.(check bool) "citations check fires when a day carries an Epistle but no Gospel" true (has_check "citations" (run ~readings good)) @@ -780,7 +787,7 @@ let test_citations_fires_on_a_lone_epistle () = even though the day is otherwise a well-formed pair. *) let test_citations_fires_on_an_out_of_scope_part () = let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = - { Citation.part = Citation.Tract; reference = "Synth 3:3" } :: well_formed_citations + (None, { Citation.part = Citation.Tract; reference = "Synth 3:3" } :: well_formed_citations) in Alcotest.(check bool) "citations check fires when a part outside First/Gospel appears" true (has_check "citations" (run ~readings good)) @@ -792,7 +799,7 @@ let test_citations_fires_on_an_out_of_scope_part () = this file singles out. *) let test_citations_unresolved_fires_on_a_gap () = let readings ~observed:_ ~temporal:_ ~date ~temporal_at:_ = - if D.compare date target = 0 then [] else well_formed_citations + if D.compare date target = 0 then (None, []) else (None, well_formed_citations) in Alcotest.(check bool) "citations-unresolved fires when one day of the year resolves nothing" true (has_check "citations-unresolved" (run ~readings good)); @@ -801,6 +808,79 @@ let test_citations_unresolved_fires_on_a_gap () = Alcotest.(check bool) "the well-formedness check stays silent on a pure coverage gap" false (has_check "citations" (run ~readings good)) +(* ---------------------------------------------------------------------- *) +(* The formulary invariant (Task 3, celebrant-rubrics-phase1): the same *) +(* negative-path discipline the citation checks above already hold *) +(* themselves to, driven through the same synthetic fixture. Model on the *) +(* citations trio above -- "same shape, its own name" is what {!Validate} *) +(* itself now does, so the tests proving it can fire follow the same *) +(* pattern. *) +(* ---------------------------------------------------------------------- *) + +(* The gate: a rite that resolves no formulary at all (the default constant + [(None, [])]) must report nothing -- not "usually", not "on this year". *) +let test_formulary_silent_without_a_lectionary () = + let fs = run good in + Alcotest.(check bool) "no formulary check fires for a rite with no readings at all" false + (has_check "formulary" fs) + +(* The positive: a formulary on every day reports nothing. *) +let test_formulary_clean_when_well_formed () = + let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = + (Some well_formed_formulary, well_formed_citations) + in + Alcotest.(check bool) "formulary check stays silent when every day resolves one" false + (has_check "formulary" (run ~readings good)) + +(* The coverage gap: a rite that resolves a formulary on most days but falls + through on one. On real EF data this has no witness at all (Task 3's own + [test_every_day_has_a_formulary] below confirms it directly), so this + fixture is the only thing that holds it honest. *) +let test_formulary_fires_on_a_gap () = + let readings ~observed:_ ~temporal:_ ~date ~temporal_at:_ = + if D.compare date target = 0 then (None, []) + else (Some well_formed_formulary, well_formed_citations) + in + Alcotest.(check bool) "formulary check fires when one day of the year resolves none" true + (has_check "formulary" (run ~readings good)) + +(* ---------------------------------------------------------------------- *) +(* Direct real-EF-data coverage (Task 3 brief): every day of every year in *) +(* the sample resolves a formulary, the same discipline the "citations" *) +(* checks already hold EF to -- asserted directly against *) +(* [Liturgical_day.t] rather than through [Validate.run]'s failure list, *) +(* so a bug in [Validate]'s own gating could not hide this gap. *) +(* ---------------------------------------------------------------------- *) + +(* 2005-2050: the same 46-year sample test_rite_ef.ml's own [sample_years] + uses, for the same reason -- non-trivial, deterministic, and already the + project's differential-testing window (CLAUDE.md). Defined locally + rather than shared: test executables in this project cross-reference + only [.suite] values (test_colitur.ml), never each other's internal + helpers. *) +let sample_years = + let rec range a b = if a > b then [] else a :: range (a + 1) b in + range 2005 2050 + +let year_of y = Colitur_kernel.Calendar.year real_ef_rite real_ef_layer y + +(* Every day of every year resolves a formulary, for the same reason + [Validate] already asserts exactly one First and one Gospel: a day that + says no Mass at all is a defect, not a gap. Mirrors the "citations" + check. *) +let test_every_day_has_a_formulary () = + let missing = ref [] in + List.iter + (fun y -> + Array.iter + (fun (d : (_, _) Colitur_kernel.Liturgical_day.t) -> + if d.Colitur_kernel.Liturgical_day.formulary = None then + missing := + Colitur_kernel.Date.to_iso8601 d.Colitur_kernel.Liturgical_day.date :: !missing) + (year_of y)) + sample_years; + Alcotest.(check (list string)) "every day resolves a formulary" [] !missing + let suite = ( "Validate", [ Alcotest.test_case "landmark years" `Quick test_landmark_years; @@ -830,6 +910,13 @@ let suite = test_citations_fires_on_an_out_of_scope_part; Alcotest.test_case "citations-unresolved fires on a gap" `Quick test_citations_unresolved_fires_on_a_gap; + Alcotest.test_case "formulary silent without a lectionary" `Quick + test_formulary_silent_without_a_lectionary; + Alcotest.test_case "formulary clean when well formed" `Quick + test_formulary_clean_when_well_formed; + Alcotest.test_case "formulary fires on a gap" `Quick test_formulary_fires_on_a_gap; + Alcotest.test_case "every day (2005-2050) resolves a formulary" `Quick + test_every_day_has_a_formulary; Alcotest.test_case "lost fires on resolution exception" `Quick test_lost_fires_on_resolution_exception; Alcotest.test_case "duplicated fires" `Quick test_duplicated_fires; Alcotest.test_case "unconverged fires" `Quick test_unconverged_fires; |
