diff options
Diffstat (limited to 'internal/calendar/transfer_plan_cache_test.go')
| -rw-r--r-- | internal/calendar/transfer_plan_cache_test.go | 154 |
1 files changed, 138 insertions, 16 deletions
diff --git a/internal/calendar/transfer_plan_cache_test.go b/internal/calendar/transfer_plan_cache_test.go index 6b3b48b..1592644 100644 --- a/internal/calendar/transfer_plan_cache_test.go +++ b/internal/calendar/transfer_plan_cache_test.go @@ -150,37 +150,69 @@ func TestEFTransferPlanCacheConcurrentUse(t *testing.T) { }} baseLayers := []calendar.Layer{base} overlaidLayers := []calendar.Layer{base, overlay} + // A third, disjoint key exercising the occupancy-index collision path + // (TestEFOccupancyIndexDetectsSameDateClass1Collision) concurrently too + // -- the plan alone does not touch efOccupancyIndex.occupied unless a + // candidate's own precedence doesn't already decide it, which the + // Joseph/Annunciation scenario above never triggers (see that test's own + // doc comment). + collideLayers := []calendar.Layer{base, {ID: "user", Cels: map[string]calendar.RawCelebration{ + "zz-test-alpha": {Fields: map[string]string{"rank": "class-1", "date": "07-06", "name.en": "ZZ Test Alpha"}, Variants: map[string]map[string]string{}}, + "zz-test-beta": {Fields: map[string]string{"rank": "class-1", "date": "07-06", "name.en": "ZZ Test Beta"}, Variants: map[string]map[string]string{}}, + }}} years := []int{2008, 2011, 2035, 2046} var wg sync.WaitGroup - for g := 0; g < 12; g++ { + for g := 0; g < 15; g++ { g := g wg.Add(1) go func() { defer wg.Done() - layers := baseLayers - if g%2 == 0 { - layers = overlaidLayers // different goroutines hammer different cache keys - } - for i := 0; i < 6; i++ { - y := years[(g+i)%len(years)] - for _, md := range []string{"03-19", "03-31", "04-01"} { - d, err := time.Parse("2006-01-02", time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC).Format("2006")+"-"+md) - if err != nil { - t.Errorf("bad date: %v", err) - return + switch g % 3 { + case 0: + for i := 0; i < 6; i++ { + y := years[(g+i)%len(years)] + for _, md := range []string{"03-19", "03-31", "04-01"} { + d, err := time.Parse("2006-01-02", time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC).Format("2006")+"-"+md) + if err != nil { + t.Errorf("bad date: %v", err) + return + } + _ = calendar.Compute(d.UTC(), sel, baseLayers).Observed.Slug + } + } + case 1: + for i := 0; i < 6; i++ { + y := years[(g+i)%len(years)] + for _, md := range []string{"03-19", "03-31", "04-01"} { + d, err := time.Parse("2006-01-02", time.Date(y, 1, 1, 0, 0, 0, 0, time.UTC).Format("2006")+"-"+md) + if err != nil { + t.Errorf("bad date: %v", err) + return + } + _ = calendar.Compute(d.UTC(), sel, overlaidLayers).Observed.Slug + } + } + default: + for i := 0; i < 6; i++ { + for _, ymd := range []string{"2026-07-06", "2026-07-07", "2026-07-08"} { + d, err := time.Parse("2006-01-02", ymd) + if err != nil { + t.Errorf("bad date: %v", err) + return + } + _ = calendar.Compute(d.UTC(), sel, collideLayers).Observed.Slug } - _ = calendar.Compute(d.UTC(), sel, layers).Observed.Slug } } }() } wg.Wait() - // After the concurrent hammering, correctness must still hold for both - // keys -- the concurrency test is not a substitute for the correctness - // test above, so re-assert both outcomes here too. + // After the concurrent hammering, correctness must still hold for all + // three keys -- the concurrency test is not a substitute for the + // correctness tests above, so re-assert their outcomes here too. d, _ := time.Parse("2006-01-02", "2008-04-01") if got := calendar.Compute(d.UTC(), sel, baseLayers).Observed.Slug; got != "joseph-spouse-of-the-bl-virgin-mary" { t.Errorf("after concurrent use, base 2008-04-01 = %q, want joseph-spouse-of-the-bl-virgin-mary", got) @@ -188,4 +220,94 @@ func TestEFTransferPlanCacheConcurrentUse(t *testing.T) { if got := calendar.Compute(d.UTC(), sel, overlaidLayers).Observed.Slug; got == "joseph-spouse-of-the-bl-virgin-mary" { t.Errorf("after concurrent use, overlaid 2008-04-01 = %q, want NOT joseph", got) } + d2, _ := time.Parse("2006-01-02", "2026-07-06") + if got := calendar.Compute(d2.UTC(), sel, collideLayers).Observed.Slug; got != "ef-time-after-pentecost-6-monday" { + t.Errorf("after concurrent use, colliding 2026-07-06 = %q, want ef-time-after-pentecost-6-monday", got) + } +} + +// TestEFOccupancyIndexDetectsSameDateClass1Collision covers a path the tests +// above do not: efOccupancyIndex.occupied's use from computeEF's +// transferIfImpededEF call site (the per-candidate fallback for a class-1 +// entry efTransferPlan judged NOT impeded by temporal precedence alone), and +// efOccupancyIndex's own use inside efTransferPlan's initial impeded check +// (occupiedByClass1(when, cel.Slug) -- "does some OTHER fixed-date class-1 +// SANCTORAL feast already sit on `when`", the doc comment's own example, RG +// 97/98). Neither TestEFTwoTransfersDoNotCollide nor the overlay/year tests +// above exercise it: St Joseph and the Annunciation are impeded by HOLY +// WEEK'S OWN temporal precedence, on DIFFERENT original dates -- never by +// colliding with each other's original date -- so no existing test had ever +// driven two class-1 SANCTORAL entries onto the exact same calendar date. +// +// A synthetic overlay is used because no two class-1 feasts share a fixed +// date in the shipped 1962 calendar (an ordinary Time-after-Pentecost +// Monday, 6 July 2026, was checked empirically before writing this test: +// alone, a single synthetic class-1 entry there is simply observed, since +// nothing outranks or occupies it; the real collision case exists only by +// construction). +// +// What is varied: whether a SECOND class-1 entry shares the first one's +// date -- both entries otherwise identical (rank class-1, real content +// unrelated to any liturgical rule under test). If efOccupancyIndex failed +// to detect the collision (or wrongly matched exceptSlug against ITSELF, +// the most dangerous failure shape -- see the mutation proof below), 6 July +// would keep reporting the lone entry regardless. +func TestEFOccupancyIndexDetectsSameDateClass1Collision(t *testing.T) { + sel := calendar.DefaultSelection() + sel.Form = "old" + base := caldata.Tridentine() + + compute := func(layers []calendar.Layer, date string) string { + d, err := time.Parse("2006-01-02", date) + if err != nil { + t.Fatalf("bad test date %q: %v", date, err) + } + return calendar.Compute(d.UTC(), sel, layers).Observed.Slug + } + + alpha := calendar.RawCelebration{ + Fields: map[string]string{"rank": "class-1", "date": "07-06", "name.en": "ZZ Test Alpha"}, + Variants: map[string]map[string]string{}, + } + beta := calendar.RawCelebration{ + Fields: map[string]string{"rank": "class-1", "date": "07-06", "name.en": "ZZ Test Beta"}, + Variants: map[string]map[string]string{}, + } + + soloLayers := []calendar.Layer{base, {ID: "user", Cels: map[string]calendar.RawCelebration{ + "zz-test-alpha": alpha, + }}} + collideLayers := []calendar.Layer{base, {ID: "user", Cels: map[string]calendar.RawCelebration{ + "zz-test-alpha": alpha, + "zz-test-beta": beta, + }}} + + // Alone, alpha is simply observed on its own date: nothing occupies 6 + // July, so efTransferPlan judges it unimpeded and computeEF's fallback + // (occ.occupied via transferIfImpededEF) must agree and leave it there. + if got := compute(soloLayers, "2026-07-06"); got != "zz-test-alpha" { + t.Fatalf("alpha alone, 2026-07-06 = %q, want zz-test-alpha", got) + } + + // Add beta on the SAME date. Both now occupy each other's date, so BOTH + // are impeded (RG 97/98) and transfer forward in table/impeded-first + // order (alpha to 7 July, beta to 8 -- empirically confirmed before + // writing this test); 6 July itself reverts to the ordinary temporal + // office, since NEITHER candidate keeps its place. + if got := compute(collideLayers, "2026-07-06"); got != "ef-time-after-pentecost-6-monday" { + t.Errorf("alpha+beta colliding, 2026-07-06 = %q, want ef-time-after-pentecost-6-monday (a stale/broken occupancy index would still show zz-test-alpha)", got) + } + if got := compute(collideLayers, "2026-07-07"); got != "zz-test-alpha" { + t.Errorf("alpha+beta colliding, 2026-07-07 = %q, want zz-test-alpha (transferred here)", got) + } + if got := compute(collideLayers, "2026-07-08"); got != "zz-test-beta" { + t.Errorf("alpha+beta colliding, 2026-07-08 = %q, want zz-test-beta (transferred here)", got) + } + + // And alpha alone (no beta) must be unaffected by having since computed + // the colliding scenario -- the two overlays must not cross-contaminate + // the shared cache. + if got := compute(soloLayers, "2026-07-06"); got != "zz-test-alpha" { + t.Errorf("alpha alone after collision query, 2026-07-06 = %q, want zz-test-alpha (unaffected)", got) + } } |
