aboutsummaryrefslogtreecommitdiff
path: root/test/test_lectionary_ef.ml
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_lectionary_ef.ml')
-rw-r--r--test/test_lectionary_ef.ml106
1 files changed, 84 insertions, 22 deletions
diff --git a/test/test_lectionary_ef.ml b/test/test_lectionary_ef.ml
index e28476d..71888e3 100644
--- a/test/test_lectionary_ef.ml
+++ b/test/test_lectionary_ef.ml
@@ -64,6 +64,16 @@ let day y m d =
(Rite_ef.context ~lectionary:(real_lectionary ()) ~commons:(real_commons ()))
(real_layer ()) date
+(* Same resolution, with a caller-supplied Commons table instead of the
+ committed one -- the seam the whole design rests on, and the only way to
+ exercise step 4's guard, which is deliberately a no-op on shipped data. *)
+let day_with ~commons y m d =
+ let date = match Date.make ~year:y ~month:m ~day:d with
+ | Ok x -> x | Error e -> Alcotest.fail e in
+ Calendar.day
+ (Rite_ef.context ~lectionary:(real_lectionary ()) ~commons)
+ (real_layer ()) date
+
let refs (ld : _ Liturgical_day.t) =
List.map (fun c -> c.Citation.reference) ld.citations
@@ -272,19 +282,64 @@ let test_step1_proper_francis_of_paola () =
[ "Phil 3:7-12"; "Luke 12:32-34" ]
(refs (day 2008 4 2))
-(* Step 4 must never divert a day whose observed office IS its temporal
- office. The guard in [readings] makes that structural; this pins it
- behaviourally on the case most at risk -- the RG 78 Saturday Office of
- the BVM, which deliberately REUSES the ordinary ferial slug (see
- temporal_ef.ml's [bvm_saturday_names], "Slug" paragraph). If that shared
- slug were ever assigned a Common, every feria sharing it would change
- Mass. 4 July 2026 is such a Saturday; it keeps its own ferial Mass. *)
-let test_step4_never_diverts_a_temporal_office () =
+(* A day whose observed office IS its temporal office is untouched by step 4
+ on the SHIPPED data. 4 July 2026 is such a day, and the case most at risk:
+ it is an RG 78 Saturday Office of the BVM, which deliberately REUSES the
+ ordinary ferial slug (temporal_ef.ml's [bvm_saturday_names], "Slug"
+ paragraph).
+
+ This test alone does NOT pin the guard -- it passes with the guard
+ removed, because no shipped assignment names a temporal slug. It is the
+ baseline the next test needs, and it is labelled as such (fix round 1,
+ coordinator review: its comment previously claimed to "pin the guard
+ behaviourally", which was false -- the reviewer forced
+ [sanctoral_office = true] and all 357 tests stayed green). *)
+let test_step4_leaves_a_temporal_office_alone_on_shipped_data () =
Alcotest.(check (list string))
- "4 July 2026 (a BVM Saturday) keeps its ferial Mass, unaffected by step 4"
+ "4 July 2026 (a BVM Saturday) keeps its ferial Mass"
[ "1 Pet 3:8-15."; "Matt 5:20-24." ]
(refs (day 2026 7 4))
+(* THE GUARD, pinned. The guard is a no-op on shipped data by design, so the
+ only way to exercise it is to build the table it defends against: a
+ SYNTHETIC [Commons.t] that assigns a Common to a really-observed FERIAL
+ slug. That is exactly the future-overlay mistake the guard exists to
+ catch -- and note the blast radius it would have, since a ferial slug is
+ shared by every year in which that week and weekday coincide, not by one
+ date.
+
+ Built through [Commons.of_tables], so it passes the same validation the
+ committed file does -- this is a well-formed table with a wrong
+ assignment, not malformed data caught by the loader.
+
+ TEETH (verified, fix round 1): with the guard removed this returns the
+ Common of Abbots' Ecclus 45:1-6 / Matt 19:27-29 and the assertion fails.
+ The second check is what makes the first one mean something: it proves
+ the hostile assignment really is present and resolvable, so the ferial
+ Mass wins because the GUARD refused it, not because the lookup missed. *)
+let test_step4_guard_refuses_a_common_assigned_to_a_ferial_slug () =
+ let ferial_slug = Slug.of_string_exn "ef-time-after-pentecost-5-saturday" in
+ let abbots = Slug.of_string_exn "common-of-abbots" in
+ let hostile =
+ match
+ Lectionary_ef.Commons.of_tables
+ ~commons:
+ [ (abbots,
+ [ { Citation.part = Citation.First; reference = "Ecclus 45:1-6" };
+ { Citation.part = Citation.Gospel; reference = "Matt 19:27-29" } ]) ]
+ ~assigned:[ (ferial_slug, abbots) ]
+ with
+ | Ok c -> c
+ | Error e -> Alcotest.failf "synthetic commons failed to build: %s" e
+ in
+ Alcotest.(check bool)
+ "the synthetic table really does resolve that ferial slug to a Common" true
+ (Lectionary_ef.commons_for ~commons:hostile ferial_slug <> None);
+ Alcotest.(check (list string))
+ "4 July 2026 keeps its ferial Mass even so: step 4 refuses a temporal office"
+ [ "1 Pet 3:8-15."; "Matt 5:20-24." ]
+ (refs (day_with ~commons:hostile 2026 7 4))
+
(* THE DATA ASSERTION, and the one that cannot drift: after Task 6, every
sanctoral entry that can ever BE the observed office -- i.e. every
[status Feast] entry -- either carries its own proper or has a Common
@@ -416,17 +471,22 @@ let test_step4_unreachable_propers_are_present () =
Written against strings rather than the committed file so the committed
file stays clean. *)
let test_commons_load_rejects_bad_data () =
- let write name contents =
- let path = Filename.concat (Filename.get_temp_dir_name ()) name in
+ (* [Filename.temp_file] + [Fun.protect], exactly as test_lectionary.ml's
+ own [with_temp_file] does it (fix round 1, coordinator review: this
+ used four FIXED names in the temp dir and removed none of them, so
+ concurrent runs collided and the files leaked). *)
+ let with_temp_file contents f =
+ let path = Filename.temp_file "commons_test" ".sexp" in
let oc = open_out path in
output_string oc contents;
close_out oc;
- path
+ Fun.protect ~finally:(fun () -> Sys.remove path) (fun () -> f path)
in
- let err name contents =
- match Lectionary_ef.Commons.load (write name contents) with
- | Ok _ -> Alcotest.failf "%s: expected Error, got Ok" name
- | Error e -> e
+ let err what contents =
+ with_temp_file contents (fun path ->
+ match Lectionary_ef.Commons.load path with
+ | Ok _ -> Alcotest.failf "%s: expected Error, got Ok" what
+ | Error e -> e)
in
let good_common = "(c-a (((part First) (reference \"A 1:1\")) ((part Gospel) (reference \"B 2:2\"))))" in
let contains needle haystack =
@@ -439,15 +499,15 @@ let test_commons_load_rejects_bad_data () =
(contains needle msg)
in
check_msg "duplicate common" "duplicate common"
- (err "commons-dup-common.sexp"
+ (err "duplicate common"
(Printf.sprintf "((commons (%s %s)) (assigned ()))" good_common good_common));
check_msg "duplicate assignment" "duplicate assignment"
- (err "commons-dup-assign.sexp"
+ (err "duplicate assignment"
(Printf.sprintf "((commons (%s)) (assigned ((s c-a) (s c-a))))" good_common));
check_msg "empty formulary" "no citations"
- (err "commons-empty.sexp" "((commons ((c-a ()))) (assigned ()))");
+ (err "empty formulary" "((commons ((c-a ()))) (assigned ()))");
check_msg "unknown common" "unknown common"
- (err "commons-unknown.sexp"
+ (err "unknown common"
(Printf.sprintf "((commons (%s)) (assigned ((s c-missing))))" good_common));
(* A missing file is an [Error] too, never an exception -- the contract
[Lectionary.load] already makes and the reason neither is read at
@@ -487,8 +547,10 @@ let suite =
test_step1_proper_beats_any_common_john_of_god);
("step 1: Thomas Aquinas's proper", `Quick, test_step1_proper_thomas_aquinas);
("step 1: Francis of Paola's proper", `Quick, test_step1_proper_francis_of_paola);
- ("step 4 never diverts a temporal office (BVM Saturday)", `Quick,
- test_step4_never_diverts_a_temporal_office);
+ ("step 4 leaves a temporal office alone on shipped data (BVM Saturday)", `Quick,
+ test_step4_leaves_a_temporal_office_alone_on_shipped_data);
+ ("step 4's GUARD refuses a Common wrongly assigned to a ferial slug", `Quick,
+ test_step4_guard_refuses_a_common_assigned_to_a_ferial_slug);
("every observable sanctoral Feast has a proper or a Common", `Quick,
test_every_observable_sanctoral_feast_has_readings);
("the Commons assignment table, exactly", `Quick, test_step4_assignment_table);