package queue
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestNew(t *testing.T) {
|
|
tests := []struct {
|
|
desc string
|
|
opts Options
|
|
want error
|
|
}{
|
|
{"empty options", Options{}, errHardLimit},
|
|
{"zero limit negative quota", Options{SoftQuota: -1}, errHardLimit},
|
|
{"zero limit and quota", Options{SoftQuota: 0}, errHardLimit},
|
|
{"zero limit", Options{SoftQuota: 1, HardLimit: 0}, errHardLimit},
|
|
{"limit less than quota", Options{SoftQuota: 5, HardLimit: 3}, errHardLimit},
|
|
{"negative credit", Options{SoftQuota: 1, HardLimit: 1, BurstCredit: -6}, errBurstCredit},
|
|
{"valid default credit", Options{SoftQuota: 1, HardLimit: 2, BurstCredit: 0}, nil},
|
|
{"valid explicit credit", Options{SoftQuota: 1, HardLimit: 5, BurstCredit: 10}, nil},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.desc, func(t *testing.T) {
|
|
got, err := New(test.opts)
|
|
if err != test.want {
|
|
t.Errorf("New(%+v): got (%+v, %v), want err=%v", test.opts, got, err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type testQueue struct {
|
|
t *testing.T
|
|
*Queue
|
|
}
|
|
|
|
func (q testQueue) mustAdd(item string) {
|
|
q.t.Helper()
|
|
if err := q.Add(item); err != nil {
|
|
q.t.Errorf("Add(%q): unexpected error: %v", item, err)
|
|
}
|
|
}
|
|
|
|
func (q testQueue) mustRemove(want string) {
|
|
q.t.Helper()
|
|
got, ok := q.Remove()
|
|
if !ok {
|
|
q.t.Error("Remove: queue is empty")
|
|
} else if got.(string) != want {
|
|
q.t.Errorf("Remove: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func mustQueue(t *testing.T, opts Options) testQueue {
|
|
t.Helper()
|
|
|
|
q, err := New(opts)
|
|
if err != nil {
|
|
t.Fatalf("New(%+v): unexpected error: %v", opts, err)
|
|
}
|
|
return testQueue{t: t, Queue: q}
|
|
}
|
|
|
|
func TestHardLimit(t *testing.T) {
|
|
q := mustQueue(t, Options{SoftQuota: 1, HardLimit: 1})
|
|
q.mustAdd("foo")
|
|
if err := q.Add("bar"); err != ErrQueueFull {
|
|
t.Errorf("Add: got err=%v, want %v", err, ErrQueueFull)
|
|
}
|
|
}
|
|
|
|
func TestSoftQuota(t *testing.T) {
|
|
q := mustQueue(t, Options{SoftQuota: 1, HardLimit: 4})
|
|
q.mustAdd("foo")
|
|
q.mustAdd("bar")
|
|
if err := q.Add("baz"); err != ErrNoCredit {
|
|
t.Errorf("Add: got err=%v, want %v", err, ErrNoCredit)
|
|
}
|
|
}
|
|
|
|
func TestBurstCredit(t *testing.T) {
|
|
q := mustQueue(t, Options{SoftQuota: 2, HardLimit: 5})
|
|
q.mustAdd("foo")
|
|
q.mustAdd("bar")
|
|
|
|
// We should still have all our initial credit.
|
|
if q.credit < 2 {
|
|
t.Errorf("Wrong credit: got %f, want ≥ 2", q.credit)
|
|
}
|
|
|
|
// Removing an item below soft quota should increase our credit.
|
|
q.mustRemove("foo")
|
|
if q.credit <= 2 {
|
|
t.Errorf("wrong credit: got %f, want > 2", q.credit)
|
|
}
|
|
|
|
// Credit should be capped by the hard limit.
|
|
q.mustRemove("bar")
|
|
q.mustAdd("baz")
|
|
q.mustRemove("baz")
|
|
if cap := float64(q.hardLimit - q.softQuota); q.credit > cap {
|
|
t.Errorf("Wrong credit: got %f, want ≤ %f", q.credit, cap)
|
|
}
|
|
}
|
|
|
|
func TestClose(t *testing.T) {
|
|
q := mustQueue(t, Options{SoftQuota: 2, HardLimit: 10})
|
|
q.mustAdd("alpha")
|
|
q.mustAdd("bravo")
|
|
q.mustAdd("charlie")
|
|
q.Close()
|
|
|
|
// After closing the queue, subsequent writes should fail.
|
|
if err := q.Add("foxtrot"); err == nil {
|
|
t.Error("Add should have failed after Close")
|
|
}
|
|
|
|
// However, the remaining contents of the queue should still work.
|
|
q.mustRemove("alpha")
|
|
q.mustRemove("bravo")
|
|
q.mustRemove("charlie")
|
|
}
|
|
|
|
func TestWait(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
q := mustQueue(t, Options{SoftQuota: 2, HardLimit: 2})
|
|
|
|
// A wait on an empty queue should time out.
|
|
t.Run("WaitTimeout", func(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
|
defer cancel()
|
|
got, err := q.Wait(ctx)
|
|
if err == nil {
|
|
t.Errorf("Wait: got %v, want error", got)
|
|
} else {
|
|
t.Logf("Wait correctly failed: %v", err)
|
|
}
|
|
})
|
|
|
|
// A wait on a non-empty queue should report an item.
|
|
t.Run("WaitNonEmpty", func(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
const input = "figgy pudding"
|
|
q.mustAdd(input)
|
|
|
|
got, err := q.Wait(ctx)
|
|
if err != nil {
|
|
t.Errorf("Wait: unexpected error: %v", err)
|
|
} else if got != input {
|
|
t.Errorf("Wait: got %q, want %q", got, input)
|
|
}
|
|
})
|
|
|
|
// Wait should block until an item arrives.
|
|
t.Run("WaitOnEmpty", func(t *testing.T) {
|
|
const input = "fleet footed kittens"
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
got, err := q.Wait(ctx)
|
|
if err != nil {
|
|
t.Errorf("Wait: unexpected error: %w", err)
|
|
} else if got != input {
|
|
t.Errorf("Wait: got %q, want %q", got, input)
|
|
}
|
|
}()
|
|
|
|
q.mustAdd(input)
|
|
<-done
|
|
})
|
|
|
|
// Closing the queue unblocks a wait.
|
|
t.Run("UnblockOnClose", func(t *testing.T) {
|
|
done := make(chan struct{})
|
|
go func() {
|
|
defer close(done)
|
|
got, err := q.Wait(ctx)
|
|
if err != ErrQueueClosed {
|
|
t.Errorf("Wait: got (%v, %v), want %v", got, err, ErrQueueClosed)
|
|
}
|
|
}()
|
|
|
|
q.Close()
|
|
<-done
|
|
})
|
|
}
|