package catalog import ( "path/filepath" "testing" "time" ) // fakeCloser counts how many times it's been called. Used to assert the // drain-grace semantics of SnapshotHolder. type fakeCloser struct{ hits int } func (f *fakeCloser) Close() error { f.hits++; return nil } // withFakeCloser swaps SnapshotReader's closer for our counter. Returns the // reader and the counter; tests then call holder.Set(reloaded) and assert // that close is invoked after the drain grace. func withFakeCloser(t *testing.T, path string, fc *fakeCloser) *SnapshotReader { t.Helper() r, err := ReadSnapshotFile(path) if err != nil { t.Fatal(err) } // replace real closer with our counter; the test asserts only via fc r.closer = fc.Close return r } // TestSnapshotHolderReloadDrainsOld — Set swaps the current reader; the // previous reader is queued. Before grace: still Get-able. After grace: // reaped and Close fires. // // We use a very short grace so the test doesn't sleep long. func TestSnapshotHolderReloadDrainsOld(t *testing.T) { dir := t.TempDir() // 1) Build a valid first snapshot (skus A-1 / A-2). w := NewSnapshotWriter() if err := w.Append(makeProjection("A-1", 100)); err != nil { t.Fatal(err) } if err := w.Append(makeProjection("A-2", 200)); err != nil { t.Fatal(err) } if _, err := w.Write(dir, "first.bin"); err != nil { t.Fatal(err) } firstCloser := &fakeCloser{} first := withFakeCloser(t, filepath.Join(dir, "first.bin"), firstCloser) defer first.Close() // 2) Build a second snapshot (skus B-1). w2 := NewSnapshotWriter() if err := w2.Append(makeProjection("B-1", 300)); err != nil { t.Fatal(err) } if _, err := w2.Write(dir, "second.bin"); err != nil { t.Fatal(err) } secondCloser := &fakeCloser{} second := withFakeCloser(t, filepath.Join(dir, "second.bin"), secondCloser) defer second.Close() h := NewSnapshotHolder(50 * time.Millisecond) defer h.Close() h.Set(first) if h.Len() != 2 { t.Errorf("Len after first Set: got %d want 2", h.Len()) } if _, ok := h.Get("A-1"); !ok { t.Error("A-1 not found in initial state") } // 3) Swap to second — first is now queued. h.Set(second) if h.Len() != 1 { t.Errorf("Len after second Set: got %d want 1", h.Len()) } if firstCloser.hits != 0 { t.Errorf("firstCloser hits: got %d want 0 (still in drain grace)", firstCloser.hits) } if h.DrainCount() != 1 { t.Errorf("DrainCount: got %d want 1", h.DrainCount()) } // 4) Wait past grace, force a reap tick. time.Sleep(80 * time.Millisecond) h.reapNow() if firstCloser.hits != 1 { t.Errorf("firstCloser hits: got %d want 1 (should have reaped)", firstCloser.hits) } if h.DrainCount() != 0 { t.Errorf("DrainCount after reap: got %d want 0", h.DrainCount()) } } // TestSnapshotHolderEmptyGet — holder with no current serves empty. func TestSnapshotHolderEmptyGet(t *testing.T) { h := NewSnapshotHolder(100 * time.Millisecond) defer h.Close() if _, ok := h.Get("anything"); ok { t.Error("empty holder should return false") } if h.Len() != 0 { t.Errorf("Len: got %d want 0", h.Len()) } } // TestSnapshotHolderCloseImmediatelyReaps — Close with pending drains // should close them now (no grace wait). Confirms the drain queue is // drained on Close. func TestSnapshotHolderCloseImmediatelyReaps(t *testing.T) { dir := t.TempDir() w := NewSnapshotWriter() if err := w.Append(makeProjection("X-1", 1)); err != nil { t.Fatal(err) } if _, err := w.Write(dir, "x.bin"); err != nil { t.Fatal(err) } fc1 := &fakeCloser{} r1 := withFakeCloser(t, filepath.Join(dir, "x.bin"), fc1) w2 := NewSnapshotWriter() if err := w2.Append(makeProjection("Y-1", 2)); err != nil { t.Fatal(err) } if _, err := w2.Write(dir, "y.bin"); err != nil { t.Fatal(err) } fc2 := &fakeCloser{} r2 := withFakeCloser(t, filepath.Join(dir, "y.bin"), fc2) h := NewSnapshotHolder(10 * time.Second) // long grace so reap wouldn't happen on its own h.Set(r1) h.Set(r2) if fc1.hits != 0 || fc2.hits != 0 { t.Errorf("premature Close hits: fc1=%d fc2=%d", fc1.hits, fc2.hits) } if err := h.Close(); err != nil { t.Errorf("Close: %v", err) } if fc1.hits != 1 || fc2.hits != 1 { t.Errorf("after Close: fc1=%d fc2=%d want 1 each", fc1.hits, fc2.hits) } } // TestSnapshotHolderIdentityReload — Open the same path twice; current reader // is replaced; DrainCount returns 1 (old queued). Old's data still served // until grace elapses. func TestSnapshotHolderIdentityReload(t *testing.T) { dir := t.TempDir() w := NewSnapshotWriter() if err := w.Append(makeProjection("ID-1", 1)); err != nil { t.Fatal(err) } if _, err := w.Write(dir, "id.bin"); err != nil { t.Fatal(err) } h := NewSnapshotHolder(10 * time.Second) defer h.Close() if err := h.Reload(filepath.Join(dir, "id.bin")); err != nil { t.Fatal(err) } if err := h.Reload(filepath.Join(dir, "id.bin")); err != nil { t.Fatal(err) } if h.DrainCount() != 1 { t.Errorf("DrainCount after two reloads of same path: got %d want 1", h.DrainCount()) } if _, ok := h.Get("ID-1"); !ok { t.Error("current reader did not surface ID-1") } }