aboutsummaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/cli.t18
-rw-r--r--test/test_calendar.ml7
-rw-r--r--test/test_colitur.ml1
-rw-r--r--test/test_rubrics_ef.ml240
-rw-r--r--test/test_validate.ml12
5 files changed, 265 insertions, 13 deletions
diff --git a/test/cli.t b/test/cli.t
index 398c6fb..ac1351b 100644
--- a/test/cli.t
+++ b/test/cli.t
@@ -210,9 +210,9 @@ separate command for the same mechanical reason `readings` is: `day`'s row
is fixed-width space-separated with a variable-length "+slug" tail.
$ colitur rubrics 2026 | head -3
- 2026-01-01 ef-circumcision own
- 2026-01-02 ef-christmas-1-friday own
- 2026-01-03 ef-christmas-1-saturday votive
+ 2026-01-01 ef-circumcision own true
+ 2026-01-02 ef-christmas-1-friday own false
+ 2026-01-03 ef-christmas-1-saturday votive false
$ colitur rubrics 2026 | wc -l
365
@@ -224,19 +224,19 @@ apply to it -- step 2 does (the day's own temporal slug in the lectionary),
tagged `own`. Contrast a real sanctoral saint with his own proper:
$ colitur rubrics 2038 | grep '^2038-03-08'
- 2038-03-08 john-of-god proper
+ 2038-03-08 john-of-god proper false
A saint with no proper of his own says his assigned Common (step 4):
$ colitur rubrics 2038 | grep '^2038-03-06'
- 2038-03-06 common-of-non-virgins-1 common
+ 2038-03-06 common-of-non-virgins-1 common false
A weekday with no proper of its own resumes the preceding Sunday's, never
its own observed slug -- 1 December 2025 is the Monday after Advent I, and
Advent's ferias have no Mass of their own (step 3):
$ colitur rubrics 2025 | grep '^2025-12-01'
- 2025-12-01 ef-advent-sunday-1 preceding-sunday
+ 2025-12-01 ef-advent-sunday-1 preceding-sunday false
3 January 2026 above ("votive") is the RG 78/309(a) Saturday Mass of Our
Lady, said IN PLACE of the day's own office's Mass while the office (an
@@ -257,9 +257,9 @@ diocesan overlay's local patron observed instead (no proper or Common of his
own in the fixture), the chain falls all the way back to step 3:
$ colitur rubrics 2026 --overlay fixtures/overlay-example-diocesan.sexp | grep '^2026-07-11'
- 2026-07-11 ef-time-after-pentecost-sunday-6 preceding-sunday
+ 2026-07-11 ef-time-after-pentecost-sunday-6 preceding-sunday false
$ colitur rubrics 2026 | grep '^2026-07-11'
- 2026-07-11 ef-time-after-pentecost-6-saturday votive
+ 2026-07-11 ef-time-after-pentecost-6-saturday votive false
`--lang`/`--raw`/`--sigla-*` are refused rather than silently ignored, unlike
`readings`: this row resolves no display name and no citation for any of
@@ -537,7 +537,7 @@ CSV run rather than one per year:
sexp and xml are also available:
$ colitur emit --format sexp --from 2027 --to 2027 | wc -l
- 8881
+ 9010
$ 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 599dfb2..74fe26a 100644
--- a/test/test_calendar.ml
+++ b/test/test_calendar.ml
@@ -104,6 +104,11 @@ module Fixture = struct
the sanctoral side. *)
let readings ~observed:_ ~temporal:_ ~date:_ ~temporal_at:_ = (None, [])
+ (* No fixture here exercises the Creed rubric either -- a rite that has
+ not implemented it returns [false] explicitly, {!Rite.t.creed}'s own
+ documented default. *)
+ let creed ~temporal:_ ~observed:_ ~date:_ = false
+
let rite : (season, rank) Rite.t =
{ Rite.id = "synthetic-calendar"; vocab; year_start; temporal; anchors = (fun _ -> []);
(* Not a Roman rite, but a Rite.t must supply SOME Easter now that
@@ -111,7 +116,7 @@ module Fixture = struct
any for a fixture; nothing here is Easter-relative, so the value
is never actually read. *)
easter = Colitur_kernel.Computus.gregorian_easter;
- rules; season_runs = [ A; B ]; transfer_target; readings }
+ rules; season_runs = [ A; B ]; transfer_target; readings; creed }
let entry ~month ~day ~slug ~rank =
{ Layer.date = (match Date_spec.fixed ~month ~day with Ok d -> d | Error e -> failwith e);
diff --git a/test/test_colitur.ml b/test/test_colitur.ml
index 2d7f5e1..60b61fc 100644
--- a/test/test_colitur.ml
+++ b/test/test_colitur.ml
@@ -13,6 +13,7 @@ let () =
Test_config.suite;
Test_overlay.suite; Test_overlay_ini.suite; Test_temporal_ef.suite; Test_validate.suite; Test_precedence.suite;
Test_calendar.suite; Test_precedence_ef.suite; Test_sanctoral_ef.suite; Test_rite_ef.suite;
+ Test_rubrics_ef.suite;
Test_differential.suite; Test_oracle.suite; Test_oracle.suite_2038; Test_oracle.suite_2035; Test_golden.suite;
("lectionary", Test_lectionary.suite);
("lectionary-ef", Test_lectionary_ef.suite);
diff --git a/test/test_rubrics_ef.ml b/test/test_rubrics_ef.ml
new file mode 100644
index 0000000..2000d61
--- /dev/null
+++ b/test/test_rubrics_ef.ml
@@ -0,0 +1,240 @@
+(* RG 475-476, the Creed -- see lib/rites/rite_ef/rubrics_ef.ml for the
+ rubric quoted in full and every branch's own citation.
+
+ One end-to-end test per clause of 475, plus a 476 negative, resolved
+ against REAL calendar dates through the shipped sanctoral data (the same
+ pipeline `colitur day`/`colitur rubrics` use) -- every expected value
+ below was derived from the rubric's own text and checked against
+ `colitur day <year>`'s real output (slug/rank/subject/weekday) BEFORE
+ this module existed, never read off [Rubrics_ef.creed]'s own answer.
+ Two synthetic unit tests isolate Trap One (RG 475(a) reads [temporal],
+ never [observed]) directly, without depending on finding a real-calendar
+ coincidence. *)
+
+module Cal = Colitur_kernel.Calendar
+module LD = Colitur_kernel.Liturgical_day
+module Date = Colitur_kernel.Date
+module Cel = Colitur_kernel.Celebration
+module Colour = Colitur_kernel.Colour
+module Subject = Colitur_kernel.Subject
+module Slug = Colitur_kernel.Slug
+module Temporal = Colitur_kernel.Temporal
+module V = Rite_ef.Vocab_ef
+module RE = Rite_ef.Rubrics_ef
+
+let mk y m d = match Date.make ~year:y ~month:m ~day:d with Ok t -> t | Error e -> failwith e
+
+(* Loaded once, module-level: every test below is a lookup against the same
+ shipped calendar, and {!Cal.day} recomputes its whole liturgical year on
+ every call (calendar.mli's own documented cost), so at minimum the layer
+ itself should not be reloaded and re-merged per test case. *)
+let layer =
+ match Test_support.load_ef_layer () with Ok l -> l | Error e -> Alcotest.failf "%s" e
+
+let ctx = Test_support.ef_context ()
+
+let creed_on y m d = (Cal.day ctx layer (mk y m d)).LD.creed
+
+let check name y m d expected = Alcotest.(check bool) name expected (creed_on y m d)
+
+(* ---- RG 475(a): "in qualibet dominica, etsi eius Officium alicui festo
+ locum cedat" ---- *)
+
+let test_475a_ordinary_sunday () =
+ (* 2026-01-25: an ordinary Time-after-Epiphany Sunday, Class2, green,
+ "Dominica III post Epiphaniam" -- confirmed via `colitur day 2026`,
+ no other clause of 475 could apply (Class2, subject Temporal, no
+ octave, no apostle/vigil slug), so [true] here can only come from
+ 475(a) itself. *)
+ check "475(a): an ordinary Sunday" 2026 1 25 true
+
+(* Trap One, isolated directly: RG 475(a)'s own "ETSI EIUS OFFICIUM ALICUI
+ FESTO LOCUM CEDAT" -- even when a feast has displaced the Sunday's own
+ office, the Creed is still said. [observed] below is deliberately shaped
+ so that NONE of 475(b)/(c)/(e) can produce [true] on their own (Class3,
+ subject Saint, a slug on no list this module knows); the only way
+ [creed] can return [true] is by reading [temporal]'s own [weekday],
+ never [observed]. The synthetic [date] (an ordinary July day, itself a
+ Wednesday in 2026) is chosen so nothing about the DATE itself suggests
+ a Sunday either -- proving the function reads [temporal.weekday], not
+ [Date.weekday date]. *)
+let impeded_observed =
+ Cel.make ~slug:(Slug.of_string_exn "some-impeding-feast-of-the-lord") ~rank:V.Class3
+ ~status:Cel.Feast ~colour:Colour.Red ~subject:Subject.Saint ~layer:"synthetic" ()
+
+let synthetic_temporal ~weekday : (V.season, V.rank) Temporal.t =
+ { Temporal.season = V.Time_after_pentecost; week = Some 1; weekday; office = impeded_observed }
+
+let test_475a_reads_temporal_not_observed () =
+ Alcotest.(check bool) "Sunday-shaped [temporal] overrides a non-Sunday-shaped [observed]" true
+ (RE.creed ~temporal:(synthetic_temporal ~weekday:Date.Sun) ~observed:impeded_observed
+ ~date:(mk 2026 7 1));
+ Alcotest.(check bool) "same [observed], non-Sunday [temporal]: false" false
+ (RE.creed ~temporal:(synthetic_temporal ~weekday:Date.Wed) ~observed:impeded_observed
+ ~date:(mk 2026 7 1))
+
+(* ---- RG 475(b): "in festis I classis" ---- *)
+
+let test_475b_class1_feast () =
+ (* 2026-08-15: the Assumption, Class1. *)
+ check "475(b): a I-class feast" 2026 8 15 true
+
+(* ---- RG 475(c): "in festis II classis Domini et B. Mariae Virg." ---- *)
+
+let test_475c_lord () =
+ (* 2026-09-14: Exaltation of the Holy Cross, Class2, subject Lord. *)
+ check "475(c): a II-class feast of the Lord" 2026 9 14 true
+
+let test_475c_bvm_via_marian_slugs () =
+ (* 2026-08-22: Immaculate Heart of Mary, Class2 -- ships [subject =
+ Saint] in data/ef/sanctoral.sexp (confirmed by grep), so this can only
+ come out [true] via {!Rite_ef.Precedence_ef.marian_slugs}, not via
+ [subject = Bvm]. Also carries a real commemoration
+ (+sts-timothy-hippolytus-and-symphorianus-martyrs), a live instance of
+ 476(e): the commemoration plays no part in this answer. *)
+ check "475(c): a II-class BVM feast (via marian_slugs, subject=Saint in the data)" 2026 8 22 true
+
+(* ---- RG 475(d): "per octavas Nativitatis Domini, Paschatis et
+ Pentecostes, etiam in festis occurrentibus et in Missis votivis" ---- *)
+
+let test_475d_octave_even_occurring_feast () =
+ (* 2026-12-26: St Stephen, Class2, "S. Stephani Protomartyris" -- a real
+ saint's feast OCCURRING within the Octave of the Nativity (RG 67's own
+ "Com. octavae Nativitatis", carried as +ef-nativity-octave-day-2 in
+ colitur's own commemoration). RG 475(d)'s own "etiam in festis
+ occurrentibus" is written for exactly this shape: the Creed is said
+ regardless. (This is the one place this suite deliberately diverges
+ from the task brief's own worked example, which expected [false] here
+ -- the brief mis-cited 26 December as 476(b)'s "plain II-class feast"
+ case, missing that RG 475(d) explicitly overrides 476(b) inside the
+ Nativity octave; see the task report.) *)
+ check "475(d): a saint's feast occurring within the Nativity octave" 2026 12 26 true
+
+let test_475d_octave_day_boundary () =
+ (* 2026-01-01: the Octave Day of the Nativity itself (Circumcision),
+ Class1 -- also [true] via 475(b) alone, kept as a boundary check that
+ 1 January is correctly included in the 8-day window. *)
+ check "475(d): 1 January, the Octave Day of the Nativity" 2026 1 1 true
+
+(* ---- RG 475(e): "in festis nataliciis Apostolorum et Evangelistarum,
+ necnon in festis Cathedrae S. Petri et S. Barnabae Ap." ---- *)
+
+let test_475e_apostle_natalicium () =
+ (* 2026-11-30: St Andrew, Class2, subject Saint -- not covered by 475(c)
+ (not Domini/BVM), so [true] here can only come from 475(e)'s own
+ natalicia list. Also carries a real commemoration
+ (+ef-advent-1-monday), another live 476(e) instance. *)
+ check "475(e): an Apostle's own natalicium (Andrew)" 2026 11 30 true
+
+let test_475e_barnabas_named_explicitly () =
+ (* 2026-06-11: St Barnabas, Class3 -- named explicitly by the clause
+ ("S. Barnabae Ap."); at Class3 it could not reach [true] via 475(b) or
+ (c) regardless. *)
+ check "475(e): St Barnabas, named explicitly" 2026 6 11 true
+
+let test_475e_chair_of_peter_named_explicitly () =
+ (* 2027-02-22 (NOT 2026: Feb 22 2026 is impeded by Lent I Sunday, so the
+ Chair is not observed that year -- checked via `colitur day 2026`
+ before picking 2027 instead): the Chair of St Peter, Class2, subject
+ Saint -- NOT a natalicium (Peter's own is 29 June, shared with Paul),
+ so [true] here can only come from the clause's own explicit "Cathedrae
+ S. Petri" naming, not from the natalicium reading in general. *)
+ check "475(e): the Chair of St Peter, named explicitly (not a natalicium)" 2027 2 22 true
+
+let test_475e_excludes_conversion_of_paul () =
+ (* Trap Two, directly: 2027-01-25, the Conversion of St Paul, Class3 --
+ names an Apostle but is NOT his natalicium (his own is 29 June, with
+ Peter); Class3 rules out 475(b)/(c), and this slug is deliberately
+ absent from [creed_apostle_slugs]. Picked 2027 for the same impeded-
+ Sunday reason as the Chair of Peter above (25 January 2026 is itself a
+ Sunday). *)
+ check "475(e) does NOT cover the Conversion of St Paul (not a natalicium)" 2027 1 25 false
+
+(* ---- RG 23 (feriae) / RG 476(a): Ash Wednesday, Holy Week's own feriae,
+ the Chrism/Lord's Supper Mass, the Easter Vigil Mass ---- *)
+
+let test_ash_wednesday_no_creed () =
+ check "RG 23(a)/476: Ash Wednesday, a I-class FERIA, not a festum" 2026 2 18 false
+
+let test_holy_thursday_no_creed () =
+ (* 2026-04-02: "Feria V in Cena Domini" -- RG 23(b)'s own "omnes feriae
+ Hebdomadae sanctae"; also explicitly named by 476(a) ("sive... in
+ Cena Domini"). *)
+ check "RG 23(b)/476(a): Holy Thursday (Mass of the Lord's Supper)" 2026 4 2 false
+
+let test_holy_saturday_easter_vigil_no_creed () =
+ (* 2026-04-04: "Sabbato sanctum" -- RG 23(b) again; also explicitly named
+ by 476(a) ("in Missa Vigiliae paschalis"). *)
+ check "RG 23(b)/476(a): Holy Saturday (the Easter Vigil Mass)" 2026 4 4 false
+
+(* ---- RG 476(b) negative: a plain II-class saint, not Domini/BVM, not an
+ Apostle/Evangelist ---- *)
+
+let test_476b_plain_class2_saint () =
+ (* 2026-08-10: St Lawrence, Class2, subject Saint -- a deacon and martyr,
+ no Apostle/Evangelist connection, not on any list this module reads. *)
+ check "476(b): a plain II-class saint (Lawrence) does not say the Creed" 2026 8 10 false
+
+(* ---- RG 28-34 (vigils): checked ahead of 475(c) so a Class2 vigil that
+ is ALSO on marian_slugs is still excluded ---- *)
+
+let test_vigil_excluded_even_when_class2_and_marian () =
+ (* 2026-08-14: Vigil of the Assumption, Class2 -- IS on
+ {!Rite_ef.Precedence_ef.marian_slugs} (a real Marian entry), so without
+ the vigil check ahead of 475(c) this would wrongly come out [true].
+ Also carries a real commemoration (+eusebius-confessor), a second live
+ 476(e) instance. *)
+ check "vigils are excluded even when Class2 and Marian (Vigil of the Assumption)" 2026 8 14 false
+
+(* ---- RG 476(d): a IV-class office (also exercises RG 78/309(a)'s votive
+ Office of the BVM on Saturday, itself IV class) ---- *)
+
+let test_476d_bvm_saturday_office () =
+ (* 2026-07-11: the unoccupied Saturday's Office of Our Lady, Class4 --
+ already a pinned example in test/cli.t (Task 4). *)
+ check "476(d): the BVM Saturday Office, IV class" 2026 7 11 false
+
+(* One full civil year, walked day by day: RG 475(a)'s own invariant, "every
+ Sunday says the Creed, no exceptions" -- the same sanity check the task
+ asks for at the domain-measurement step, pinned here as a real assertion
+ rather than left to a one-off shell scan. *)
+let test_every_sunday_in_2026_says_the_creed () =
+ let days = Cal.year ctx layer 2026 in
+ Array.iter
+ (fun (d : (V.season, V.rank) LD.t) ->
+ if d.LD.temporal.Temporal.weekday = Date.Sun then
+ Alcotest.(check bool)
+ (Printf.sprintf "%s is a Sunday: creed must be true" (Date.to_iso8601 d.LD.date))
+ true d.LD.creed)
+ days
+
+let suite =
+ ( "Rubrics_ef",
+ [ Alcotest.test_case "475(a): ordinary Sunday" `Quick test_475a_ordinary_sunday;
+ Alcotest.test_case "475(a): reads [temporal], not [observed] (Trap One)" `Quick
+ test_475a_reads_temporal_not_observed;
+ Alcotest.test_case "475(b): I-class feast" `Quick test_475b_class1_feast;
+ Alcotest.test_case "475(c): II-class feast of the Lord" `Quick test_475c_lord;
+ Alcotest.test_case "475(c): II-class BVM feast via marian_slugs" `Quick
+ test_475c_bvm_via_marian_slugs;
+ Alcotest.test_case "475(d): octave overrides an occurring feast" `Quick
+ test_475d_octave_even_occurring_feast;
+ Alcotest.test_case "475(d): 1 January octave-day boundary" `Quick test_475d_octave_day_boundary;
+ Alcotest.test_case "475(e): an Apostle's own natalicium" `Quick test_475e_apostle_natalicium;
+ Alcotest.test_case "475(e): Barnabas, named explicitly" `Quick
+ test_475e_barnabas_named_explicitly;
+ Alcotest.test_case "475(e): Chair of Peter, named explicitly" `Quick
+ test_475e_chair_of_peter_named_explicitly;
+ Alcotest.test_case "475(e) excludes the Conversion of St Paul (Trap Two)" `Quick
+ test_475e_excludes_conversion_of_paul;
+ Alcotest.test_case "RG 23/476: Ash Wednesday" `Quick test_ash_wednesday_no_creed;
+ Alcotest.test_case "RG 23/476(a): Holy Thursday" `Quick test_holy_thursday_no_creed;
+ Alcotest.test_case "RG 23/476(a): Holy Saturday / Easter Vigil" `Quick
+ test_holy_saturday_easter_vigil_no_creed;
+ Alcotest.test_case "476(b): plain II-class saint (negative)" `Quick
+ test_476b_plain_class2_saint;
+ Alcotest.test_case "vigils excluded even when Class2 and Marian" `Quick
+ test_vigil_excluded_even_when_class2_and_marian;
+ Alcotest.test_case "476(d): BVM Saturday Office, IV class" `Quick test_476d_bvm_saturday_office;
+ Alcotest.test_case "every Sunday in 2026 says the Creed" `Quick
+ test_every_sunday_in_2026_says_the_creed ] )
diff --git a/test/test_validate.ml b/test/test_validate.ml
index 7387971..ea8fc0f 100644
--- a/test/test_validate.ml
+++ b/test/test_validate.ml
@@ -324,16 +324,22 @@ module Synthetic = struct
{ Colitur_kernel.Mass_formulary.said = Slug.of_string_exn "syn-formulary";
via = Colitur_kernel.Mass_formulary.Own_slug }
+ (* No fixture here exercises the Creed rubric -- a rite that has not
+ implemented it returns [false] explicitly, {!Rite.t.creed}'s own
+ documented default. Made overridable ([?creed] below) on the same
+ footing as [?readings] just above, for Task 6's own fixtures. *)
+ let creed ~temporal:_ ~observed:_ ~date:_ = false
+
let rite ?(vocab = vocab) ?(anchors = fun _ -> []) ?(season_runs = [ A; B ]) ?(rules = rules)
- ?(transfer_target = fun _ origin _ -> origin) ?(readings = readings) temporal :
- (season, rank) Rite.t =
+ ?(transfer_target = fun _ origin _ -> origin) ?(readings = readings) ?(creed = creed) temporal
+ : (season, rank) Rite.t =
{ Rite.id = "synthetic"; vocab; year_start; temporal; anchors; rules; season_runs;
(* Not a Roman rite, but a Rite.t must supply SOME Easter now that
movable Date_spec variants exist. The Gregorian one is as good as
any for a fixture; nothing here is Easter-relative, so the value
is never actually read. *)
easter = Colitur_kernel.Computus.gregorian_easter;
- transfer_target; readings }
+ transfer_target; readings; creed }
(* Empty by default: every check built before Task 12 exercises the
TEMPORAL-only pass, where an empty layer is exactly the fixture that