aboutsummaryrefslogtreecommitdiff
path: root/internal/readings/readings_test.go
blob: c81e324ac528f6a6f97f29ac34508a1f1f6e5b9c (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
package readings

import (
	"reflect"
	"strings"
	"testing"

	"github.com/lukaszkasprzak/lectio/internal/config"
	"github.com/lukaszkasprzak/lectio/internal/liturgy"
)

func sectionsForFilterTests() []liturgy.Section {
	return []liturgy.Section{
		{PartID: "pierwsze_czytanie", Heading: "1. czytanie"},
		{PartID: "psalm", Heading: "Psalm"},
		{PartID: "ewangelia", Heading: "Ewangelia"},
	}
}

func TestFilterPartsGospelOnly(t *testing.T) {
	secs := sectionsForFilterTests()
	got := filterParts(secs, config.Config{}, false)

	if len(got) != 1 {
		t.Fatalf("got %d sections, want 1: %+v", len(got), got)
	}
	if got[0].PartID != "ewangelia" {
		t.Errorf("got PartID %q, want ewangelia", got[0].PartID)
	}
}

func TestFilterPartsAll(t *testing.T) {
	secs := sectionsForFilterTests()
	cfg := config.Config{
		Lectionary: "new",
		Parts: map[string]map[string]bool{
			"new": {"psalm": false},
		},
	}
	got := filterParts(secs, cfg, true)

	if len(got) != 2 {
		t.Fatalf("got %d sections, want 2: %+v", len(got), got)
	}
	for _, s := range got {
		if s.PartID == "psalm" {
			t.Errorf("psalm should have been filtered out: %+v", got)
		}
	}
	ids := map[string]bool{got[0].PartID: true, got[1].PartID: true}
	if !ids["pierwsze_czytanie"] || !ids["ewangelia"] {
		t.Errorf("got %+v, want pierwsze_czytanie and ewangelia", got)
	}
}

func TestFilterPartsTraditionalAllReadingsOnly(t *testing.T) {
	secs := []liturgy.Section{
		{PartID: "introitus", Heading: "Introit"},
		{PartID: "oratio", Heading: "Kolekta"},
		{PartID: "lectio", Heading: "Lekcja"},
		{PartID: "graduale", Heading: "Graduał"},
		{PartID: "evangelium", Heading: "Ewangelia"},
		{PartID: "offertorium", Heading: "Ofiarowanie"},
		{PartID: "secreta", Heading: "Sekreta"},
		{PartID: "communio", Heading: "Komunia"},
		{PartID: "postcommunio", Heading: "Pokomunia"},
	}
	cfg := config.Config{Lectionary: "traditional"}
	got := filterParts(secs, cfg, true)

	if len(got) != 2 {
		t.Fatalf("got %d sections, want 2: %+v", len(got), got)
	}
	if got[0].PartID != "lectio" || got[1].PartID != "evangelium" {
		t.Errorf("got %+v, want [lectio evangelium] in order", got)
	}
}

// TestLoadModern computes the Ordinary Form day offline: the gospel section is
// present and the header carries the celebration name (English by default,
// since the universal sanctoral is authored in English).
func TestLoadModern(t *testing.T) {
	cfg := config.Config{Lectionary: "new"}
	secs, info, err := Load(cfg, Options{Date: "2026-07-22", All: true})
	if err != nil {
		t.Fatalf("Load: %v", err)
	}

	found := false
	for _, s := range secs {
		if s.PartID == "ewangelia" {
			found = true
		}
	}
	if !found {
		t.Errorf("no gospel section (PartID=ewangelia) found in %+v", secs)
	}
	if !strings.Contains(info.Name, "Mary Magdalene") {
		t.Errorf("DayInfo.Name = %q, want it to contain %q", info.Name, "Mary Magdalene")
	}
}

// TestSundayRankIsDisplayOnly guards the Ordinary Form's Sunday display rank.
// Sunday is its own category in the 1969 Universal Norms' Table of Liturgical
// Days -- never a solemnity -- so an ordinary "Nth Sunday of <season>" temporal
// day (Ordinary Time, Advent, Lent, the Easter season, Christmas time; this
// also covers Palm Sunday) must report "sunday". A NAMED solemnity of the Lord
// that happens to fall on a Sunday (Easter Sunday itself, Pentecost) is a
// genuine solemnity and must keep reporting "solemnity"; a feast of the Lord
// (Holy Family) must keep reporting "feast". This is display-only: it must
// never touch the engine's internal precedence, so the 1962 form (which has no
// such Sunday category -- Sundays report their class exactly as before) is
// asserted unchanged too. Dates are the ones from the original bug report,
// plus the Easter/Pentecost/Holy Family dates they imply for the same
// liturgical year.
func TestSundayRankIsDisplayOnly(t *testing.T) {
	cases := []struct {
		date, lect, wantRank, why string
	}{
		{"2026-08-09", "new", "sunday", "19th Sunday in Ordinary Time"},
		{"2026-11-29", "new", "sunday", "1st Sunday of Advent"},
		{"2027-03-28", "new", "solemnity", "Easter Sunday itself"},
		{"2027-05-16", "new", "solemnity", "Pentecost"},
		{"2026-12-27", "new", "feast", "Holy Family"},
		{"2026-08-09", "traditional", "class-2", "1962 Sunday after Pentecost"},
		{"2026-11-29", "traditional", "class-1", "1962 1st Sunday of Advent"},
	}
	for _, c := range cases {
		cfg := config.Config{Lectionary: c.lect}
		_, info, err := Load(cfg, Options{Date: c.date, All: true})
		if err != nil {
			t.Fatalf("%s (%s, %s): Load: %v", c.date, c.lect, c.why, err)
		}
		if info.Rank != c.wantRank {
			t.Errorf("%s (%s, %s): Rank = %q, want %q (name=%q)", c.date, c.lect, c.why, info.Rank, c.wantRank, info.Name)
		}
	}
}

// TestLoadWithAgreesWithLoad guards the fast path multi-date callers (e.g.
// mobile.Days) use to avoid re-stacking the calendar layers and re-parsing
// the book table once per date: LoadWith, given a cfg's own Prepared value,
// must return exactly what Load(cfg, opts) returns, for every date and both
// forms. This is the correctness backstop for the perf fix -- Prepare only
// hoists WHEN the date-independent setup happens, never WHAT it computes.
func TestLoadWithAgreesWithLoad(t *testing.T) {
	dates := []string{
		"2026-07-22", // ordinary weekday
		"2028-02-29", // leap day
		"2026-04-09", // Holy Thursday 2026
		"2026-04-10", // Good Friday 2026
		"2026-04-11", // Holy Saturday 2026
		"2026-11-02", // All Souls (a Requiem day)
		"2026-12-25", // Christmas
	}
	for _, lect := range []string{"new", "traditional"} {
		cfg := config.Config{Lectionary: lect}
		p := Prepare(cfg)
		for _, date := range dates {
			opts := Options{Date: date, All: true}
			wantSecs, wantInfo, wantErr := Load(cfg, opts)
			gotSecs, gotInfo, gotErr := LoadWith(p, cfg, opts)
			if (wantErr == nil) != (gotErr == nil) {
				t.Fatalf("%s %s: Load err=%v, LoadWith err=%v", lect, date, wantErr, gotErr)
			}
			if wantErr != nil {
				continue
			}
			if gotInfo != wantInfo {
				t.Errorf("%s %s: LoadWith info = %+v, want %+v", lect, date, gotInfo, wantInfo)
			}
			if len(gotSecs) != len(wantSecs) {
				t.Fatalf("%s %s: LoadWith %d sections, want %d", lect, date, len(gotSecs), len(wantSecs))
			}
			for i := range wantSecs {
				if !reflect.DeepEqual(gotSecs[i], wantSecs[i]) {
					t.Errorf("%s %s: section %d = %+v, want %+v", lect, date, i, gotSecs[i], wantSecs[i])
				}
			}
		}
	}
}

// TestLoadTraditional computes the Extraordinary Form day offline: it never
// needs the network, and yields the EF epistle+gospel with a header name.
func TestLoadTraditional(t *testing.T) {
	cfg := config.Config{Lectionary: "traditional"}
	secs, info, err := Load(cfg, Options{Date: "2026-07-22", All: true})
	if err != nil {
		t.Fatalf("Load: %v", err)
	}
	found := false
	for _, s := range secs {
		if s.PartID == "evangelium" {
			found = true
		}
	}
	if !found {
		t.Errorf("no EF gospel section (PartID=evangelium) found in %+v", secs)
	}
	if info.Name == "" {
		t.Error("DayInfo.Name empty for a traditional feast day")
	}
}