summaryrefslogtreecommitdiff
path: root/test/test_validate.ml
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_validate.ml')
-rw-r--r--test/test_validate.ml109
1 files changed, 98 insertions, 11 deletions
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;