blob: 3c20aa49106f8dbd8093af7aa41c8f4709691c18 [file] [edit]
package container
import (
"context"
"testing"
"time"
"github.com/moby/moby/api/types/container"
libcontainerdtypes "github.com/moby/moby/v2/daemon/internal/libcontainerd/types"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
type mockTask struct {
libcontainerdtypes.Task
pid uint32
}
func (t *mockTask) Pid() uint32 { return t.pid }
func TestStateRunStop(t *testing.T) {
s := &State{}
// Begin another wait with WaitConditionRemoved. It should complete
// within 200 milliseconds.
ctx, cancel := context.WithTimeout(t.Context(), 200*time.Millisecond)
defer cancel()
removalWait := s.Wait(ctx, container.WaitConditionRemoved)
// Full lifecycle two times.
for i := 1; i <= 2; i++ {
// A wait with WaitConditionNotRunning should return
// immediately since the state is now either "created" (on the
// first iteration) or "exited" (on the second iteration). It
// shouldn't take more than 50 milliseconds.
ctx, cancel := context.WithTimeout(t.Context(), 50*time.Millisecond)
defer cancel()
// Expectx exit code to be i-1 since it should be the exit
// code from the previous loop or 0 for the created state.
if status := <-s.Wait(ctx, container.WaitConditionNotRunning); status.ExitCode() != i-1 {
t.Fatalf("ExitCode %v, expected %v, err %q", status.ExitCode(), i-1, status.Err())
}
// A wait with WaitConditionNextExit should block until the
// container has started and exited. It shouldn't take more
// than 100 milliseconds.
ctx, cancel = context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
initialWait := s.Wait(ctx, container.WaitConditionNextExit)
// Set the state to "Running".
s.Lock()
s.SetRunning(nil, &mockTask{pid: uint32(i)}, time.Now())
s.Unlock()
// Assert desired state.
if !s.IsRunning() {
t.Fatal("State not running")
}
if s.Pid != i {
t.Fatalf("Pid %v, expected %v", s.Pid, i)
}
if s.ExitCode != 0 {
t.Fatalf("ExitCode %v, expected 0", s.ExitCode)
}
// Now that it's running, a wait with WaitConditionNotRunning
// should block until we stop the container. It shouldn't take
// more than 100 milliseconds.
ctx, cancel = context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
exitWait := s.Wait(ctx, container.WaitConditionNotRunning)
// Set the state to "Exited".
s.Lock()
s.SetStopped(&ExitStatus{ExitCode: i})
s.Unlock()
// Assert desired state.
if s.IsRunning() {
t.Fatal("State is running")
}
if s.ExitCode != i {
t.Fatalf("ExitCode %v, expected %v", s.ExitCode, i)
}
if s.Pid != 0 {
t.Fatalf("Pid %v, expected 0", s.Pid)
}
// Receive the initialWait result.
if status := <-initialWait; status.ExitCode() != i {
t.Fatalf("ExitCode %v, expected %v, err %q", status.ExitCode(), i, status.Err())
}
// Receive the exitWait result.
if status := <-exitWait; status.ExitCode() != i {
t.Fatalf("ExitCode %v, expected %v, err %q", status.ExitCode(), i, status.Err())
}
}
// Set the state to dead and removed.
s.Lock()
s.Dead = true
s.Unlock()
s.SetRemoved()
// Wait for removed status or timeout.
if status := <-removalWait; status.ExitCode() != 2 {
// Should have the final exit code from the loop.
t.Fatalf("Removal wait exitCode %v, expected %v, err %q", status.ExitCode(), 2, status.Err())
}
}
func TestStateTimeoutWait(t *testing.T) {
s := &State{}
s.Lock()
s.SetRunning(nil, nil, time.Now())
s.Unlock()
// Start a wait with a timeout.
ctx, cancel := context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
waitC := s.Wait(ctx, container.WaitConditionNotRunning)
// It should timeout *before* this 200ms timer does.
select {
case <-time.After(200 * time.Millisecond):
t.Fatal("Stop callback doesn't fire in 200 milliseconds")
case status := <-waitC:
t.Log("Stop callback fired")
// Should be a timeout error.
if status.Err() == nil {
t.Fatal("expected timeout error, got nil")
}
if status.ExitCode() != -1 {
t.Fatalf("expected exit code %v, got %v", -1, status.ExitCode())
}
}
s.Lock()
s.SetStopped(&ExitStatus{ExitCode: 0})
s.Unlock()
// Start another wait with a timeout. This one should return
// immediately.
ctx, cancel = context.WithTimeout(t.Context(), 100*time.Millisecond)
defer cancel()
waitC = s.Wait(ctx, container.WaitConditionNotRunning)
select {
case <-time.After(200 * time.Millisecond):
t.Fatal("Stop callback doesn't fire in 200 milliseconds")
case status := <-waitC:
t.Log("Stop callback fired")
if status.ExitCode() != 0 {
t.Fatalf("expected exit code %v, got %v, err %q", 0, status.ExitCode(), status.Err())
}
}
}
// TestStateWaitReturnsExitStatusAfterRestart verifies that Wait returns the
// correct exit status when a container is restarted immediately after exiting,
// covering a race where Wait could observe the restarted state instead.
//
// Related issue: https://github.com/moby/moby/issues/39352
func TestStateWaitReturnsExitStatusAfterRestart(t *testing.T) {
s := &State{}
s.Lock()
s.SetRunning(nil, nil, time.Now())
s.Unlock()
waitC := s.Wait(t.Context(), container.WaitConditionNotRunning)
want := ExitStatus{ExitCode: 10, ExitedAt: time.Now()}
s.Lock()
s.SetRestarting(&want)
s.Unlock()
s.Lock()
s.SetRunning(nil, nil, time.Now())
s.Unlock()
got := <-waitC
if got.ExitCode() != want.ExitCode {
t.Fatalf("expected exit code %v, got %v", want.ExitCode, got.ExitCode())
}
}
func TestStateStateAndString(t *testing.T) {
now := time.Now().UTC()
tests := []struct {
doc string
state *State
wantState container.ContainerState
wantString string
}{
{
doc: "removal in progress",
state: &State{RemovalInProgress: true},
wantState: container.StateRemoving,
wantString: "Removal In Progress",
},
{
doc: "dead",
state: &State{Dead: true},
wantState: container.StateDead,
wantString: "Dead",
},
{
doc: "restarting",
state: &State{Running: true, Restarting: true, ExitCode: 1, FinishedAt: now.Add(-72 * time.Hour)},
wantState: container.StateRestarting,
wantString: "Restarting (1) 3 days ago",
},
{
doc: "running",
state: &State{Running: true, StartedAt: now.Add(-72 * time.Hour)},
wantState: container.StateRunning,
wantString: "Up 3 days",
},
{
doc: "paused",
state: &State{Running: true, Paused: true, StartedAt: now.Add(-72 * time.Hour)},
wantState: container.StatePaused,
wantString: "Up 3 days (Paused)",
},
{
doc: "created",
state: &State{},
wantState: container.StateCreated,
wantString: "Created",
},
{
doc: "exited",
state: &State{StartedAt: now.Add(-96 * time.Hour), FinishedAt: now.Add(-72 * time.Hour), ExitCode: 137},
wantState: container.StateExited,
wantString: "Exited (137) 3 days ago",
},
{
doc: "started but not finished",
state: &State{StartedAt: now.Add(-72 * time.Hour)},
wantState: container.StateExited,
wantString: "",
},
{
doc: "restarting while not running",
state: &State{Restarting: true, StartedAt: now.Add(-96 * time.Hour), FinishedAt: now.Add(-72 * time.Hour), ExitCode: 1},
wantState: container.StateExited,
wantString: "Exited (1) 3 days ago",
},
{
doc: "paused while not running",
state: &State{Paused: true},
wantState: container.StateCreated,
wantString: "Created",
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
assert.Check(t, is.Equal(tc.state.State(), tc.wantState))
assert.Check(t, is.Equal(tc.state.String(), tc.wantString))
})
}
}