package caldata import ( "strings" "testing" "github.com/lukaszkasprzak/lectio/internal/calendar" ) 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." 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. 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) } }