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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
|
package caldata
import (
"strings"
"testing"
"github.com/lukaszkasprzak/lectio/internal/calendar"
"time"
)
func TestUniversalLoads(t *testing.T) {
l := Universal()
if l.ID != "universal" {
t.Fatalf("layer id = %q", l.ID)
}
// the Assumption (08-15, solemnity) must be present under some slug.
assumption := false
for _, rc := range l.Cels {
if rc.Fields["date"] == "08-15" && rc.Fields["rank"] == "solemnity" {
assumption = true
}
}
if !assumption {
t.Fatal("no 08-15 solemnity (Assumption) in the universal calendar")
}
// every entry must build into a typed Celebration whose rank parsed.
for slug, rc := range l.Cels {
c := calendar.BuildCelebrationForTest(slug, rc)
if c.Rank == calendar.RankFerial && rc.Fields["rank"] != "" {
t.Errorf("%s: rank did not parse (%q)", slug, rc.Fields["rank"])
}
}
}
func TestUniversalSanctoralReadings(t *testing.T) {
l := Universal()
// A feast/solemnity or a PROPER-reading memorial carries its own inline Mass.
// Partial-proper memorials keep ONLY the strictly-proper part (the resolver
// fills the rest from the ferial): the Guardian Angels a proper gospel, St
// Barnabas a proper first reading, Our Lady of Sorrows a proper gospel.
want := map[string][2]string{ // slug -> {reading.first substr, reading.gospel substr; "" = absent}
"assumption-of-the-blessed-virgin-mary": {"Revelation 11", "Luke 1:39-56"},
"all-saints": {"Revelation 7", "Matthew 5:1-12"},
"barnabas-the-apostle": {"Acts 11", ""},
"guardian-angels": {"", "Matthew 18:1-5"},
"our-lady-of-sorrows": {"", "John 19:25-27"},
}
for slug, w := range want {
rc, ok := l.Cels[slug]
if !ok {
t.Errorf("missing %q", slug)
continue
}
for i, part := range []string{"reading.first", "reading.gospel"} {
got := rc.Fields[part]
if w[i] == "" {
if got != "" {
t.Errorf("%s: %s = %q, want absent (ferial)", slug, part, got)
}
} else if !strings.Contains(got, w[i]) {
t.Errorf("%s: %s = %q, want containing %q", slug, part, got, w[i])
}
}
}
}
func TestTridentineLoads(t *testing.T) {
l := Tridentine()
if l.ID != "tridentine" {
t.Fatalf("layer id = %q", l.ID)
}
// The full 1962 sanctoral is populated (generated from missalemeum), not the
// 16-entry bootstrap.
if len(l.Cels) < 250 {
t.Fatalf("EF sanctoral has only %d entries; expected the full calendar", len(l.Cels))
}
// Landmark feasts must be present with the right rank/class: Sts Peter & Paul
// (I class, 06-29), the Purification/Presentation (II class, 02-02, class
// = lord -- see classOf's own doc comment in scripts/gen-sanctoral-ef.go
// for why: the calendarium's own title names the BLESSED VIRGIN, but the
// tag exists to drive OCCURRENCE behaviour, and missalemeum -- this
// data's own oracle -- shows the Purification taking a II-class Sunday's
// place outright, the "festum Domini" pattern, not the ordinary-BVM
// pattern), the Immaculate Conception (I class, 12-08).
want := map[string]struct{ date, rank, class string }{
"sts-peter-paul": {"06-29", "class-1", ""},
"purification-of-the-blessed-virgin-mary": {"02-02", "class-2", "lord"},
"immaculate-conception-of-the-blessed-virgin-mary": {"12-08", "class-1", ""},
}
for slug, w := range want {
rc, ok := l.Cels[slug]
if !ok {
t.Errorf("missing landmark feast %q", slug)
continue
}
if rc.Fields["date"] != w.date || rc.Fields["rank"] != w.rank {
t.Errorf("%s: date/rank = %q/%q, want %q/%q", slug, rc.Fields["date"], rc.Fields["rank"], w.date, w.rank)
}
if rc.Fields["class"] != w.class {
t.Errorf("%s: class = %q, want %q", slug, rc.Fields["class"], w.class)
}
}
// every entry must build into a typed Celebration whose rank parsed.
for slug, rc := range l.Cels {
c := calendar.BuildCelebrationForTest(slug, rc)
if c.Rank == calendar.RankFerial && rc.Fields["rank"] != "" {
t.Errorf("%s: rank did not parse (%q)", slug, rc.Fields["rank"])
}
}
}
// TestTridentineClassOfLord: scripts/gen-sanctoral-ef.go's classOf tags
// feasts of the Lord (class = lord). "Most Holy Name of Mary" was wrongly
// tagged lord (a title-substring false positive: "holy name" alone matches
// both "Holy Name of Jesus" and "Most Holy Name of MARY", calendarium
// "Sanctissimi Nominis Mariae") and is fixed here, not contested. The
// Baptism commemoration was missing the tag entirely (13 January's title
// ends "of THE Lord", not "of OUR Lord", the only suffix classOf used to
// check -- calendarium "IN COMMEMORATIONE BAPTISMATIS D. N. I. C."), also
// fixed and not contested. The Purification is DELIBERATELY absent from
// this table: it stays tagged lord, on occurrence-behaviour evidence, not
// the calendarium's title -- see TestTridentineLoads and classOf's own doc
// comment in scripts/gen-sanctoral-ef.go for the full account of why it is
// the one contested case, not a clean-cut fix.
func TestTridentineClassOfLord(t *testing.T) {
l := Tridentine()
want := map[string]string{
"most-holy-name-of-mary": "", // BVM, not the Lord
"commemoration-of-the-baptism-of-the-lord": "lord",
}
for slug, wantClass := range want {
rc, ok := l.Cels[slug]
if !ok {
t.Errorf("missing landmark feast %q", slug)
continue
}
if got := rc.Fields["class"]; got != wantClass {
t.Errorf("%s: class = %q, want %q", slug, got, wantClass)
}
}
}
// TestTridentineLentRankNotCommemoration: 15 III-class feasts, every one
// falling 6 March - 5 April, were tagged rank = commemoration because all
// six of the generator's reference years happen to place that fixed date
// within Lent, where a privileged Lenten feria always outranks an
// equal-class feast (RG 109(e)) -- the generator mistook "how the entry
// presented on the sampled days" for the entry's true, intrinsic rank. A
// sample of the 15 (full list in the report); the calendarium gives each one
// III class outright, e.g. 5 April "S. Vincentii Ferrerii Conf., III
// classis."
func TestTridentineLentRankNotCommemoration(t *testing.T) {
l := Tridentine()
for _, slug := range []string{
"sts-felicitas-perpetua", "thomas-aquinas", "frances-rome",
"gregory-the-great", "patrick", "benedict", "francis-of-paola",
"isidore-of-seville", "vincent-ferrer",
} {
rc, ok := l.Cels[slug]
if !ok {
t.Errorf("missing %q", slug)
continue
}
if got := rc.Fields["rank"]; got != "class-3" {
t.Errorf("%s: rank = %q, want class-3 (not commemoration)", slug, got)
}
}
}
// TestTridentineGenuineCommemorationStaysCommemoration: the fix above must
// NOT promote every commemoration-only entry indiscriminately -- some really
// are without an independent Mass in the 1960-reformed books, demoted even
// on an ORDINARY, unprivileged day (not explained by Lent/Passiontide or any
// higher-class temporal day). St Blaise (3 Feb, an ordinary Septuagesima-
// season feria) is live-confirmed by missalemeum as a mere commemoration
// even then, so RankCommemoration is the honest, correct rank -- promoting
// him to class-4 would let him wrongly win an ordinary green-season feria he
// has no independent Mass to celebrate.
func TestTridentineGenuineCommemorationStaysCommemoration(t *testing.T) {
l := Tridentine()
rc, ok := l.Cels["blaise"]
if !ok {
t.Fatal("missing blaise")
}
if got := rc.Fields["rank"]; got != "commemoration" {
t.Errorf("blaise: rank = %q, want commemoration (genuinely no independent Mass)", got)
}
}
// TestTridentineNoMissingEntries: four entries present in missalemeum (and
// so in the calendarium) were silently dropped by the generator's own
// dedup, which keyed a single global map by SLUG alone -- two by a literal
// slug collision with an unrelated feast of the same English title on a
// different date (28 January's "St. Agnes", the traditional SECOND
// commemoration of 21 January's own Agnes; 14 May's "St. Boniface", a
// different martyr from 5 June's Boniface of Mainz), and two (Evaristus,
// Theodore) by the generator returning on the first reference year that
// showed an observed office, before ever reaching the later year that
// revealed their commemoration.
func TestTridentineNoMissingEntries(t *testing.T) {
l := Tridentine()
want := map[string]string{
"agnes-secundo": "01-28",
"boniface-martyr": "05-14",
"evaristus": "10-26",
"theodore": "11-09",
}
for slug, date := range want {
rc, ok := l.Cels[slug]
if !ok {
t.Errorf("missing %q (%s)", slug, date)
continue
}
if got := rc.Fields["date"]; got != date {
t.Errorf("%s: date = %q, want %q", slug, got, date)
}
}
}
// TestTridentineRomanusAndEusebiusPresent: CORRECTED after review found the
// opposite claim resting on an incomplete primary-source check. An earlier
// version of this test (and of scripts/gen-sanctoral-ef.go's
// knownSpuriousComm) asserted Romanus ABSENT, on the strength of ONE of the
// three local Missal scans -- "1962-06-23,…LT.pdf", an ELECTRONIC
// TRANSCRIPTION that silently drops vigil commemorations generally (also
// missing: 7 August Donatus, 25 December Anastasia, both present elsewhere).
// The other two, PHOTOGRAPHIC scans of the actual 1962 Missale Romanum, both
// carry it: "missale-romanum-1962.pdf" calendarium, 9 August row: "XVI d V
// 9 Vigilia, III classis, Commemoratio S. Romani Mart.", with the saint's
// own proper text elsewhere in the same scan ("Et fit commemoratio S. Romani
// Mar-"), and its own back-of-book index ("Romani Mart., 9 augusti ... 621").
// Where the scans and the transcription disagree, the scans win -- see
// gen-sanctoral-ef.go's own primary-source note, same rule recorded there so
// it is not lost a second time.
//
// 14 August's "St. Eusebius" is the identical shape (calendarium: "XI b XIX
// 14 Vigilia, II classis, Commemoratio S. Eusebii Conf.") and a DIFFERENT
// person from 16 December's "St. Eusebius, Ep. et Mart." (calendarium: "V
// XVII S. Eusebii Ep. et Mart., III classis.") -- a Confessor and a Bishop
// and Martyr, not the same saint moved or duplicated. missalemeum gives both
// the same bare English title, so they collide by slug; slugOverride
// disambiguates rather than either being dropped.
func TestTridentineRomanusAndEusebiusPresent(t *testing.T) {
l := Tridentine()
if rc, ok := l.Cels["romanus"]; !ok {
t.Error("romanus missing: the calendarium's photographic scans both carry \"Commemoratio S. Romani Mart.\" on 9 August")
} else if rc.Fields["date"] != "08-09" {
t.Errorf("romanus: date = %q, want 08-09", rc.Fields["date"])
}
want := map[string]struct{ date, rank string }{
// St Eusebius Confessor (14 Aug) and St Romanus (9 Aug, checked
// above) are both bare "Commemoratio" in the calendarium (no class
// of their own) -- pinned here, not just presence/date, per
// TestTridentineCommemorationRanksPinned's own reasoning below:
// romanus's own rank is pinned there instead, since that test
// groups every commemoration-rank entry this task's fixes touched
// in one place. eusebius-confessor is listed here because it is
// the OTHER half of this specific test's own slug-disambiguation
// story.
"eusebius-confessor": {"08-14", "commemoration"}, // "S. Eusebii Conf." -- a Confessor
"eusebius": {"12-16", "class-3"}, // "S. Eusebii Ep. et Mart." -- a Bishop and Martyr, a different person, unaffected by this round
}
for slug, w := range want {
rc, ok := l.Cels[slug]
if !ok {
t.Errorf("missing %q (%s)", slug, w.date)
continue
}
if rc.Fields["date"] != w.date {
t.Errorf("%s: date = %q, want %q", slug, rc.Fields["date"], w.date)
}
if rc.Fields["rank"] != w.rank {
t.Errorf("%s: rank = %q, want %q", slug, rc.Fields["rank"], w.rank)
}
}
}
// TestTridentineCommemorationRanksPinned: nothing in this test suite
// asserted `rank` for thomas-becket, silvester, or romanus before this --
// `TestTridentineRomanusAndEusebiusPresent` above pinned presence and date
// for romanus but not rank, and Thomas Becket/Silvester (the I3 fix, RG
// 68(d)/(e)) had no rank assertion anywhere at all. So a future
// regeneration could silently rewrite any of the three back to class-4 --
// the EXACT I3 failure mode -- with the suite green throughout. Pinned
// together because they are the same failure shape (a bare "Commemoratio"
// in the calendarium, no class of its own, silently promoted by
// refYearExplainsAbsence's own coupling to temporal_ef.go, see that
// function's doc comment) even though they were found in two different
// review rounds (romanus in C2, Thomas Becket/Silvester in I3).
func TestTridentineCommemorationRanksPinned(t *testing.T) {
l := Tridentine()
for _, slug := range []string{"romanus", "thomas-becket", "silvester"} {
rc, ok := l.Cels[slug]
if !ok {
t.Errorf("missing %q", slug)
continue
}
if rc.Fields["rank"] != "commemoration" {
t.Errorf("%s: rank = %q, want commemoration", slug, rc.Fields["rank"])
}
}
}
// TestTridentineNoTransferArtifacts: a movable-transfer feast displayed on
// whatever civil date it actually landed on in a given reference year (St
// Joseph pushed to 20 March by a Sunday of Lent; the Annunciation deferred
// past Holy Week; All Souls moved to the Monday; St Matthias shown on the
// 25th in a leap year) must not leak into the sanctoral as a phantom
// fixed-date entry keyed to that transferred civil date.
func TestTridentineNoTransferArtifacts(t *testing.T) {
l := Tridentine()
for _, slug := range []string{
"joseph-spouse-of-the-bl-virgin-mary-0320",
"annunciation-of-the-blessed-virgin-mary-0405",
"annunciation-of-the-blessed-virgin-mary-0408",
"annunciation-of-the-blessed-virgin-mary-0409",
"commemoration-of-all-souls-1103",
"matthias-0225",
} {
if _, ok := l.Cels[slug]; ok {
t.Errorf("phantom transfer-artifact entry %q present", slug)
}
}
}
// TestTridentineNamesPreservedAcrossRegeneration is the coverage guard a
// regeneration silently destroying a whole language's names needed and did
// not have: an earlier version of scripts/gen-sanctoral-ef.go's main()
// preserved existing name.la values across a regeneration (missalemeum
// supplies English only) but had no equivalent for name.pl -- and, because
// the bootstrapped file has in fact never carried a name.la value, that
// mechanism looked correct while doing nothing at all. A regeneration
// deleted all 322 Polish names outright (name.pl count 322 -> 0), silently:
// no test here asserted anything about a name.* field, and
// `len(l.Cels) < 250` (TestTridentineLoads) does not notice a field going
// missing within entries that still exist. `naming.CelebrationName`'s own
// name[lang] -> name.en fallback (internal/naming/naming.go) then quietly
// substituted English for Polish on every EF display in that language,
// reaching mobile.Day(date, "ef", version, "pl") -- a shipped dlectio entry
// point -- with no error anywhere in the chain.
//
// CORRECTED after review: the first version of this test counted name.pl
// only (a hardcoded single language -- the exact "whitelist of two
// languages" mistake C1's own fix was written to stop repeating, just
// moved into the test), and its coverage floor (>= 315) had 7 entries of
// slack -- the review proved dropping 6 entries' Polish names still passed
// it. Fixed on both axes: the language set is DISCOVERED from the data
// (every "name.<lang>" key actually present, not a hardcoded list, so a
// regeneration dropping name.la or introducing a future name.de is
// checked the same way as name.pl), and the floor for each discovered
// language is its EXACT true count, not a loose approximation -- verified
// directly against the branch point (`git show 2386a45:...`) once, by
// hand, and hardcoded as the answer, not derived at test time from data
// that could itself be wrong.
//
// True counts, independently verified: name.en on every entry (327, all
// regenerated fresh from missalemeum, including the 5 entries this task's
// own fixes added); name.pl on exactly 322 (preserved from the branch
// point; the 5 new entries -- St Agnes secundo, St Boniface Martyr, St
// Evaristus, St Theodore, St Eusebius Confessor -- never had a curated
// Polish name to preserve in the first place, so 322, not 327, is the
// correct target, not a shortfall); no OTHER name.<lang> exists in the
// branch point at all (name.la is read by the generator but the
// bootstrapped file has in fact never carried one), so the discovered
// language set itself must be exactly {en, pl} -- a regeneration that
// silently introduced or lost an entire language key, not just some
// values within one, is caught by this assertion, not only by the count.
func TestTridentineNamesPreservedAcrossRegeneration(t *testing.T) {
l := Tridentine()
counts := map[string]int{}
for _, rc := range l.Cels {
for k, v := range rc.Fields {
if v == "" || !strings.HasPrefix(k, "name.") {
continue
}
lang := strings.TrimPrefix(k, "name.")
counts[lang]++
}
}
wantLangs := map[string]bool{"en": true, "pl": true}
for lang := range counts {
if !wantLangs[lang] {
t.Errorf("unexpected name.%s present (%d entries) -- discovered language set must be exactly {en, pl}", lang, counts[lang])
}
}
for lang := range wantLangs {
if counts[lang] == 0 {
t.Errorf("name.%s entirely absent -- discovered language set must be exactly {en, pl}", lang)
}
}
if got := counts["en"]; got != len(l.Cels) {
t.Errorf("name.en coverage = %d entries, want %d (every entry, all freshly regenerated)", got, len(l.Cels))
}
if got := counts["pl"]; got != 322 {
t.Errorf("name.pl coverage = %d entries, want exactly 322 (the branch point's own true count; the 5 entries this task added have no curated Polish name to preserve, so more or fewer than 322 is a bug either way)", got)
}
rc, ok := l.Cels["assumption-of-the-blessed-virgin-mary"]
if !ok {
t.Fatal("missing assumption-of-the-blessed-virgin-mary")
}
if got := rc.Fields["name.pl"]; got != "Wniebowzięcie N. M. P." {
t.Errorf("assumption-of-the-blessed-virgin-mary: name.pl = %q, want the preserved Polish name", got)
}
}
// TestTridentineMissalOverrides pins the four fields where the 1962 MISSALE
// ROMANUM decides against missalemeum, and the Missal wins.
//
// This is the safety net for a specific accident. This file is generated by
// scripts/gen-sanctoral-ef.go from missalemeum, which does NOT carry any of
// these values; the generator re-applies them from its own missalOverrides
// table on every run. If that table is ever removed, or a slug is renamed
// upstream so an override silently stops matching, the generated file quietly
// reverts to missalemeum's values and nothing else in this repository would
// notice. This test notices.
//
// Each was found by the sibling project colitur, which validates this data
// against the Missal and its Rubricae Generales rather than against another
// calendar program:
//
// - ubaldus and didacus: the Missal's own universal calendarium ranks both
// "III classis" outright, with no commemoration rubric, and each has a
// Mass entry of its own in the Proprium taking its readings from a Common
// -- which a bare commemoration never has.
// - the two August vigils: RG 128 gives violet to "vigils of II and III
// class outside Paschaltide". Both qualify. The rule's sole exception is
// the Ascension's vigil, inside Paschaltide, which is white -- and which
// is therefore checked here too, in the opposite direction, so that a
// careless "make every vigil violet" fix would fail rather than pass.
func TestTridentineMissalOverrides(t *testing.T) {
l := Tridentine()
for _, c := range []struct{ slug, field, want, why string }{
{"ubaldus", "rank", "class-3", "Missal calendarium: S. Ubaldi Ep. et Conf., III classis"},
{"didacus", "rank", "class-3", "Missal calendarium: S. Didaci Conf., III classis"},
{"vigil-of-st-lawrence", "colour", "violet", "RG 128: III-class vigil outside Paschaltide"},
{"vigil-of-the-assumption", "colour", "violet", "RG 128: II-class vigil outside Paschaltide"},
// Saints whose Mass comes from a Common. The Missal prints no proper
// for these; its rubric at each names a Common and only the Oratio is
// proper. Without the readings the day resolves nothing at all, so
// these are pinned like the rank and colour overrides -- a regeneration
// that stopped applying them would leave nine saints mute.
{"ubaldus", "reading.first", "Ecclus 44:16-27; 45:3-20", "Common of a Confessor Bishop I"},
{"didacus", "reading.first", "1 Cor 4:9-14", "Common of a Confessor not a Bishop II"},
{"isidore-of-seville", "reading.first", "2 Tim 4:1-8", "Common of Doctors"},
{"benedict", "reading.first", "Ecclus 45:1-6", "Common of Abbots"},
// RG 124: red is for Apostles, Evangelists and Martyrs. missalemeum has
// these inverted in BOTH directions -- confessors and virgins red, two
// actual martyrs white -- which is why both directions are pinned here.
// A fix that turned every one of them white would pass ten of these
// twelve and fail the two that matter.
{"alphonsus-liguori", "colour", "white", "RG 124: not a martyr, so white"},
{"apollinaris", "colour", "red", "RG 124(e): Bishop and MARTYR, red"},
{"augustine", "colour", "white", "RG 124: not a martyr, so white"},
{"camillus-de-lellis", "colour", "white", "RG 124: not a martyr, so white"},
{"ephrem-of-syria", "colour", "white", "RG 124: not a martyr, so white"},
{"jerome-emiliani", "colour", "white", "RG 124: not a martyr, so white"},
{"john-gualbert", "colour", "white", "RG 124: not a martyr, so white"},
{"john-of-san-fecundo", "colour", "white", "RG 124: not a martyr, so white"},
{"josaphat", "colour", "red", "RG 124(e): Bishop and MARTYR, red"},
{"julia-of-falconieri", "colour", "white", "RG 124: not a martyr, so white"},
{"martha", "colour", "white", "RG 124: not a martyr, so white"},
{"rose-of-lima", "colour", "white", "RG 124: not a martyr, so white"},
} {
rc, ok := l.Cels[c.slug]
if !ok {
t.Errorf("missing %q -- if the slug was renamed upstream, the generator's "+
"missalOverrides entry must be renamed with it, or the correction is lost", c.slug)
continue
}
if got := rc.Fields[c.field]; got != c.want {
t.Errorf("%s: %s = %q, want %q (%s). This is a MISSAL correction that "+
"missalemeum does not carry -- if it reverted, the generator's "+
"missalOverrides table has stopped being applied", c.slug, c.field, got, c.want, c.why)
}
}
// The exception, asserted in the other direction: RG 128's vigil rule is
// "outside Paschaltide", and the Ascension's vigil is inside it.
if rc, ok := l.Cels["vigil-of-the-ascension"]; ok {
if got := rc.Fields["colour"]; got != "" && got != "white" {
t.Errorf("vigil-of-the-ascension: colour = %q, want white -- RG 128's "+
"violet rule is for vigils OUTSIDE Paschaltide and this one is inside it", got)
}
}
}
// TestEFBVMSaturdayReadings pins RG 309(a)'s seasonal selection among the five
// Missae de sancta Maria in sabbato. Without it the day was kept as Our Lady's
// office (RG 78) but read the FERIA's Mass -- so a Saturday in Septuagesima
// announced Our Lady and then read the parable of the labourers.
//
// Citations verified by colitur against both photographic scans of the 1962
// Missal; see its lectionary_ef.ml for the per-formulary scan references.
func TestEFBVMSaturdayReadings(t *testing.T) {
sel := calendar.Selection{Form: "old"}
for _, c := range []struct{ date, first string }{
{"2026-01-03", "Titus 3:4-7"}, // Christmas Time
{"2026-01-10", "Titus 3:4-7"}, // still Christmas Time
{"2026-02-14", "Ecclus 24:14-16"}, // after the Purification
{"2026-06-20", "Ecclus 24:14-16"}, // Time after Pentecost
{"2026-04-18", "Ecclus 24:14-16"}, // Paschaltide -- same first, DIFFERENT gospel
} {
day, _ := time.Parse("2006-01-02", c.date)
layers := []calendar.Layer{Tridentine()}
ld := calendar.Compute(day, sel, layers)
got := Readings(sel, layers, day, ld)
if len(got) == 0 || got[0].Citation != c.first {
first := "(none)"
if len(got) > 0 {
first = got[0].Citation
}
t.Errorf("%s: first reading = %q, want %q (RG 309(a), the Mass of Our Lady on Saturday)",
c.date, first, c.first)
}
}
// Paschaltide shares its first reading with the rest of the year but has
// its own gospel -- St John at the Cross rather than the woman in the
// crowd. Asserted separately because a first-reading-only test would pass
// with the wrong gospel.
day, _ := time.Parse("2006-01-02", "2026-04-18")
layers := []calendar.Layer{Tridentine()}
got := Readings(sel, layers, day, calendar.Compute(day, sel, layers))
var gospel string
for _, r := range got {
if r.Part == "gospel" {
gospel = r.Citation
}
}
if gospel != "John 19:25-27" {
t.Errorf("2026-04-18: gospel = %q, want %q (the Paschaltide formulary)", gospel, "John 19:25-27")
}
}
// TestEFNativityOctaveWeekdayMass pins the Mass of a WEEKDAY within the Octave
// of the Nativity, and the Sunday it must not swallow.
//
// The Missal gives 29-31 December a direct formulary, "Diebus infra octavam
// Nativitatis Domini, II classis" -- Titus 3:4-7 / Luke 2:15-20 -- and names
// them in the calendarium ("De V die infra octavam ... II classis"). Treated
// as ordinary ferias they fell back to the preceding Sunday and served
// "Dominica infra octavam" (Gal 4:1-7 / Luke 2:33-40), which is a different
// heading for a different day.
//
// The Sunday case is asserted too, because it is the one the wrong citation
// blurred: when 29-31 December IS a Sunday the day really is "Dominica infra
// octavam" and keeps that Mass. A fix that applied the weekday formulary by
// date alone would pass the first half of this test and fail the second.
func TestEFNativityOctaveWeekdayMass(t *testing.T) {
sel := calendar.Selection{Form: "old"}
layers := []calendar.Layer{Tridentine()}
get := func(date string) (string, string) {
day, _ := time.Parse("2006-01-02", date)
var first, gospel string
for _, r := range Readings(sel, layers, day, calendar.Compute(day, sel, layers)) {
switch r.Part {
case "first":
first = r.Citation
case "gospel":
gospel = r.Citation
}
}
return first, gospel
}
// 2026: 29-31 December are Tuesday, Wednesday, Thursday -- weekdays.
for _, date := range []string{"2026-12-29", "2026-12-30", "2026-12-31"} {
if f, g := get(date); f != "Titus 3:4-7" || g != "Luke 2:15-20" {
t.Errorf("%s: got %q / %q, want Titus 3:4-7 / Luke 2:15-20 "+
"(Diebus infra octavam Nativitatis Domini)", date, f, g)
}
}
// 2030: 29 December is a SUNDAY, so it is Dominica infra octavam and keeps
// the Sunday's own Mass.
if f, g := get("2030-12-29"); f != "Gal 4:1-7" || g != "Luke 2:33-40" {
t.Errorf("2030-12-29 (a Sunday): got %q / %q, want Gal 4:1-7 / Luke 2:33-40 "+
"-- the weekday formulary must not swallow Dominica infra octavam", f, g)
}
}
|