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)
}
}