DockerCLI/cli/connhelper/commandconn/commandconn_unix_test.go

215 lines
4.8 KiB
Go

//go:build !windows
package commandconn
import (
"context"
"io"
"io/fs"
"testing"
"time"
"github.com/docker/docker/pkg/process"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
// For https://github.com/docker/cli/pull/1014#issuecomment-409308139
func TestEOFWithError(t *testing.T) {
ctx := context.TODO()
cmd := "sh"
args := []string{"-c", "echo hello; echo some error >&2; exit 42"}
c, err := New(ctx, cmd, args...)
assert.NilError(t, err)
b := make([]byte, 32)
n, err := c.Read(b)
assert.Check(t, is.Equal(len("hello\n"), n))
assert.NilError(t, err)
n, err = c.Read(b)
assert.Check(t, is.Equal(0, n))
assert.ErrorContains(t, err, "some error")
assert.ErrorContains(t, err, "42")
}
func TestEOFWithoutError(t *testing.T) {
ctx := context.TODO()
cmd := "sh"
args := []string{"-c", "echo hello; echo some debug log >&2; exit 0"}
c, err := New(ctx, cmd, args...)
assert.NilError(t, err)
b := make([]byte, 32)
n, err := c.Read(b)
assert.Check(t, is.Equal(len("hello\n"), n))
assert.NilError(t, err)
n, err = c.Read(b)
assert.Check(t, is.Equal(0, n))
assert.Check(t, is.Equal(io.EOF, err))
}
func TestCloseRunningCommand(t *testing.T) {
cmd := "sh"
args := []string{"-c", "while true; sleep 1; done"}
done := make(chan struct{})
defer close(done)
go func() {
c, err := New(context.TODO(), cmd, args...)
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
n, err := c.Write([]byte("hello"))
assert.Check(t, is.Equal(len("hello"), n))
assert.NilError(t, err)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
err = cmdConn.Close()
assert.NilError(t, err)
assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid))
done <- struct{}{}
}()
select {
case <-time.After(5 * time.Second):
t.Error("test did not finish in time")
case <-done:
break
}
}
func TestCloseTwice(t *testing.T) {
cmd := "sh"
args := []string{"-c", "echo hello; sleep 1; exit 0"}
done := make(chan struct{})
go func() {
c, err := New(context.TODO(), cmd, args...)
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
b := make([]byte, 32)
n, err := c.Read(b)
assert.Check(t, is.Equal(len("hello\n"), n))
assert.NilError(t, err)
err = cmdConn.Close()
assert.NilError(t, err)
assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid))
err = cmdConn.Close()
assert.NilError(t, err)
assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid))
done <- struct{}{}
}()
select {
case <-time.After(10 * time.Second):
t.Error("test did not finish in time")
case <-done:
break
}
}
func TestEOFTimeout(t *testing.T) {
cmd := "sh"
args := []string{"-c", "sleep 20"}
done := make(chan struct{})
go func() {
c, err := New(context.TODO(), cmd, args...)
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
cmdConn.stdout = mockStdoutEOF{}
b := make([]byte, 32)
n, err := c.Read(b)
assert.Check(t, is.Equal(0, n))
assert.ErrorContains(t, err, "did not exit after EOF")
done <- struct{}{}
}()
// after receiving an EOF, we try to kill the command
// if it doesn't exit after 10s, we throw an error
select {
case <-time.After(12 * time.Second):
t.Error("test did not finish in time")
case <-done:
break
}
}
type mockStdoutEOF struct{}
func (mockStdoutEOF) Read(_ []byte) (int, error) {
return 0, io.EOF
}
func (mockStdoutEOF) Close() error {
return nil
}
func TestCloseWhileWriting(t *testing.T) {
cmd := "sh"
args := []string{"-c", "while true; sleep 1; done"}
c, err := New(context.TODO(), cmd, args...)
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
writeErrC := make(chan error)
go func() {
for {
n, err := c.Write([]byte("hello"))
if err != nil {
writeErrC <- err
return
}
assert.Equal(t, n, len("hello"))
}
}()
err = c.Close()
assert.NilError(t, err)
assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid))
writeErr := <-writeErrC
assert.ErrorContains(t, writeErr, "file already closed")
assert.Check(t, is.ErrorIs(writeErr, fs.ErrClosed))
}
func TestCloseWhileReading(t *testing.T) {
cmd := "sh"
args := []string{"-c", "while true; sleep 1; done"}
c, err := New(context.TODO(), cmd, args...)
assert.NilError(t, err)
cmdConn := c.(*commandConn)
assert.Check(t, process.Alive(cmdConn.cmd.Process.Pid))
readErrC := make(chan error)
go func() {
for {
b := make([]byte, 32)
n, err := c.Read(b)
if err != nil {
readErrC <- err
return
}
assert.Check(t, is.Equal(0, n))
}
}()
err = cmdConn.Close()
assert.NilError(t, err)
assert.Check(t, !process.Alive(cmdConn.cmd.Process.Pid))
readErr := <-readErrC
assert.Check(t, is.ErrorIs(readErr, fs.ErrClosed))
}