Files
spinifex/cmd/ecs-agent/runtimegate_test.go
T
Julian Sommer c473365f1a refactor: move new test files to external test packages
Tests added since main used in-package test packages, failing the
test-package check. Convert them to <pkg>_test with export_test.go
hooks; the ecs-agent main package keeps an in-package marker.
2026-09-28 10:54:58 +10:00

125 lines
3.4 KiB
Go

//test:in-package — package main cannot be imported by an external test package.
package main
import (
"context"
"errors"
"testing"
ctrruntime "github.com/mulgadc/spinifex/cmd/ecs-agent/runtime"
)
// TestRuntimeGate_DialsUntilTheSocketAnswers is the boot case the agent used to
// lose: the node starts containerd and the agent together, so the first dials
// fail. Resolving once left the agent runtime-less for the life of the process.
func TestRuntimeGate_DialsUntilTheSocketAnswers(t *testing.T) {
g := newRuntimeGate(nil)
if g.available() {
t.Fatal("empty gate reported a runtime")
}
rt := &ctrruntime.FakePuller{}
dials := 0
dial := func() (ctrruntime.Runtime, error) {
dials++
if dials < 3 {
return nil, errors.New("dial containerd: no such file or directory")
}
return rt, nil
}
for range 2 {
g.step(t.Context(), dial)
if g.available() {
t.Fatalf("gate resolved a runtime after %d failed dials", dials)
}
}
g.step(t.Context(), dial)
if g.get() != rt {
t.Fatalf("gate did not pick up the runtime once the socket answered (dials=%d)", dials)
}
}
// TestRuntimeGate_DropsARuntimeThatStopsAnswering covers a containerd restarted
// underneath a live agent: the held runtime stops answering, so the gate clears
// it and the next pass re-dials rather than handing out a dead client forever.
func TestRuntimeGate_DropsARuntimeThatStopsAnswering(t *testing.T) {
dead := &ctrruntime.FakePuller{ListErr: errors.New("connection refused")}
fresh := &ctrruntime.FakePuller{}
g := newRuntimeGate(dead)
dial := func() (ctrruntime.Runtime, error) { return fresh, nil }
g.step(t.Context(), dial)
if g.available() {
t.Fatal("gate kept a runtime whose probe failed")
}
if !dead.Closed {
t.Error("displaced runtime was not closed")
}
g.step(t.Context(), dial)
if g.get() != fresh {
t.Fatal("gate did not re-dial after clearing")
}
}
// TestRuntimeGate_KeepsAHealthyRuntime guards the other direction: a probe that
// succeeds must not churn the runtime, or every pass would drop a working client.
func TestRuntimeGate_KeepsAHealthyRuntime(t *testing.T) {
rt := &ctrruntime.FakePuller{}
g := newRuntimeGate(rt)
dial := func() (ctrruntime.Runtime, error) {
t.Error("dialled while holding a healthy runtime")
return nil, errors.New("unexpected dial")
}
g.step(t.Context(), dial)
if g.get() != rt {
t.Fatal("gate dropped a runtime whose probe succeeded")
}
if rt.Closed {
t.Error("healthy runtime was closed")
}
}
// TestRuntimeGate_ClearIsIdempotent keeps Stop safe to call on an agent that
// never resolved a runtime.
func TestRuntimeGate_ClearIsIdempotent(t *testing.T) {
g := newRuntimeGate(nil)
if g.clear() {
t.Error("clear reported dropping a runtime the gate never held")
}
rt := &ctrruntime.FakePuller{}
g.set(rt)
if !g.clear() {
t.Error("clear did not report dropping the held runtime")
}
if !rt.Closed {
t.Error("cleared runtime was not closed")
}
if g.clear() {
t.Error("second clear reported dropping a runtime")
}
}
// TestRuntimeGate_WatchStopsWithContext keeps the watch loop from outliving the
// agent it belongs to.
func TestRuntimeGate_WatchStopsWithContext(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
g := newRuntimeGate(nil)
done := make(chan struct{})
go func() {
g.watch(ctx, func() (ctrruntime.Runtime, error) { return nil, errors.New("no socket") }, 0)
close(done)
}()
cancel()
<-done
}