231 lines
6.6 KiB
Go
231 lines
6.6 KiB
Go
package observe
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func quietLogger() *slog.Logger {
|
|
return slog.New(slog.NewTextHandler(io.Discard, nil))
|
|
}
|
|
|
|
func TestCacheSet_InsertsAndOrders(t *testing.T) {
|
|
m := map[string]string{}
|
|
var order []string
|
|
CacheSet(m, &order, 4, "a", "1")
|
|
CacheSet(m, &order, 4, "b", "2")
|
|
CacheSet(m, &order, 4, "c", "3")
|
|
if got := m["b"]; got != "2" {
|
|
t.Fatalf("m[b] = %q want %q", got, "2")
|
|
}
|
|
if len(order) != 3 || order[0] != "a" || order[2] != "c" {
|
|
t.Fatalf("order = %v, want [a b c]", order)
|
|
}
|
|
}
|
|
|
|
func TestCacheSet_UpdateDoesNotRepeatOrder(t *testing.T) {
|
|
m := map[string]string{}
|
|
var order []string
|
|
CacheSet(m, &order, 4, "a", "1")
|
|
CacheSet(m, &order, 4, "a", "1b")
|
|
if got := m["a"]; got != "1b" {
|
|
t.Fatalf("m[a] = %q want %q", got, "1b")
|
|
}
|
|
if len(order) != 1 || order[0] != "a" {
|
|
t.Fatalf("order = %v, want [a] (repeat sets must not duplicate the slot)", order)
|
|
}
|
|
}
|
|
|
|
func TestCacheSet_EvictsOldestPastMax(t *testing.T) {
|
|
m := map[string]int{}
|
|
var order []string
|
|
for i, k := range []string{"a", "b", "c", "d"} {
|
|
CacheSet(m, &order, 2, k, i)
|
|
}
|
|
if _, ok := m["a"]; ok {
|
|
t.Fatalf("a should have been evicted, got %v", m)
|
|
}
|
|
if _, ok := m["b"]; ok {
|
|
t.Fatalf("b should have been evicted, got %v", m)
|
|
}
|
|
if len(order) != 2 || order[0] != "c" || order[1] != "d" {
|
|
t.Fatalf("order = %v, want [c d]", order)
|
|
}
|
|
}
|
|
|
|
func TestCacheSet_GenericOverTime(t *testing.T) {
|
|
m := map[string]time.Time{}
|
|
var order []string
|
|
now := time.Unix(1700000000, 0)
|
|
CacheSet(m, &order, 4, "k", now)
|
|
if !m["k"].Equal(now) {
|
|
t.Fatalf("time round-trip failed: %v vs %v", m["k"], now)
|
|
}
|
|
}
|
|
|
|
func TestCacheDelete_RemovesKeyAndOrderSlot(t *testing.T) {
|
|
m := map[string]bool{}
|
|
var order []string
|
|
CacheSet(m, &order, 4, "a", true)
|
|
CacheSet(m, &order, 4, "b", true)
|
|
CacheDelete(m, &order, "a")
|
|
if _, ok := m["a"]; ok {
|
|
t.Fatalf("a should be removed: %v", m)
|
|
}
|
|
if len(order) != 1 || order[0] != "b" {
|
|
t.Fatalf("order = %v, want [b]", order)
|
|
}
|
|
}
|
|
|
|
func TestCacheDelete_MissingKeyIsNoop(t *testing.T) {
|
|
m := map[string]bool{"a": true}
|
|
order := []string{"a"}
|
|
CacheDelete(m, &order, "z")
|
|
if !m["a"] || len(order) != 1 || order[0] != "a" {
|
|
t.Fatalf("missing-key delete must not mutate, got m=%v order=%v", m, order)
|
|
}
|
|
}
|
|
|
|
func TestCheckCredentialsOnce_NilProbeMarksChecked(t *testing.T) {
|
|
var checked, disabled bool
|
|
ok, err := CheckCredentialsOnce(context.Background(), nil, &checked, &disabled, quietLogger(), "test")
|
|
if err != nil || !ok {
|
|
t.Fatalf("nil probe: ok=%v err=%v", ok, err)
|
|
}
|
|
if !checked || disabled {
|
|
t.Fatalf("nil probe: checked=%v disabled=%v", checked, disabled)
|
|
}
|
|
}
|
|
|
|
func TestCheckCredentialsOnce_ProbeAvailable(t *testing.T) {
|
|
var checked, disabled bool
|
|
calls := 0
|
|
probe := func(context.Context) (bool, error) { calls++; return true, nil }
|
|
if ok, err := CheckCredentialsOnce(context.Background(), probe, &checked, &disabled, quietLogger(), "test"); err != nil || !ok {
|
|
t.Fatalf("first call: ok=%v err=%v", ok, err)
|
|
}
|
|
if !checked || disabled {
|
|
t.Fatalf("after success: checked=%v disabled=%v", checked, disabled)
|
|
}
|
|
// Second call must NOT re-invoke probe.
|
|
if ok, err := CheckCredentialsOnce(context.Background(), probe, &checked, &disabled, quietLogger(), "test"); err != nil || !ok {
|
|
t.Fatalf("second call: ok=%v err=%v", ok, err)
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("probe should run once, ran %d", calls)
|
|
}
|
|
}
|
|
|
|
func TestCheckCredentialsOnce_ProbeUnavailableDisables(t *testing.T) {
|
|
var checked, disabled bool
|
|
calls := 0
|
|
probe := func(context.Context) (bool, error) { calls++; return false, nil }
|
|
ok, err := CheckCredentialsOnce(context.Background(), probe, &checked, &disabled, quietLogger(), "test")
|
|
if err != nil || ok {
|
|
t.Fatalf("ok=%v err=%v, want (false, nil)", ok, err)
|
|
}
|
|
if !checked || !disabled {
|
|
t.Fatalf("after unavailable: checked=%v disabled=%v", checked, disabled)
|
|
}
|
|
// Subsequent calls must keep reporting (false, nil) — the short-circuit
|
|
// on *checked still has to honour *disabled, otherwise a disabled
|
|
// observer's Poll path silently flips back to "credentials available".
|
|
for i := 0; i < 3; i++ {
|
|
ok, err := CheckCredentialsOnce(context.Background(), probe, &checked, &disabled, quietLogger(), "test")
|
|
if err != nil || ok {
|
|
t.Fatalf("repeat call %d: ok=%v err=%v, want (false, nil)", i, ok, err)
|
|
}
|
|
}
|
|
if calls != 1 {
|
|
t.Fatalf("probe should run exactly once even when disabled, ran %d times", calls)
|
|
}
|
|
}
|
|
|
|
func TestCheckCredentialsOnce_TransientErrorRetries(t *testing.T) {
|
|
var checked, disabled bool
|
|
calls := 0
|
|
probe := func(context.Context) (bool, error) {
|
|
calls++
|
|
if calls == 1 {
|
|
return false, errors.New("transient")
|
|
}
|
|
return true, nil
|
|
}
|
|
if ok, err := CheckCredentialsOnce(context.Background(), probe, &checked, &disabled, quietLogger(), "test"); err != nil || ok {
|
|
t.Fatalf("first call: ok=%v err=%v, want (false,nil)", ok, err)
|
|
}
|
|
if checked || disabled {
|
|
t.Fatalf("transient error must leave state untouched: checked=%v disabled=%v", checked, disabled)
|
|
}
|
|
if ok, err := CheckCredentialsOnce(context.Background(), probe, &checked, &disabled, quietLogger(), "test"); err != nil || !ok {
|
|
t.Fatalf("retry call: ok=%v err=%v, want (true,nil)", ok, err)
|
|
}
|
|
}
|
|
|
|
func TestStartPollLoop_FirstPollImmediateThenTicks(t *testing.T) {
|
|
var mu sync.Mutex
|
|
calls := 0
|
|
poll := func(context.Context) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
calls++
|
|
return nil
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := StartPollLoop(ctx, 10*time.Millisecond, poll, quietLogger(), "test")
|
|
// Wait for at least 2 polls (initial + one tick).
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
mu.Lock()
|
|
n := calls
|
|
mu.Unlock()
|
|
if n >= 2 {
|
|
break
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("done channel not closed after cancel")
|
|
}
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if calls < 2 {
|
|
t.Fatalf("expected at least 2 polls, got %d", calls)
|
|
}
|
|
}
|
|
|
|
func TestStartPollLoop_LogsPollErrorWithoutPanic(t *testing.T) {
|
|
var ran atomic.Int32
|
|
poll := func(context.Context) error {
|
|
ran.Add(1)
|
|
return errors.New("boom")
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
done := StartPollLoop(ctx, 10*time.Millisecond, poll, quietLogger(), "test")
|
|
deadline := time.Now().Add(2 * time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if ran.Load() >= 2 {
|
|
break
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
}
|
|
cancel()
|
|
select {
|
|
case <-done:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("done channel not closed after cancel")
|
|
}
|
|
if ran.Load() < 2 {
|
|
t.Fatalf("expected at least 2 polls under error path, got %d", ran.Load())
|
|
}
|
|
}
|