summaryrefslogtreecommitdiff
path: root/test/test_amendments_of.ml
diff options
context:
space:
mode:
Diffstat (limited to 'test/test_amendments_of.ml')
-rw-r--r--test/test_amendments_of.ml217
1 files changed, 217 insertions, 0 deletions
diff --git a/test/test_amendments_of.ml b/test/test_amendments_of.ml
new file mode 100644
index 0000000..1ed21d9
--- /dev/null
+++ b/test/test_amendments_of.ml
@@ -0,0 +1,217 @@
+(* Task 2 (2026-08-25-colitur-of-phases-3-5): the post-2002 decree overlays.
+ data/of/calendar-2002.sexp (Task 1) transcribes the 2002 typical edition
+ and is never edited again -- every subsequent Calendarium Romanum Generale
+ change is its own overlay file under data/of/amendments/, one per decree,
+ per docs/research/of/PROVENANCE-amendments.md (13 decrees, 16
+ celebration-level changes -- the research inventory's own "15 items,
+ numbered 1-15, where items 10-12 are three entries carried by a single
+ decree" collapses to 13 distinct decrees once items 2/3, both dated 28
+ September 2002 but carrying DIFFERENT protocol numbers, are counted as the
+ two separate juridical instruments they are).
+
+ This suite asserts: every file parses; each has a pinned SHA-256;
+ applying all 13 in their own decree-chronological order to the base
+ calendar yields a pinned entry count; Overlay.merge reports NO diagnostic
+ (a diagnostic would mean a directive was transcribed against a slug that
+ does not exist in the base -- the exact silent failure one-file-per-decree
+ exists to prevent); and Mary, Mother of the Church resolves to the Monday
+ after Pentecost in three real, independently-computed years. *)
+
+module L = Colitur_kernel.Layer
+module O = Colitur_kernel.Overlay
+module Cel = Colitur_kernel.Celebration
+module S = Colitur_kernel.Slug
+module DS = Colitur_kernel.Date_spec
+module D = Colitur_kernel.Date
+module Computus = Colitur_kernel.Computus
+module Names = Colitur_kernel.Names
+module Lang = Colitur_kernel.Lang
+module V = Rite_of.Vocab_of
+
+let base_path = "../data/of/calendar-2002.sexp"
+let amendments_dir = "../data/of/amendments/"
+
+(* Decree-chronological order, matching docs/research/of/
+ PROVENANCE-amendments.md's own "Ordered by decree date" -- the numeric
+ filename prefixes already encode this, spelled out here explicitly so the
+ intended application order is visible in the test itself, not merely
+ inferred from a directory listing. Each paired with its SHA-256, pinned
+ 2026-08-26 against the shipped files themselves (`sha256sum
+ data/of/amendments/*.sexp`), not copied from any tool's own stdout. *)
+let files =
+ [ ("001-padre-pio.sexp",
+ "8ddd92f1820a736c7a4f3c27b3181579d6280ed82f0983f80219d523782731f6");
+ ("002-juan-diego-cuauhtlatoatzin.sexp",
+ "01000fe5615894ff82b89a099e1539d6556beccfe1d07d5664362b0126724b09");
+ ("003-our-lady-of-guadalupe.sexp",
+ "a19100769786d110d462805cc4533867c9d56308f8a058f46b257b2b0d6cb3ef");
+ ("004-john-xxiii-john-paul-ii.sexp",
+ "ae24ac41935600d0d11a81072bedce2ba9790b79e2f8192c5d4a08a9a7db4e6e");
+ ("005-mary-magdalene-rank.sexp",
+ "08a19050c21391ee565bc4d5a3ba228e3162875188db4bbc3b7f4b066a60a0c1");
+ ("006-mary-mother-of-the-church.sexp",
+ "8a1dda10c1014bc59c22779f3686c6cce0bf9ff1ec4e504cb912cf2c89319ff9");
+ ("007-paul-vi.sexp",
+ "d7b442b6b6dd6c8e7aca5f7126f7b3e65104191ae84c158e07e9afd0f70edcfc");
+ ("008-our-lady-of-loreto.sexp",
+ "772366fb6c04d1c779109d8b56fa1c0a71ff0129f92349564e1a353b253c2e13");
+ ("009-faustina-kowalska.sexp",
+ "a6bf8a1ab8e3f4aa65bb22d4ff9ba863e5f65865180444f4b143f36aa99e45b3");
+ ("010-narek-avila-hildegard.sexp",
+ "6ca11a7c2e02491b9b96349282c7b1d6fc28c9e4729f678bd54d11a70e43f568");
+ ("011-martha-mary-lazarus.sexp",
+ "c4ede10b24f22def5fd557a3627d1ecaeb5de583f263372a67bccf9ab838f023");
+ ("012-teresa-of-calcutta.sexp",
+ "5646597472f6170a2b56e1f1c4a2ef7e7917cb5c87eb5e3d1f98b8ee1aaae646");
+ ("013-john-henry-newman.sexp",
+ "f6c12d1ead043a609b0086a130ae1a9fc680a8926fe77ccd97756ee747105d7c") ]
+
+let sha256_of_file path =
+ let tmp = Filename.temp_file "colitur_amendments_of_sha256" ".txt" in
+ let cmd = Printf.sprintf "sha256sum %s > %s" (Filename.quote path) (Filename.quote tmp) in
+ (match Sys.command cmd with
+ | 0 -> ()
+ | n -> Alcotest.failf "sha256sum exited %d" n);
+ let ic = open_in tmp in
+ let line = input_line ic in
+ close_in ic;
+ Sys.remove tmp;
+ match String.split_on_char ' ' line with
+ | hash :: _ -> hash
+ | [] -> Alcotest.fail "sha256sum produced no output"
+
+let load_overlay name =
+ let path = amendments_dir ^ name in
+ match O.load V.rank_of_sexp path with
+ | Ok o -> o
+ | Error e -> Alcotest.failf "%s: failed to load: %s" path e
+
+let load_base () =
+ match L.load V.rank_of_sexp base_path with
+ | Ok l -> l
+ | Error e -> Alcotest.failf "%s: failed to load: %s" base_path e
+
+(* Every file parses. *)
+let test_files_parse () =
+ List.iter
+ (fun (name, _) ->
+ let (_ : V.rank O.t) = load_overlay name in
+ ())
+ files
+
+(* Each has a pinned SHA-256. *)
+let test_sha256_pinned () =
+ List.iter
+ (fun (name, expected) ->
+ Alcotest.(check string) (name ^ " SHA-256") expected
+ (sha256_of_file (amendments_dir ^ name)))
+ files
+
+(* Applying all 13, in decree-chronological order, to the base calendar
+ yields a pinned entry count, AND Overlay.merge reports no diagnostic --
+ the silent-no-op check the whole one-file-per-decree discipline exists
+ to enforce (a diagnostic here would mean a directive was transcribed
+ against a slug the base calendar does not actually have). *)
+let test_apply_in_order () =
+ let base = load_base () in
+ let overlays = List.map (fun (name, _) -> load_overlay name) files in
+ let merged, diagnostics = O.merge base overlays in
+ Alcotest.(check (list string)) "no diagnostics -- every directive matched something" []
+ (List.map O.diagnostic_to_string diagnostics);
+ (* 208 base entries + 14 Adds (1+1+1+2+1+1+1+1+3+1+1, files
+ 001/002/003/004/006/007/008/009/010x3/012/013) + 2 Edits (005, 011,
+ no count change) = 222. *)
+ Alcotest.(check int) "208 base + 14 adds = 222 entries" 222 (List.length merged.L.entries)
+
+(* Every new/edited slug this task introduces is actually present after
+ the merge, and the two Edits did not accidentally become Adds (no
+ "slug not present; edit ignored" diagnostic already ruled that out
+ above, but a direct spot-check on the merged layer is cheap insurance
+ against a slug typo that happened to also dodge the diagnostic, e.g. by
+ matching a DIFFERENT, wrong existing slug). *)
+let test_new_slugs_present () =
+ let base = load_base () in
+ let overlays = List.map (fun (name, _) -> load_overlay name) files in
+ let merged, _ = O.merge base overlays in
+ let has slug = L.mem merged (S.of_string_exn slug) in
+ List.iter
+ (fun slug -> Alcotest.(check bool) (slug ^ " present after merge") true (has slug))
+ [ "pio-of-pietrelcina-padre-pio-priest"; "juan-diego"; "our-lady-of-guadalupe";
+ "john-xxiii-pope"; "john-paul-ii-pope"; "mary-mother-of-the-church"; "paul-vi-pope";
+ "our-lady-of-loreto"; "faustina-kowalska-virgin"; "gregory-of-narek-abbot-and-doctor";
+ "john-of-avila-priest-and-doctor"; "hildegard-of-bingen-virgin-and-doctor";
+ "teresa-of-calcutta-virgin"; "john-henry-newman-priest-and-doctor" ];
+ (* The two Edits kept their original slugs. *)
+ match L.find merged (S.of_string_exn "mary-magdalene") with
+ | None -> Alcotest.fail "mary-magdalene: slug lost after Edit"
+ | Some e -> Alcotest.(check bool) "mary-magdalene is now Festum" true (e.L.cel.Cel.rank = V.Festum)
+
+let test_martha_mary_lazarus_renamed () =
+ let base = load_base () in
+ let overlays = List.map (fun (name, _) -> load_overlay name) files in
+ let merged, _ = O.merge base overlays in
+ match L.find merged (S.of_string_exn "martha") with
+ | None -> Alcotest.fail "martha: slug lost after Edit"
+ | Some e ->
+ Alcotest.(check (option string)) "en name widened" (Some "Saints Martha, Mary and Lazarus")
+ (Names.find e.L.cel.Cel.names (Lang.of_string_exn "en"));
+ Alcotest.(check bool) "date unchanged, 29 July" true
+ (e.L.date = (match DS.fixed ~month:7 ~day:29 with Ok d -> d | Error err -> failwith err));
+ Alcotest.(check bool) "rank unchanged, Memoria_obligatoria" true
+ (e.L.cel.Cel.rank = V.Memoria_obligatoria)
+
+(* Mary, Mother of the Church resolves to the Monday after Pentecost --
+ Easter+50 -- in three real, independently-computed years, not merely
+ trusted from the decree's own arithmetic. Pentecost is Easter+49 (the
+ 50th day of the Paschal season inclusive of Easter); the day after is
+ Easter+50. Checked structurally (weekday = Mon, and the resolved date is
+ exactly 50 days after that year's real Gregorian Easter, computed via
+ Colitur_kernel.Computus.gregorian_easter), not by hand-typing expected
+ calendar dates. *)
+let test_mater_ecclesiae_monday_after_pentecost () =
+ let base = load_base () in
+ let overlays = List.map (fun (name, _) -> load_overlay name) files in
+ let merged, _ = O.merge base overlays in
+ let entry =
+ match L.find merged (S.of_string_exn "mary-mother-of-the-church") with
+ | Some e -> e
+ | None -> Alcotest.fail "mary-mother-of-the-church: not found after merge"
+ in
+ Alcotest.(check bool) "date spec is Easter_offset 50" true (entry.L.date = DS.Easter_offset 50);
+ Alcotest.(check bool) "Memoria_obligatoria" true (entry.L.cel.Cel.rank = V.Memoria_obligatoria);
+ Alcotest.(check bool) "white" true (entry.L.cel.Cel.colour = Colitur_kernel.Colour.White);
+ Alcotest.(check bool) "subject Bvm" true (entry.L.cel.Cel.subject = Colitur_kernel.Subject.Bvm);
+ List.iter
+ (fun year ->
+ let easter = Computus.gregorian_easter year in
+ let pentecost = D.add_days easter 49 in
+ let expected = D.add_days easter 50 in
+ Alcotest.(check bool)
+ (Printf.sprintf "%d: Pentecost (Easter+49) is a Sunday" year)
+ true (D.weekday pentecost = D.Sun);
+ let resolved =
+ match DS.resolve entry.L.date ~year ~easter with
+ | Some d -> d
+ | None -> Alcotest.failf "%d: Easter_offset 50 did not resolve" year
+ in
+ Alcotest.(check bool)
+ (Printf.sprintf "%d: resolves to exactly Easter+50" year) true (resolved = expected);
+ Alcotest.(check bool)
+ (Printf.sprintf "%d: resolved date is a Monday" year) true (D.weekday resolved = D.Mon);
+ Alcotest.(check bool)
+ (Printf.sprintf "%d: resolved date is the day after Pentecost" year) true
+ (resolved = D.add_days pentecost 1))
+ [ 2026; 2027; 2035 ]
+
+let suite =
+ ( "Amendments_of (data/of/amendments/*.sexp)",
+ [ Alcotest.test_case "every overlay file parses" `Quick test_files_parse;
+ Alcotest.test_case "SHA-256 pinned per file" `Quick test_sha256_pinned;
+ Alcotest.test_case "apply in decree-chronological order: 222 entries, no diagnostics" `Quick
+ test_apply_in_order;
+ Alcotest.test_case "every new/edited slug is present after merge" `Quick
+ test_new_slugs_present;
+ Alcotest.test_case "Martha/Mary/Lazarus: renamed in place, date/rank unchanged" `Quick
+ test_martha_mary_lazarus_renamed;
+ Alcotest.test_case "Mary, Mother of the Church: Monday after Pentecost, 3 real years" `Quick
+ test_mater_ecclesiae_monday_after_pentecost ] )