go-network-configurator/services_unit_test.go

122 lines
3.9 KiB
Go

package netconfig
import (
"context"
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// fastPolling shrinks the readiness poll interval for the duration of a test,
// so the wait loop can be exercised without sleeping for real.
func fastPolling(t *testing.T) {
t.Helper()
original := serviceReadyPollInterval
serviceReadyPollInterval = time.Millisecond
t.Cleanup(func() { serviceReadyPollInterval = original })
}
// countingProbe reports ready only from the nth call onwards, and records how
// many times it was asked.
func countingProbe(readyOn int, err error) (readyProbe, *int) {
calls := 0
probe := func() (bool, error) {
calls++
if readyOn > 0 && calls >= readyOn {
return true, nil
}
return false, err
}
return probe, &calls
}
func TestWaitForServiceReady(t *testing.T) {
t.Run("a service already up returns on the first probe", func(t *testing.T) {
fastPolling(t)
probe, calls := countingProbe(1, nil)
err := waitForServiceReady(context.Background(), "svc", time.Minute, probe)
require.NoError(t, err)
assert.Equal(t, 1, *calls, "a ready service must not be polled twice")
})
t.Run("waits until the service arrives", func(t *testing.T) {
fastPolling(t)
probe, calls := countingProbe(4, errors.New("no bus yet"))
err := waitForServiceReady(context.Background(), "svc", time.Minute, probe)
require.NoError(t, err)
assert.Equal(t, 4, *calls)
})
t.Run("a probe error while booting is not fatal", func(t *testing.T) {
fastPolling(t)
// The first probes fail with the error a host that has not started
// dbus-daemon yet would give; the service then arrives.
probe, _ := countingProbe(3, errors.New("dial unix /run/dbus: no such file"))
assert.NoError(t, waitForServiceReady(context.Background(), "svc", time.Minute, probe))
})
t.Run("times out and reports the last probe error", func(t *testing.T) {
fastPolling(t)
probeErr := errors.New("name has no owner")
probe, calls := countingProbe(0, probeErr)
err := waitForServiceReady(context.Background(), "NetworkManager", 20*time.Millisecond, probe)
require.Error(t, err)
assert.ErrorIs(t, err, probeErr, "the probe error explains the wait better than a deadline does")
assert.ErrorContains(t, err, "timed out waiting for NetworkManager")
assert.Greater(t, *calls, 1, "the probe must be retried before giving up")
})
t.Run("times out and reports the deadline when the probe never errored", func(t *testing.T) {
fastPolling(t)
probe, _ := countingProbe(0, nil)
err := waitForServiceReady(context.Background(), "svc", 20*time.Millisecond, probe)
require.Error(t, err)
assert.ErrorIs(t, err, context.DeadlineExceeded)
})
t.Run("a cancelled context aborts the wait", func(t *testing.T) {
fastPolling(t)
ctx, cancel := context.WithCancel(context.Background())
cancel()
probe, _ := countingProbe(0, nil)
err := waitForServiceReady(ctx, "svc", time.Minute, probe)
require.Error(t, err)
assert.ErrorIs(t, err, context.Canceled)
})
t.Run("zero timeout probes once and does not wait", func(t *testing.T) {
probe, calls := countingProbe(0, nil)
start := time.Now()
err := waitForServiceReady(context.Background(), "svc", 0, probe)
require.Error(t, err)
assert.ErrorContains(t, err, "svc is not ready")
assert.Equal(t, 1, *calls, "a zero timeout must not retry")
assert.Less(t, time.Since(start), serviceReadyPollInterval, "a zero timeout must not sleep")
})
t.Run("zero timeout surfaces the probe error", func(t *testing.T) {
probeErr := errors.New("name has no owner")
probe, _ := countingProbe(0, probeErr)
err := waitForServiceReady(context.Background(), "svc", 0, probe)
require.Error(t, err)
assert.ErrorIs(t, err, probeErr)
})
t.Run("zero timeout still succeeds when the service is up", func(t *testing.T) {
probe, calls := countingProbe(1, nil)
assert.NoError(t, waitForServiceReady(context.Background(), "svc", 0, probe))
assert.Equal(t, 1, *calls)
})
}