diff --git a/docker.go b/docker.go index a9f2d583db..5e935c6782 100644 --- a/docker.go +++ b/docker.go @@ -193,23 +193,11 @@ func (c *DockerContainer) Start(ctx context.Context) error { return err } - shortID := c.ID[:12] - if err := c.provider.client.ContainerStart(ctx, c.ID, types.ContainerStartOptions{}); err != nil { return err } defer c.provider.Close() - // if a Wait Strategy has been specified, wait before returning - if c.WaitingFor != nil { - c.logger.Printf("🚧 Waiting for container id %s image: %s", shortID, c.Image) - if err := c.WaitingFor.WaitUntilReady(ctx, c); err != nil { - return err - } - } - - c.isRunning = true - err = c.startedHook(ctx) if err != nil { return err @@ -1027,6 +1015,27 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque } } + return nil + }, + }, + PostStarts: []ContainerHook{ + // first post-start hook is to wait for the container to be ready + func(ctx context.Context, c Container) error { + dockerContainer := c.(*DockerContainer) + + // if a Wait Strategy has been specified, wait before returning + if dockerContainer.WaitingFor != nil { + dockerContainer.logger.Printf( + "🚧 Waiting for container id %s image: %s. Waiting for: %+v", + dockerContainer.ID[:12], dockerContainer.Image, dockerContainer.WaitingFor, + ) + if err := dockerContainer.WaitingFor.WaitUntilReady(ctx, c); err != nil { + return err + } + } + + dockerContainer.isRunning = true + return nil }, }, diff --git a/lifecycle.go b/lifecycle.go index 600adbe4c7..55dd48cf7f 100644 --- a/lifecycle.go +++ b/lifecycle.go @@ -2,6 +2,7 @@ package testcontainers import ( "context" + "io" "strings" "github.com/docker/docker/api/types/container" @@ -130,6 +131,7 @@ func (c *DockerContainer) startingHook(ctx context.Context) error { for _, lifecycleHooks := range c.lifecycleHooks { err := containerHookFn(ctx, lifecycleHooks.PreStarts)(c) if err != nil { + c.printLogs(ctx) return err } } @@ -142,6 +144,7 @@ func (c *DockerContainer) startedHook(ctx context.Context) error { for _, lifecycleHooks := range c.lifecycleHooks { err := containerHookFn(ctx, lifecycleHooks.PostStarts)(c) if err != nil { + c.printLogs(ctx) return err } } @@ -149,6 +152,24 @@ func (c *DockerContainer) startedHook(ctx context.Context) error { return nil } +// printLogs is a helper function that will print the logs of a Docker container +// We are going to use this helper function to inform the user of the logs when an error occurs +func (c *DockerContainer) printLogs(ctx context.Context) { + reader, err := c.Logs(ctx) + if err != nil { + c.logger.Printf("failed accessing container logs: %w\n", err) + return + } + + b, err := io.ReadAll(reader) + if err != nil { + c.logger.Printf("failed reading container logs: %w\n", err) + return + } + + c.logger.Printf("container logs:\n%s", b) +} + // stoppingHook is a hook that will be called before a container is stopped func (c *DockerContainer) stoppingHook(ctx context.Context) error { for _, lifecycleHooks := range c.lifecycleHooks { diff --git a/lifecycle_test.go b/lifecycle_test.go index 2b5db5fcaa..2b3ac8ed29 100644 --- a/lifecycle_test.go +++ b/lifecycle_test.go @@ -1,6 +1,7 @@ package testcontainers import ( + "bufio" "context" "fmt" "strings" @@ -14,6 +15,8 @@ import ( "github.com/docker/go-connections/nat" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go/internal/testcontainersdocker" + "github.com/testcontainers/testcontainers-go/wait" ) func TestPreCreateModifierHook(t *testing.T) { @@ -634,6 +637,76 @@ func TestLifecycleHooks_WithMultipleHooks(t *testing.T) { require.Equal(t, 20, len(dl.data)) } +type linesTestLogger struct { + data []string +} + +func (l *linesTestLogger) Printf(format string, args ...interface{}) { + l.data = append(l.data, fmt.Sprintf(format, args...)) +} + +func TestPrintContainerLogsOnError(t *testing.T) { + ctx := context.Background() + client, err := testcontainersdocker.NewClient(ctx) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + req := ContainerRequest{ + Image: "docker.io/alpine", + Cmd: []string{"echo", "-n", "I am expecting this"}, + WaitingFor: wait.ForLog("I was expecting that").WithStartupTimeout(5 * time.Second), + } + + arrayOfLinesLogger := linesTestLogger{ + data: []string{}, + } + + container, err := GenericContainer(ctx, GenericContainerRequest{ + ProviderType: providerType, + ContainerRequest: req, + Logger: &arrayOfLinesLogger, + Started: true, + }) + // it should fail because the waiting for condition is not met + if err == nil { + t.Fatal(err) + } + terminateContainerOnEnd(t, ctx, container) + + containerLogs, err := container.Logs(ctx) + if err != nil { + t.Fatal(err) + } + defer containerLogs.Close() + + // read container logs line by line, checking that each line is present in the stdout + rd := bufio.NewReader(containerLogs) + for { + line, err := rd.ReadString('\n') + if err != nil { + if err.Error() == "EOF" { + break + } + + t.Fatal("Read Error:", err) + } + + // the last line of the array should contain the line of interest, + // but we are checking all the lines to make sure that is present + found := false + for _, l := range arrayOfLinesLogger.data { + if strings.Contains(l, line) { + found = true + break + } + } + assert.True(t, found, "container log line not found in the output of the logger: %s", line) + } + +} + func lifecycleHooksIsHonouredFn(t *testing.T, ctx context.Context, container Container, prints []string) { require.Equal(t, 20, len(prints))