mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 18:30:09 +00:00
211 lines
4.8 KiB
Go
211 lines
4.8 KiB
Go
package runtime
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestTogglePause_StateCycles verifies a /pause /pause /pause sequence
|
|
// alternates between paused and resumed.
|
|
func TestTogglePause_StateCycles(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := &LocalRuntime{}
|
|
|
|
assertToggle := func(want bool, msg string) {
|
|
t.Helper()
|
|
got, err := r.TogglePause(t.Context())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, want, got, msg)
|
|
}
|
|
|
|
assertToggle(true, "first toggle should pause")
|
|
assertToggle(false, "second toggle should resume")
|
|
assertToggle(true, "third toggle should pause again")
|
|
assertToggle(false, "fourth toggle should resume again")
|
|
}
|
|
|
|
// TestIsPaused_TracksToggle verifies isPaused mirrors the armed state set by
|
|
// TogglePause.
|
|
func TestIsPaused_TracksToggle(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
r := &LocalRuntime{}
|
|
assert.False(t, r.isPaused(), "should not be paused initially")
|
|
|
|
_, _ = r.TogglePause(t.Context())
|
|
assert.True(t, r.isPaused(), "should be paused after first toggle")
|
|
|
|
_, _ = r.TogglePause(t.Context())
|
|
assert.False(t, r.isPaused(), "should not be paused after second toggle")
|
|
}
|
|
|
|
// TestWaitIfPaused_NotPaused returns immediately when the runtime isn't paused.
|
|
func TestWaitIfPaused_NotPaused(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
synctest.Test(t, func(t *testing.T) {
|
|
r := &LocalRuntime{}
|
|
|
|
done := make(chan error, 1)
|
|
go func() { done <- r.waitIfPaused(t.Context()) }()
|
|
|
|
synctest.Wait()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.NoError(t, err)
|
|
default:
|
|
t.Fatal("waitIfPaused should return immediately when not paused")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestWaitIfPaused_BlocksUntilResumed verifies that a goroutine in
|
|
// waitIfPaused stays blocked while paused and wakes up on resume.
|
|
func TestWaitIfPaused_BlocksUntilResumed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
synctest.Test(t, func(t *testing.T) {
|
|
r := &LocalRuntime{}
|
|
_, _ = r.TogglePause(t.Context()) // pause
|
|
|
|
done := make(chan error, 1)
|
|
go func() { done <- r.waitIfPaused(t.Context()) }()
|
|
|
|
synctest.Wait()
|
|
synctest.Sleep(50 * time.Millisecond)
|
|
|
|
select {
|
|
case <-done:
|
|
t.Fatal("waitIfPaused returned before resume")
|
|
default:
|
|
}
|
|
|
|
_, _ = r.TogglePause(t.Context()) // resume
|
|
synctest.Wait()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.NoError(t, err)
|
|
default:
|
|
t.Fatal("waitIfPaused did not unblock after resume")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestWaitIfPaused_ContextCancellation verifies cancelling the context wakes
|
|
// up a goroutine waiting in waitIfPaused, returning the context error.
|
|
func TestWaitIfPaused_ContextCancellation(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
synctest.Test(t, func(t *testing.T) {
|
|
r := &LocalRuntime{}
|
|
_, _ = r.TogglePause(t.Context()) // pause
|
|
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
done := make(chan error, 1)
|
|
go func() { done <- r.waitIfPaused(ctx) }()
|
|
|
|
synctest.Wait()
|
|
synctest.Sleep(50 * time.Millisecond)
|
|
|
|
select {
|
|
case <-done:
|
|
t.Fatal("waitIfPaused returned before cancellation")
|
|
default:
|
|
}
|
|
|
|
cancel()
|
|
synctest.Wait()
|
|
|
|
select {
|
|
case err := <-done:
|
|
require.ErrorIs(t, err, context.Canceled)
|
|
default:
|
|
t.Fatal("waitIfPaused did not unblock after ctx cancellation")
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestWaitIfPaused_BroadcastsToAllWaiters verifies a single resume wakes up
|
|
// every goroutine that was waiting on the same pause.
|
|
func TestWaitIfPaused_BroadcastsToAllWaiters(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
synctest.Test(t, func(t *testing.T) {
|
|
r := &LocalRuntime{}
|
|
_, _ = r.TogglePause(t.Context())
|
|
|
|
const n = 8
|
|
var wg sync.WaitGroup
|
|
for range n {
|
|
wg.Go(func() {
|
|
_ = r.waitIfPaused(t.Context())
|
|
})
|
|
}
|
|
|
|
// All n waiters are durably parked on the pause channel now.
|
|
synctest.Wait()
|
|
|
|
_, _ = r.TogglePause(t.Context()) // single resume must wake all waiters
|
|
|
|
// A partial wake-up leaves waiters parked forever, which synctest
|
|
// reports as a deadlock instead of hanging the suite.
|
|
wg.Wait()
|
|
})
|
|
}
|
|
|
|
// TestTogglePause_RaceFreeUnderConcurrentCallers exercises concurrent
|
|
// TogglePause and waitIfPaused calls. Run with -race to detect data races.
|
|
func TestTogglePause_RaceFreeUnderConcurrentCallers(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
synctest.Test(t, func(t *testing.T) {
|
|
r := &LocalRuntime{}
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
defer cancel()
|
|
|
|
var wg sync.WaitGroup
|
|
const togglers = 4
|
|
const waiters = 4
|
|
|
|
for range togglers {
|
|
wg.Go(func() {
|
|
for range 200 {
|
|
_, _ = r.TogglePause(ctx)
|
|
}
|
|
})
|
|
}
|
|
for range waiters {
|
|
wg.Go(func() {
|
|
for range 200 {
|
|
_ = r.waitIfPaused(ctx)
|
|
}
|
|
})
|
|
}
|
|
|
|
done := make(chan struct{})
|
|
go func() {
|
|
wg.Wait()
|
|
close(done)
|
|
}()
|
|
|
|
// If a waiter is left blocked on a pause that no toggler will flip,
|
|
// cancelling the context unblocks it so wg.Wait() can return.
|
|
select {
|
|
case <-done:
|
|
case <-time.After(2 * time.Second):
|
|
cancel()
|
|
<-done
|
|
}
|
|
})
|
|
}
|