Files
egress-proxies-operator/internal/provider/kvm2/hosts_test.go
T
kacerrandClaude Fable 5.1 2510842396 kvm2: apply the milestone review
Shorter start wait with immediate verdicts for dead or paused domains,
ListByTag without state or address RPCs, Timeout for an expired check
round, no adoption of unmarked domains, reference-counted keyed locks,
dead options removed, HEAD fallback on any non-2xx.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 21:19:50 +00:00

196 lines
5.3 KiB
Go

package kvm2
import (
"context"
"errors"
"sync"
"testing"
"time"
"gitea.home.hrajfrisbee.cz/kacerr/egress-proxies-operator/internal/provider"
)
var (
hostA = provider.KVMHost{Name: "beelink", Address: "beelink:22", User: "virt"}
hostB = provider.KVMHost{Name: "lab2", Address: "lab2:22", User: "virt"}
)
func TestHosts_oneDialPerHostUnderConcurrency(t *testing.T) {
t.Parallel()
shared := newFakeBackend()
d := &stubDialer{backend: shared}
table := newHostTable(d)
const n = 50
var wg sync.WaitGroup
got := make([]backend, n)
errs := make([]error, n)
for i := range n {
wg.Add(1)
go func() {
defer wg.Done()
got[i], errs[i] = table.get(context.Background(), hostA)
}()
}
wg.Wait()
for i := range n {
if errs[i] != nil {
t.Fatalf("get #%d error = %v", i, errs[i])
}
if got[i] != backend(shared) {
t.Fatalf("get #%d returned a different backend", i)
}
}
if d.dials.Load() != 1 {
t.Errorf("dials = %d, want 1", d.dials.Load())
}
}
func TestHosts_redialsAfterDrop(t *testing.T) {
t.Parallel()
first := newFakeBackend()
second := newFakeBackend()
backends := []backend{first, second}
d := &stubDialer{fresh: func() backend {
b := backends[0]
backends = backends[1:]
return b
}}
table := newHostTable(d)
b, err := table.get(context.Background(), hostA)
if err != nil || b != backend(first) {
t.Fatalf("first get = %v, %v, want the first backend", b, err)
}
if b, _ := table.get(context.Background(), hostA); b != backend(first) {
t.Fatal("second get did not reuse the live backend")
}
first.markDead()
b, err = table.get(context.Background(), hostA)
if err != nil || b != backend(second) {
t.Fatalf("get after drop = %v, %v, want the second backend", b, err)
}
if first.closed != 1 {
t.Errorf("dropped backend closed %d times, want 1", first.closed)
}
if d.dials.Load() != 2 {
t.Errorf("dials = %d, want 2", d.dials.Load())
}
}
func TestHosts_dialFreshIsNotCached(t *testing.T) {
t.Parallel()
d := &stubDialer{fresh: func() backend { return newFakeBackend() }}
table := newHostTable(d)
pooled, err := table.get(context.Background(), hostA)
if err != nil {
t.Fatalf("get error = %v", err)
}
fresh, err := table.dialFresh(context.Background(), hostA)
if err != nil {
t.Fatalf("dialFresh error = %v", err)
}
if fresh == pooled {
t.Fatal("dialFresh returned the pooled backend")
}
again, _ := table.get(context.Background(), hostA)
if again != pooled {
t.Error("get after dialFresh did not return the pooled backend")
}
if d.dials.Load() != 2 {
t.Errorf("dials = %d, want 2", d.dials.Load())
}
if fresh.(*fakeBackend).closed != 0 {
t.Error("dialFresh's backend was closed by the table; the caller owns it")
}
}
func TestHosts_waitingForDialEndsWithContext(t *testing.T) {
t.Parallel()
release := make(chan struct{})
d := &stubDialer{block: release}
table := newHostTable(d)
firstDone := make(chan error, 1)
go func() {
_, err := table.get(context.Background(), hostA)
firstDone <- err
}()
waitForDials(t, d, 1)
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
start := time.Now()
_, err := table.get(ctx, hostA)
if time.Since(start) > time.Second {
t.Errorf("second get took %v, want to give up with its ctx", time.Since(start))
}
if !errors.Is(err, provider.ErrTransient) || !errors.Is(err, context.DeadlineExceeded) {
t.Errorf("second get error = %v, want ErrTransient wrapping DeadlineExceeded", err)
}
close(release)
if err := <-firstDone; err != nil {
t.Errorf("first get error = %v, want nil once the dial completes", err)
}
if d.dials.Load() != 1 {
t.Errorf("dials = %d, want 1: the waiter must not dial itself", d.dials.Load())
}
}
func TestHosts_hostsDialIndependently(t *testing.T) {
t.Parallel()
release := make(chan struct{})
defer close(release)
dA := &stubDialer{block: release}
dB := &stubDialer{}
table := newHostTable(perHostDialer{hostA.Name: dA, hostB.Name: dB})
go func() { _, _ = table.get(context.Background(), hostA) }()
waitForDials(t, dA, 1)
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
start := time.Now()
if _, err := table.get(ctx, hostB); err != nil {
t.Fatalf("get(hostB) error = %v while hostA's dial is blocked", err)
}
if time.Since(start) > 500*time.Millisecond {
t.Errorf("get(hostB) took %v, want not to wait for hostA", time.Since(start))
}
}
func TestHosts_dialErrorIsNotCached(t *testing.T) {
t.Parallel()
d := &stubDialer{err: transientf("boom")}
table := newHostTable(d)
if _, err := table.get(context.Background(), hostA); !errors.Is(err, provider.ErrTransient) {
t.Fatalf("get error = %v, want the dial error", err)
}
d.err = nil
if _, err := table.get(context.Background(), hostA); err != nil {
t.Fatalf("get after the dialer recovered error = %v", err)
}
if d.dials.Load() != 2 {
t.Errorf("dials = %d, want 2", d.dials.Load())
}
}
// perHostDialer routes each host to its own stub.
type perHostDialer map[string]*stubDialer
func (p perHostDialer) dial(ctx context.Context, host provider.KVMHost) (backend, error) {
return p[host.Name].dial(ctx, host)
}
func waitForDials(t *testing.T, d *stubDialer, n int32) {
t.Helper()
deadline := time.Now().Add(2 * time.Second)
for d.dials.Load() < n {
if time.Now().After(deadline) {
t.Fatalf("dials = %d, want %d", d.dials.Load(), n)
}
time.Sleep(time.Millisecond)
}
}