package nixcache import ( "context" "errors" "fmt" "slices" "sync" "testing" "time" ) type useReports struct { mu sync.Mutex calls [][]string caches []string err error flushed chan struct{} } func newUseReports() *useReports { return &useReports{flushed: make(chan struct{}, 16)} } func (r *useReports) record(_ context.Context, cache string, hashes []string) error { r.mu.Lock() slices.Sort(hashes) r.calls = append(r.calls, hashes) r.caches = append(r.caches, cache) r.mu.Unlock() r.flushed <- struct{}{} return r.err } func (r *useReports) all() [][]string { r.mu.Lock() defer r.mu.Unlock() return slices.Clone(r.calls) } func TestUseBatcherFlushesDedupedOnTheInterval(t *testing.T) { reports := newUseReports() b := newUseBatcher("plc-a", reports.record, nil, 10*time.Millisecond, useMaxPending) defer b.Close() b.Add("b") b.Add("a") b.Add("b") select { case <-reports.flushed: case <-time.After(5 * time.Second): t.Fatal("no report within the interval") } if got, want := reports.all(), [][]string{{"a", "b"}}; !slices.EqualFunc(got, want, slices.Equal) { t.Fatalf("reports = %v, want %v", got, want) } if reports.caches[0] != "plc-a" { t.Fatalf("cache = %q, want plc-a", reports.caches[0]) } } func TestUseBatcherBoundsPending(t *testing.T) { reports := newUseReports() b := newUseBatcher("plc-a", reports.record, nil, time.Hour, 3) for i := range 1000 { b.Add(fmt.Sprintf("h%04d", i)) } b.Close() if got, want := reports.all(), [][]string{{"h0000", "h0001", "h0002"}}; !slices.EqualFunc(got, want, slices.Equal) { t.Fatalf("reports = %v, want %v", got, want) } } func TestUseBatcherDropsAFailedReport(t *testing.T) { reports := newUseReports() reports.err = errors.New("mill down") b := newUseBatcher("plc-a", reports.record, nil, time.Hour, useMaxPending) b.Add("a") b.Close() if got := len(reports.all()); got != 1 { t.Fatalf("calls = %d, want 1 and no retry", got) } } func TestUseBatcherAddDoesNotWaitForAFlush(t *testing.T) { block := make(chan struct{}) b := newUseBatcher("plc-a", func(context.Context, string, []string) error { <-block return nil }, nil, 5*time.Millisecond, useMaxPending) defer func() { close(block); b.Close() }() b.Add("a") time.Sleep(50 * time.Millisecond) done := make(chan struct{}) go func() { b.Add("b"); close(done) }() select { case <-done: case <-time.After(5 * time.Second): t.Fatal("Add blocked behind a flush in flight") } } func TestNilUseBatcherIsInert(t *testing.T) { var b *UseBatcher b.Add("a") if err := b.Close(); err != nil { t.Fatal(err) } }