2ec3e14c0f
1. Add integration tests for the ContainerLogs API call Each test handle a distinct case of ContainerLogs output. - Muxed stream, when container is started without tty - Single stream, when container is started with tty 2. Add unit test for LogReader suite that tests concurrent logging It checks that there are no race conditions when logging concurrently from multiple goroutines. Co-authored-by: Cory Snider <csnider@mirantis.com> Signed-off-by: Cory Snider <csnider@mirantis.com> Signed-off-by: Paweł Gronowski <pawel.gronowski@docker.com>
190 lines
5 KiB
Go
190 lines
5 KiB
Go
package container // import "github.com/docker/docker/integration/container"
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"io"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/docker/docker/api/types"
|
|
"github.com/docker/docker/daemon/logger/jsonfilelog"
|
|
"github.com/docker/docker/daemon/logger/local"
|
|
"github.com/docker/docker/integration/internal/container"
|
|
"github.com/docker/docker/integration/internal/termtest"
|
|
"github.com/docker/docker/pkg/stdcopy"
|
|
"gotest.tools/v3/assert"
|
|
"gotest.tools/v3/assert/cmp"
|
|
"gotest.tools/v3/poll"
|
|
"gotest.tools/v3/skip"
|
|
)
|
|
|
|
// Regression test for #35370
|
|
// Makes sure that when following we don't get an EOF error when there are no logs
|
|
func TestLogsFollowTailEmpty(t *testing.T) {
|
|
// FIXME(vdemeester) fails on a e2e run on linux...
|
|
skip.If(t, testEnv.IsRemoteDaemon)
|
|
defer setupTest(t)()
|
|
client := testEnv.APIClient()
|
|
ctx := context.Background()
|
|
|
|
id := container.Run(ctx, t, client, container.WithCmd("sleep", "100000"))
|
|
|
|
logs, err := client.ContainerLogs(ctx, id, types.ContainerLogsOptions{ShowStdout: true, Tail: "2"})
|
|
if logs != nil {
|
|
defer logs.Close()
|
|
}
|
|
assert.Check(t, err)
|
|
|
|
_, err = stdcopy.StdCopy(io.Discard, io.Discard, logs)
|
|
assert.Check(t, err)
|
|
}
|
|
|
|
func TestLogs(t *testing.T) {
|
|
drivers := []string{local.Name, jsonfilelog.Name}
|
|
|
|
for _, logDriver := range drivers {
|
|
t.Run("driver "+logDriver, func(t *testing.T) {
|
|
testLogs(t, logDriver)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testLogs(t *testing.T, logDriver string) {
|
|
defer setupTest(t)()
|
|
client := testEnv.APIClient()
|
|
ctx := context.Background()
|
|
|
|
testCases := []struct {
|
|
desc string
|
|
logOps types.ContainerLogsOptions
|
|
expectedOut string
|
|
expectedErr string
|
|
tty bool
|
|
}{
|
|
// TTY, only one output stream
|
|
{
|
|
desc: "tty/stdout and stderr",
|
|
tty: true,
|
|
logOps: types.ContainerLogsOptions{
|
|
ShowStdout: true,
|
|
ShowStderr: true,
|
|
},
|
|
expectedOut: "this is fineaccidents happen",
|
|
},
|
|
{
|
|
desc: "tty/only stdout",
|
|
tty: true,
|
|
logOps: types.ContainerLogsOptions{
|
|
ShowStdout: true,
|
|
ShowStderr: false,
|
|
},
|
|
expectedOut: "this is fineaccidents happen",
|
|
},
|
|
{
|
|
desc: "tty/only stderr",
|
|
tty: true,
|
|
logOps: types.ContainerLogsOptions{
|
|
ShowStdout: false,
|
|
ShowStderr: true,
|
|
},
|
|
expectedOut: "",
|
|
},
|
|
// Without TTY, both stdout and stderr
|
|
{
|
|
desc: "without tty/stdout and stderr",
|
|
tty: false,
|
|
logOps: types.ContainerLogsOptions{
|
|
ShowStdout: true,
|
|
ShowStderr: true,
|
|
},
|
|
expectedOut: "this is fine",
|
|
expectedErr: "accidents happen",
|
|
},
|
|
{
|
|
desc: "without tty/only stdout",
|
|
tty: false,
|
|
logOps: types.ContainerLogsOptions{
|
|
ShowStdout: true,
|
|
ShowStderr: false,
|
|
},
|
|
expectedOut: "this is fine",
|
|
expectedErr: "",
|
|
},
|
|
{
|
|
desc: "without tty/only stderr",
|
|
tty: false,
|
|
logOps: types.ContainerLogsOptions{
|
|
ShowStdout: false,
|
|
ShowStderr: true,
|
|
},
|
|
expectedOut: "",
|
|
expectedErr: "accidents happen",
|
|
},
|
|
}
|
|
|
|
for _, tC := range testCases {
|
|
tC := tC
|
|
t.Run(tC.desc, func(t *testing.T) {
|
|
t.Parallel()
|
|
tty := tC.tty
|
|
id := container.Run(ctx, t, client,
|
|
container.WithCmd("sh", "-c", "echo -n this is fine; echo -n accidents happen >&2"),
|
|
container.WithTty(tty),
|
|
container.WithLogDriver(logDriver),
|
|
)
|
|
defer client.ContainerRemove(ctx, id, types.ContainerRemoveOptions{Force: true})
|
|
|
|
poll.WaitOn(t, container.IsStopped(ctx, client, id), poll.WithDelay(time.Millisecond*100))
|
|
|
|
logs, err := client.ContainerLogs(ctx, id, tC.logOps)
|
|
assert.NilError(t, err)
|
|
defer logs.Close()
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
if tty {
|
|
// TTY, only one output stream
|
|
_, err = io.Copy(&stdout, logs)
|
|
} else {
|
|
_, err = stdcopy.StdCopy(&stdout, &stderr, logs)
|
|
}
|
|
assert.NilError(t, err)
|
|
|
|
stdoutStr := stdout.String()
|
|
|
|
if tty && testEnv.OSType == "windows" {
|
|
stdoutStr = stripEscapeCodes(t, stdoutStr)
|
|
|
|
// Special case for Windows Server 2019
|
|
// Check only that the raw output stream contains strings
|
|
// that were printed to container's stdout and stderr.
|
|
// This is a workaround for the backspace being outputted in an unexpected place
|
|
// which breaks the parsed output: https://github.com/moby/moby/issues/43710
|
|
if strings.Contains(testEnv.DaemonInfo.OperatingSystem, "Windows Server Version 1809") {
|
|
if tC.logOps.ShowStdout {
|
|
assert.Check(t, cmp.Contains(stdout.String(), "this is fine"))
|
|
assert.Check(t, cmp.Contains(stdout.String(), "accidents happen"))
|
|
} else {
|
|
assert.DeepEqual(t, stdoutStr, "")
|
|
}
|
|
return
|
|
}
|
|
|
|
}
|
|
|
|
assert.DeepEqual(t, stdoutStr, tC.expectedOut)
|
|
assert.DeepEqual(t, stderr.String(), tC.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
// This hack strips the escape codes that appear in the Windows TTY output and don't have
|
|
// any effect on the text content.
|
|
// This doesn't handle all escape sequences, only ones that were encountered during testing.
|
|
func stripEscapeCodes(t *testing.T, input string) string {
|
|
t.Logf("Stripping: %q\n", input)
|
|
output, err := termtest.StripANSICommands(input)
|
|
assert.NilError(t, err)
|
|
return output
|
|
}
|