aboutsummaryrefslogtreecommitdiff
path: root/test/test_lectionary.ml
blob: 59c739dc258bf5a2497363761dcde523e0fe0e54 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
open Colitur_kernel

let slug = Slug.of_string_exn
let cit part reference = { Citation.part; reference }

let entry s cs = (slug s, cs)

let ok_exn = function Ok x -> x | Error e -> Alcotest.fail e

(* Stdlib-only substring search -- Astring is not among the frozen deps. *)
let contains_substring ~needle haystack =
  let hn = String.length needle and hh = String.length haystack in
  let rec go i = i + hn <= hh && (String.sub haystack i hn = needle || go (i + 1)) in
  hn = 0 || go 0

let test_find_present () =
  let l =
    ok_exn
      (Lectionary.of_entries
         [ entry "ef-advent-sunday-1"
             [ cit Citation.First "Rom 13:11-14"; cit Citation.Gospel "Luke 21:25-33" ] ])
  in
  match Lectionary.find l (slug "ef-advent-sunday-1") with
  | Some [ a; b ] ->
      Alcotest.(check string) "epistle" "Rom 13:11-14" a.Citation.reference;
      Alcotest.(check string) "gospel" "Luke 21:25-33" b.Citation.reference
  | Some _ -> Alcotest.fail "expected exactly two citations"
  | None -> Alcotest.fail "slug not found"

let test_find_absent () =
  let l = ok_exn (Lectionary.of_entries []) in
  Alcotest.(check bool) "absent" true
    (Lectionary.find l (slug "ef-advent-sunday-1") = None)

let test_duplicate_slug_rejected () =
  let e = entry "ef-advent-sunday-1" [ cit Citation.First "Rom 13:11-14" ] in
  match Lectionary.of_entries [ e; e ] with
  | Ok _ -> Alcotest.fail "duplicate slug must be rejected"
  | Error msg ->
      (* Strengthened beyond the brief's "non-empty and contains 'e'" check:
         the interface doc promises the error names the slug, so assert the
         offending slug string actually appears in the message. *)
      Alcotest.(check bool) "names the slug" true
        (contains_substring ~needle:"ef-advent-sunday-1" msg)

let test_sexp_round_trip () =
  let l =
    ok_exn
      (Lectionary.of_entries
         [ entry "ef-lent-1-monday"
             [ cit Citation.First "Ezech 34:11-16"; cit Citation.Gospel "Matt 25:31-46" ] ])
  in
  let l' = Lectionary.t_of_sexp (Lectionary.sexp_of_t l) in
  Alcotest.(check bool) "round trips" true (l = l')

let with_temp_file contents f =
  let path = Filename.temp_file "lectionary_test" ".sexp" in
  let oc = open_out path in
  output_string oc contents;
  close_out oc;
  Fun.protect ~finally:(fun () -> Sys.remove path) (fun () -> f path)

let load_is_error contents what =
  with_temp_file contents (fun path ->
      match Lectionary.load path with
      | Error _ -> ()
      | Ok _ -> Alcotest.fail (what ^ ": expected Error, got Ok"))

let test_load_never_raises () =
  load_is_error "((ef-advent-sunday-1 (" "unterminated list";
  load_is_error "((ef-advent-sunday-1 ((part First) (reference \"x" "unterminated string";
  load_is_error "" "empty file";
  load_is_error "() ()" "more than one sexp";
  load_is_error "not-a-list" "wrong shape";
  load_is_error "((\"NOT A SLUG\" ()))" "invalid slug";
  match Lectionary.load "/nonexistent/path/lectionary.sexp" with
  | Error _ -> ()
  | Ok _ -> Alcotest.fail "missing file: expected Error, got Ok"

let test_ef_data_file_loads () =
  match Lectionary.load "../data/ef/lectionary.sexp" with
  | Error e -> Alcotest.fail e
  | Ok l ->
      (* 119 lectio ini sections, translated/widened/renamed into colitur's
         own Temporal_ef vocabulary, PLUS hand-authored/derived entries
         (tools/bootstrap_lectionary.ml's own [colitur_keys]/
         [vigil_entries]/[holy_week_entries]/[passion_tuesday_entry]/
         [nativity_octave_entries]/[holy_name_entries]/
         [epiphanytide_opening_entries]/[movable_feast_entries] comments
         have the full account, task 8 (branch ef-lectionary, fix rounds
         1-2) then Task 9 (same branch, layer-4 oracle) below):
         "ef-passiontide-0-tuesday" is EXCLUDED (task 8 fix round 2,
         Important -- it is Holy Tuesday's data, not Passion Tuesday's,
         -1); the other five Passiontide sections rename 1:1 (no count
         change); the three Lent Ember sections rename 1:1 (no count
         change); "ef-advent-ember-sat"/"ef-september-ember-sat" are
         EXCLUDED, task 8 fix round 3 (Important -- Thomas the Apostle's/
         Matthew's Mass verbatim, not the real Ember Saturday, -2);
         119 - 1 - 2 = 116 real ini-translated entries. PLUS: one
         hand-authored entry sourced from lectio's SANCTORAL calendar, not
         this lectionary ini ([ef-nativity-vigil], +1); SIX hand-authored
         directly from the Missal, all of Holy Week ([holy_week_entries],
         +6); ONE hand-authored directly from the Missal, Passion
         Tuesday's own real citation ([passion_tuesday_entry], +1); TWO
         hand-authored directly from the Missal, replacing the two
         excluded Ember Saturdays above ([ember_saturday_corrections],
         +2); three hand-authored directly from the Missal, the fixed
         Nativity-Octave days ([nativity_octave_entries], +3).
         Task 9 (branch ef-lectionary, layer-4 oracle -- CORRECTED,
         [ef-holy-name-sunday]/[ef-holy-name] no longer widened/derived
         from "ef-christmas-sunday-0", see [colitur_keys]'s own
         CORRECTED note): TWO hand-authored directly from the Missal
         ([holy_name_entries], +2); FIFTEEN hand-authored directly from
         the Missal, three 5-weekday families
         ([epiphanytide_opening_entries], +15); TWO hand-authored
         directly from the Missal, two major movable feasts with
         previously no entry at all ([movable_feast_entries], +2).
         116 + 1 + 6 + 1 + 2 + 3 + 2 + 15 + 2 = 148.
         celebrant-rubrics-phase1, Bug 2 (2026-08-22): SIX hand-authored
         directly from the Missal's own marginal Ascension-repeat rubric
         ([ascension_week_entries], +6, 153 -> 159) -- see that
         function's own comment in bootstrap_lectionary.ml for the
         citation and why the window's two Saturdays are deliberately
         excluded. *)
      Alcotest.(check int) "entry count" 159
        (List.length (Lectionary.entries l));
      (match Lectionary.find l (slug "ef-lent-1-monday") with
       | Some [ a; b ] ->
           Alcotest.(check string) "epistle" "Ezech 34:11-16" a.Citation.reference;
           Alcotest.(check string) "gospel" "Matt 25:31-46" b.Citation.reference
       | Some _ -> Alcotest.fail "expected exactly two citations"
       | None -> Alcotest.fail "ef-lent-1-monday missing");
      (* Task 8's own renamed/derived/hand-authored keys, spot-checked here
         so a future regeneration that silently drops one of them fails
         loudly and locally, not only via the much bigger differential
         suite. *)
      let check_entry name first gospel =
        match Lectionary.find l (slug name) with
        | Some [ a; b ] ->
            Alcotest.(check string) (name ^ ": epistle") first a.Citation.reference;
            Alcotest.(check string) (name ^ ": gospel") gospel b.Citation.reference
        | Some _ -> Alcotest.fail (name ^ ": expected exactly two citations")
        | None -> Alcotest.fail (name ^ ": missing")
      in
      (* Task 9 (branch ef-lectionary, layer-4 oracle): CORRECTED --
         [ef-holy-name-sunday]/[ef-holy-name] used to be pinned here at
         "Gal 4:1-7"/"Luke 2:33-40", the Sunday-within-the-Octave's own
         citation this generator used to (wrongly) borrow by analogy.
         missalemeum's own independent extraction (2026-01-04/2027-01-03,
         real Holy Name Sundays) disproved it; the Missal itself confirms
         the real value, both scans (scan1.txt:6579-6608,
         scan2.txt:7225-7297, "SANCTISSIMI NOMINIS IESU... Act. 4, 8-12...
         Luc. 2, 21") -- see [colitur_keys]'s own CORRECTED note in
         bootstrap_lectionary.ml for the full account. *)
      check_entry "ef-holy-name-sunday" "Acts 4:8-12" "Luke 2:21";
      check_entry "ef-christmas-sunday-0" "Gal 4:1-7" "Luke 2:33-40";
      check_entry "ef-holy-name" "Acts 4:8-12" "Luke 2:21";
      (* Task 9: the Missal's own Mass-propers "diebus ferialibus" rubric
         (CORRECTED, fix round 2: this said RG 17(a)'s, which it is not --
         RG 17(a) fixes only WHEN the Holy Name is kept; the rubric quoted
         governs what the ordinary ferias say when it is not) (2-13 January,
         three 5-weekday families -- bootstrap_lectionary.ml's own
         [epiphanytide_opening_entries] comment has the full citations and
         the RG 78/BVM-Saturday reasoning for why Saturday is excluded from
         all three). One weekday pinned per family (a regression net, not
         an exhaustive per-weekday audit -- the generator's own
         [assert_reachable] already guards every emitted key is a real
         Temporal_ef slug). *)
      check_entry "ef-christmas-1-monday" "Titus 2:11-15" "Luke 2:21";
      check_entry "ef-christmas-1-friday" "Titus 2:11-15" "Luke 2:21";
      check_entry "ef-christmas-2-tuesday" "Isa 60:1-6" "Matt 2:1-12";
      check_entry "ef-christmas-2-monday" "Isa 60:1-6" "Matt 2:1-12";
      check_entry "ef-time-after-epiphany-1-wednesday" "Rom 12:1-5" "Luke 2:42-52";
      let must_stay_absent name =
        match Lectionary.find l (slug name) with
        | None -> ()
        | Some _ -> Alcotest.fail (name ^ ": must stay absent -- RG 78's BVM Saturday Office")
      in
      must_stay_absent "ef-christmas-1-saturday";
      must_stay_absent "ef-christmas-2-saturday";
      must_stay_absent "ef-time-after-epiphany-1-saturday";
      (* Task 9: two major, permanently-named I-class movable feasts with
         no entry at all before this task (bootstrap_lectionary.ml's own
         [movable_feast_entries] comment has the full citations, both
         scans). *)
      check_entry "ef-corpus-christi" "1 Cor 11:23-29" "John 6:56-59";
      check_entry "ef-sacred-heart" "Eph 3:8-12, 14-19" "John 19:31-37";
      (* celebrant-rubrics-phase1, Bug 2: the Missal's own marginal rubric
         after the Ascension's own Mass propers ("Diebus a feria VI post
         Ascensionem usque ad feriam VI ante vigiliam Pentecostes
         inclusive, dicitur Missa de festo Ascensionis") repeats the
         Ascension's own citation on the ferias that would otherwise walk
         back to the wrong Sunday. Two families pinned (the tail of
         Ascension's own week; the following week), not merely one, the
         same "more than one weekday" discipline this file already uses
         for the Lent Ember pins above. *)
      check_entry "ef-easter-6-friday" "Acts 1:1-11" "Mark 16:14-20";
      check_entry "ef-easter-7-monday" "Acts 1:1-11" "Mark 16:14-20";
      check_entry "ef-easter-7-friday" "Acts 1:1-11" "Mark 16:14-20";
      (* [ef-easter-6-saturday] keeps its own PRE-EXISTING entry, unchanged
         by this task -- RG 78's votive Office of Our Lady always wins an
         otherwise-unoccupied Class4 Saturday first ([readings]' own step
         ordering runs that branch before step 2 is ever reached), so this
         table's own value for that slug is dead data on shipped data, but
         it was already there before this task and touching it is out of
         scope. [ef-easter-7-saturday] must stay ABSENT instead: that civil
         day is the Vigil of Pentecost, a NAMED day with its own slug and
         Mass (temporal_ef.ml's own [named]), so the generic
         [ef-<season>-<week>-<weekday>] ferial slug this table's new
         entries key off never gets produced there at all -- an entry
         would be unreachable data [assert_reachable] would have refused
         to emit, checked here too as a direct regression net. *)
      (match Lectionary.find l (slug "ef-easter-7-saturday") with
      | None -> ()
      | Some _ -> Alcotest.fail "ef-easter-7-saturday: must stay absent -- the Vigil of Pentecost, named, never a ferial slug");
      check_entry "ef-nativity-vigil" "Rom 1:1-6" "Matt 1:18-21";
      check_entry "ef-pentecost-ember-wed" "Acts 5:12-16" "John 6:44-52.";
      check_entry "ef-pentecost-ember-fri" "Joel 2:23-24; 26-27" "Luke 5:17-26";
      check_entry "ef-pentecost-ember-sat" "Rom 5:1-5." "Luke 4:38-44.";
      (* Critical 2 fix (coordinator review round 1): the Lenten Ember days,
         previously dead keys under lectio's own "ef-lent-1-<weekday>"
         naming -- now reachable under colitur's own slugs. Minor, fix
         round 2 (coordinator review): all THREE are pinned, not just
         Wednesday -- a mutation on Friday's or Saturday's own value used
         to leave the whole suite green. *)
      check_entry "ef-lent-ember-wed" "3 Kgs. 19:3-8" "Matt 12:38-50";
      check_entry "ef-lent-ember-fri" "Ezech 18:20-28" "John 5:1-15";
      check_entry "ef-lent-ember-sat" "1 Thess. 5:14-23" "Matt 17:1-9";
      (* Passiontide widens into Passion week ONLY now (Critical 1 fix) --
         "ef-passiontide-1-monday" keeps lectio's own citation;
         "ef-passiontide-2-monday" (Holy Monday) is Missal-sourced,
         DIFFERENT text, hand-authored in [holy_week_entries]. Fix round 2
         (Important): "ef-passiontide-1-tuesday" no longer carries lectio's
         own (wrong -- Holy Tuesday's) ini value; it is hand-authored from
         the Missal's own real Passion Tuesday Mass, [passion_tuesday_entry]. *)
      check_entry "ef-passiontide-1-monday" "Jonas 3:1-10" "John 7:32-39";
      check_entry "ef-passiontide-1-tuesday" "Dan 14:27, 28-42" "John 7:1-13";
      check_entry "ef-passiontide-2-monday" "Isa 50:5-10" "John 12:1-9";
      (* Gospel punctuation ("15, 1-46") intentionally kept as lectio's own
         ini already has it -- see bootstrap_lectionary.ml's own
         [holy_week_entries] comment on this one entry. *)
      check_entry "ef-passiontide-2-tuesday" "Jer 11:18-20" "Mark 14:32-72; 15, 1-46";
      (* Critical fix, round 2: Wednesday and Good Friday were left entirely
         absent by the first pass, which meant step 3 quietly resumed the
         preceding Sunday's Mass there (Good Friday emitting PALM SUNDAY's
         Epistle and Passion) -- the FIRST pass's own reasoning ("neither
         day has a single Epistle") was right about the First slot alone;
         the Gospel is always unambiguous and is now always authored, and
         First uses the SAME "last lesson before the Gospel" convention
         this file's own Lenten Ember Wednesday entry above already uses
         for an identical two-lesson shape. *)
      check_entry "ef-passiontide-2-wednesday" "Isa 53:1-12" "Luke 22:39-71; 23:1-53";
      check_entry "ef-passiontide-2-thursday" "1 Cor 11:20-32" "John 13:1-15";
      check_entry "ef-passiontide-2-friday" "Ex 12:1-11" "John 18:1-40; 19:1-42";
      check_entry "ef-passiontide-2-saturday" "Col 3:1-4" "Matt 28:1-7";
      (* Important 3(a) fix (coordinator review round 1): the fixed
         Nativity-Octave days now carry their own direct Missal formulary
         ("Diebus infra octavam Nativitatis Domini"), not Holy Name
         Sunday's (a first guess, measured wrong, reverted -- see
         [nativity_octave_entries]'s own comment). *)
      check_entry "ef-nativity-octave-day-5" "Titus 3:4-7" "Luke 2:15-20";
      check_entry "ef-nativity-octave-day-6" "Titus 3:4-7" "Luke 2:15-20";
      check_entry "ef-nativity-octave-day-7" "Titus 3:4-7" "Luke 2:15-20";
      (* Fix round 3 (coordinator review, Important): two more Ember
         Saturdays that inherited a saint's Mass verbatim from lectio's
         ini, the exact C26 shape -- pinned here so a future regeneration
         that silently reverts to the wrong (saint's) value fails loudly
         and locally, the same discipline the Lent Ember/Passion Tuesday
         pins above already established for their own fix rounds. Both
         readings are explicitly labelled "Lectio Epistolae" in the
         Missal, so neither needed the "last lesson before the Gospel"
         convention Holy Wednesday/Good Friday above rely on. *)
      check_entry "ef-advent-ember-sat" "2 Thess 2:1-8" "Luke 3:1-6";
      check_entry "ef-september-ember-sat" "Heb 9:2-12" "Luke 13:6-17";
      (* Lent's OWN "ef-lent-1-wednesday/friday/saturday" keys must not
         reappear once renamed away (colitur never computes them as its
         own slug at all) -- a regression net for that decision, not
         merely documentation of it. *)
      match Lectionary.find l (slug "ef-lent-1-wednesday") with
      | None -> ()
      | Some _ ->
          Alcotest.fail "ef-lent-1-wednesday: must stay absent -- see bootstrap_lectionary.ml"

let suite =
  [ ("find present", `Quick, test_find_present);
    ("find absent", `Quick, test_find_absent);
    ("duplicate slug rejected", `Quick, test_duplicate_slug_rejected);
    ("sexp round-trip", `Quick, test_sexp_round_trip);
    ("load never raises", `Quick, test_load_never_raises);
    ("ef data file loads", `Quick, test_ef_data_file_loads) ]