From 23e69366e004a5d30b4f349e298f1f7599f68d4f Mon Sep 17 00:00:00 2001 From: Guillaume Date: Mon, 18 Mar 2024 10:34:15 -0400 Subject: [PATCH] Move the remaining tests into a test package This work has been extracted from testcontainers#2202 and is related to testcontainers#2180. See the original PR for the full context and reasoning. This PR in particular moves all the simpler tests in one go, or adds a comment to any remaining tests that would not make sense to move (or could not be moved without exposing a lot of variables). This will help with the documentation, since all examples will now have the module prefixes. --- from_dockerfile_test.go | 40 ++-- generic_test.go | 2 + image_substitutors_test.go | 2 + image_test.go | 2 + logconsumer_internal_test.go | 380 +++++++++++++++++++++++++++++++++ logconsumer_test.go | 395 +++-------------------------------- logger_test.go | 2 + parallel_test.go | 37 ++-- testcontainers_test.go | 2 + testing_test.go | 2 + 10 files changed, 465 insertions(+), 399 deletions(-) create mode 100644 logconsumer_internal_test.go diff --git a/from_dockerfile_test.go b/from_dockerfile_test.go index 7576499a64..583620e7d7 100644 --- a/from_dockerfile_test.go +++ b/from_dockerfile_test.go @@ -1,4 +1,4 @@ -package testcontainers +package testcontainers_test import ( "context" @@ -12,10 +12,12 @@ import ( "github.com/docker/docker/api/types" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go" ) func TestBuildImageFromDockerfile(t *testing.T) { - provider, err := NewDockerProvider() + provider, err := testcontainers.NewDockerProvider() if err != nil { t.Fatal(err) } @@ -25,9 +27,9 @@ func TestBuildImageFromDockerfile(t *testing.T) { ctx := context.Background() - tag, err := provider.BuildImage(ctx, &ContainerRequest{ + tag, err := provider.BuildImage(ctx, &testcontainers.ContainerRequest{ // fromDockerfileIncludingRepo { - FromDockerfile: FromDockerfile{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "testdata", Dockerfile: "echo.Dockerfile", Repo: "test-repo", @@ -53,7 +55,7 @@ func TestBuildImageFromDockerfile(t *testing.T) { } func TestBuildImageFromDockerfile_NoRepo(t *testing.T) { - provider, err := NewDockerProvider() + provider, err := testcontainers.NewDockerProvider() if err != nil { t.Fatal(err) } @@ -63,8 +65,8 @@ func TestBuildImageFromDockerfile_NoRepo(t *testing.T) { ctx := context.Background() - tag, err := provider.BuildImage(ctx, &ContainerRequest{ - FromDockerfile: FromDockerfile{ + tag, err := provider.BuildImage(ctx, &testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "testdata", Dockerfile: "echo.Dockerfile", Repo: "test-repo", @@ -88,7 +90,7 @@ func TestBuildImageFromDockerfile_NoRepo(t *testing.T) { } func TestBuildImageFromDockerfile_NoTag(t *testing.T) { - provider, err := NewDockerProvider() + provider, err := testcontainers.NewDockerProvider() if err != nil { t.Fatal(err) } @@ -98,8 +100,8 @@ func TestBuildImageFromDockerfile_NoTag(t *testing.T) { ctx := context.Background() - tag, err := provider.BuildImage(ctx, &ContainerRequest{ - FromDockerfile: FromDockerfile{ + tag, err := provider.BuildImage(ctx, &testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "testdata", Dockerfile: "echo.Dockerfile", Tag: "test-tag", @@ -126,9 +128,9 @@ func TestBuildImageFromDockerfile_Target(t *testing.T) { // there are thre targets: target0, target1 and target2. for i := 0; i < 3; i++ { ctx := context.Background() - c, err := GenericContainer(ctx, GenericContainerRequest{ - ContainerRequest: ContainerRequest{ - FromDockerfile: FromDockerfile{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "testdata", Dockerfile: "target.Dockerfile", PrintBuildLog: true, @@ -160,9 +162,9 @@ func ExampleGenericContainer_buildFromDockerfile() { ctx := context.Background() // buildFromDockerfileWithModifier { - c, err := GenericContainer(ctx, GenericContainerRequest{ - ContainerRequest: ContainerRequest{ - FromDockerfile: FromDockerfile{ + c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "testdata", Dockerfile: "target.Dockerfile", PrintBuildLog: true, @@ -199,9 +201,9 @@ func TestBuildImageFromDockerfile_TargetDoesNotExist(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() - _, err := GenericContainer(ctx, GenericContainerRequest{ - ContainerRequest: ContainerRequest{ - FromDockerfile: FromDockerfile{ + _, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "testdata", Dockerfile: "target.Dockerfile", PrintBuildLog: true, diff --git a/generic_test.go b/generic_test.go index 72688876ec..d6834b83f9 100644 --- a/generic_test.go +++ b/generic_test.go @@ -1,3 +1,5 @@ +// This test is testing very internal logic that should not be exported away from this package. We'll +// leave it in the main testcontainers package. Do not use for user facing examples. package testcontainers import ( diff --git a/image_substitutors_test.go b/image_substitutors_test.go index 64e3b95d2d..ef1ee232ab 100644 --- a/image_substitutors_test.go +++ b/image_substitutors_test.go @@ -1,3 +1,5 @@ +// This test is testing very internal logic that should not be exported away from this package. We'll +// leave it in the main testcontainers package. Do not use for user facing examples. package testcontainers import ( diff --git a/image_test.go b/image_test.go index 17595b6590..385256c5f1 100644 --- a/image_test.go +++ b/image_test.go @@ -1,3 +1,5 @@ +// This test is testing very internal logic that should not be exported away from this package. We'll +// leave it in the main testcontainers package. Do not use for user facing examples. package testcontainers import ( diff --git a/logconsumer_internal_test.go b/logconsumer_internal_test.go new file mode 100644 index 0000000000..8e6b5e3de1 --- /dev/null +++ b/logconsumer_internal_test.go @@ -0,0 +1,380 @@ +// This test is testing very internal logic that should not be exported away from this package. We'll +// leave it in the main testcontainers package. Do not use for user facing examples. +package testcontainers + +import ( + "bytes" + "context" + "errors" + "fmt" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/docker/docker/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/testcontainers/testcontainers-go/internal/config" + "github.com/testcontainers/testcontainers-go/wait" +) + +const lastMessage = "DONE" + +type TestLogConsumer struct { + Msgs []string + Done chan bool + + // Accepted provides a blocking way of ensuring the logs messages have been consumed. + // This allows for proper synchronization during Test_StartStop in particular. + // Please see func devNullAcceptorChan if you're not interested in this synchronization. + Accepted chan string +} + +func (g *TestLogConsumer) Accept(l Log) { + s := string(l.Content) + if s == fmt.Sprintf("echo %s\n", lastMessage) { + g.Done <- true + return + } + g.Accepted <- s + g.Msgs = append(g.Msgs, s) +} + +// devNullAcceptorChan returns string channel that essentially sends all strings to dev null +func devNullAcceptorChan() chan string { + c := make(chan string) + go func(c <-chan string) { + for range c { + // do nothing, just pull off channel + } + }(c) + return c +} + +func TestContainerLogWithErrClosed(t *testing.T) { + if os.Getenv("XDG_RUNTIME_DIR") != "" { + t.Skip("Skipping as flaky on GitHub Actions, Please see https://github.com/testcontainers/testcontainers-go/issues/1924") + } + + t.Cleanup(func() { + config.Reset() + }) + + if providerType == ProviderPodman { + t.Skip("Docker-in-Docker does not work with rootless Podman") + } + // First spin up a docker-in-docker container, then spin up an inner container within that dind container + // Logs are being read from the inner container via the dind container's tcp port, which can be briefly + // closed to test behaviour in connection-closed situations. + ctx := context.Background() + + dind, err := GenericContainer(ctx, GenericContainerRequest{ + Started: true, + ContainerRequest: ContainerRequest{ + Image: "docker.io/docker:dind", + ExposedPorts: []string{"2375/tcp"}, + Env: map[string]string{"DOCKER_TLS_CERTDIR": ""}, + WaitingFor: wait.ForListeningPort("2375/tcp"), + Privileged: true, + }, + }) + + require.NoError(t, err) + terminateContainerOnEnd(t, ctx, dind) + + var remoteDocker string + + ctxEndpoint, cancel := context.WithTimeout(ctx, 5*time.Second) + defer cancel() + + // todo: remove this temporary fix (test is flaky). + for { + remoteDocker, err = dind.Endpoint(ctxEndpoint, "2375/tcp") + if err == nil { + break + } + if errors.Is(err, context.DeadlineExceeded) { + break + } + time.Sleep(100 * time.Microsecond) + t.Log("retrying get endpoint") + } + if err != nil { + t.Fatal("get endpoint:", err) + } + + opts := []client.Opt{client.WithHost(remoteDocker), client.WithAPIVersionNegotiation()} + + client, err := NewDockerClientWithOpts(ctx, opts...) + if err != nil { + t.Fatal(err) + } + defer client.Close() + + provider := &DockerProvider{ + client: client, + config: ReadConfig(), + DockerProviderOptions: &DockerProviderOptions{ + GenericProviderOptions: &GenericProviderOptions{ + Logger: TestLogger(t), + }, + }, + } + + consumer := TestLogConsumer{ + Msgs: []string{}, + Done: make(chan bool), + Accepted: devNullAcceptorChan(), + } + + nginx, err := provider.CreateContainer(ctx, ContainerRequest{ + Image: "nginx", + ExposedPorts: []string{"80/tcp"}, + LogConsumerCfg: &LogConsumerConfig{ + Consumers: []LogConsumer{&consumer}, + }, + }) + if err != nil { + t.Fatal(err) + } + if err := nginx.Start(ctx); err != nil { + t.Fatal(err) + } + terminateContainerOnEnd(t, ctx, nginx) + + port, err := nginx.MappedPort(ctx, "80/tcp") + if err != nil { + t.Fatal(err) + } + + // Gather the initial container logs + time.Sleep(time.Second * 1) + existingLogs := len(consumer.Msgs) + + hitNginx := func() { + i, _, err := dind.Exec(ctx, []string{"wget", "--spider", "localhost:" + port.Port()}) + if err != nil || i > 0 { + t.Fatalf("Can't make request to nginx container from dind container") + } + } + + hitNginx() + time.Sleep(time.Second * 1) + if len(consumer.Msgs)-existingLogs != 1 { + t.Fatalf("logConsumer should have 1 new log message, instead has: %v", consumer.Msgs[existingLogs:]) + } + existingLogs = len(consumer.Msgs) + + iptableArgs := []string{ + "INPUT", "-p", "tcp", "--dport", "2375", + "-j", "REJECT", "--reject-with", "tcp-reset", + } + // Simulate a transient closed connection to the docker daemon + i, _, err := dind.Exec(ctx, append([]string{"iptables", "-A"}, iptableArgs...)) + if err != nil || i > 0 { + t.Fatalf("Failed to close connection to dind daemon: i(%d), err %v", i, err) + } + i, _, err = dind.Exec(ctx, append([]string{"iptables", "-D"}, iptableArgs...)) + if err != nil || i > 0 { + t.Fatalf("Failed to re-open connection to dind daemon: i(%d), err %v", i, err) + } + time.Sleep(time.Second * 3) + + hitNginx() + hitNginx() + time.Sleep(time.Second * 1) + if len(consumer.Msgs)-existingLogs != 2 { + t.Fatalf( + "LogConsumer should have 2 new log messages after detecting closed connection and"+ + " re-requesting logs. Instead has:\n%s", consumer.Msgs[existingLogs:], + ) + } +} + +func Test_StartLogProductionStillStartsWithTooLowTimeout(t *testing.T) { + ctx := context.Background() + + g := TestLogConsumer{ + Msgs: []string{}, + Done: make(chan bool), + Accepted: devNullAcceptorChan(), + } + + req := ContainerRequest{ + FromDockerfile: FromDockerfile{ + Context: "./testdata/", + Dockerfile: "echoserver.Dockerfile", + }, + ExposedPorts: []string{"8080/tcp"}, + WaitingFor: wait.ForLog("ready"), + LogConsumerCfg: &LogConsumerConfig{ + Opts: []LogProductionOption{WithLogProductionTimeout(4 * time.Second)}, + Consumers: []LogConsumer{&g}, + }, + } + + gReq := GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + c, err := GenericContainer(ctx, gReq) + require.NoError(t, err) + terminateContainerOnEnd(t, ctx, c) +} + +func Test_StartLogProductionStillStartsWithTooHighTimeout(t *testing.T) { + ctx := context.Background() + + g := TestLogConsumer{ + Msgs: []string{}, + Done: make(chan bool), + Accepted: devNullAcceptorChan(), + } + + req := ContainerRequest{ + FromDockerfile: FromDockerfile{ + Context: "./testdata/", + Dockerfile: "echoserver.Dockerfile", + }, + ExposedPorts: []string{"8080/tcp"}, + WaitingFor: wait.ForLog("ready"), + LogConsumerCfg: &LogConsumerConfig{ + Opts: []LogProductionOption{WithLogProductionTimeout(61 * time.Second)}, + Consumers: []LogConsumer{&g}, + }, + } + + gReq := GenericContainerRequest{ + ContainerRequest: req, + Started: true, + } + + c, err := GenericContainer(ctx, gReq) + require.NoError(t, err) + require.NotNil(t, c) + + // because the log production timeout is too high, the container should have already been terminated + // so no need to terminate it again with "terminateContainerOnEnd(t, ctx, c)" + dc := c.(*DockerContainer) + require.NoError(t, dc.stopLogProduction()) + + terminateContainerOnEnd(t, ctx, c) +} + +func Test_MultiContainerLogConsumer_CancelledContext(t *testing.T) { + // Redirect stderr to a buffer + oldStderr := os.Stderr + r, w, _ := os.Pipe() + os.Stderr = w + + // Context with cancellation functionality for simulating user interruption + ctx, cancel := context.WithCancel(context.Background()) + + first := TestLogConsumer{ + Msgs: []string{}, + Done: make(chan bool), + Accepted: devNullAcceptorChan(), + } + + containerReq1 := ContainerRequest{ + FromDockerfile: FromDockerfile{ + Context: "./testdata/", + Dockerfile: "echoserver.Dockerfile", + }, + ExposedPorts: []string{"8080/tcp"}, + WaitingFor: wait.ForLog("ready"), + LogConsumerCfg: &LogConsumerConfig{ + Consumers: []LogConsumer{&first}, + }, + } + + genericReq1 := GenericContainerRequest{ + ContainerRequest: containerReq1, + Started: true, + } + + c, err := GenericContainer(ctx, genericReq1) + require.NoError(t, err) + + ep1, err := c.Endpoint(ctx, "http") + require.NoError(t, err) + + _, err = http.Get(ep1 + "/stdout?echo=hello1") + require.NoError(t, err) + + _, err = http.Get(ep1 + "/stdout?echo=there1") + require.NoError(t, err) + + second := TestLogConsumer{ + Msgs: []string{}, + Done: make(chan bool), + Accepted: devNullAcceptorChan(), + } + + containerReq2 := ContainerRequest{ + FromDockerfile: FromDockerfile{ + Context: "./testdata/", + Dockerfile: "echoserver.Dockerfile", + }, + ExposedPorts: []string{"8080/tcp"}, + WaitingFor: wait.ForLog("ready"), + LogConsumerCfg: &LogConsumerConfig{ + Consumers: []LogConsumer{&second}, + }, + } + + genericReq2 := GenericContainerRequest{ + ContainerRequest: containerReq2, + Started: true, + } + + c2, err := GenericContainer(ctx, genericReq2) + require.NoError(t, err) + + ep2, err := c2.Endpoint(ctx, "http") + require.NoError(t, err) + + _, err = http.Get(ep2 + "/stdout?echo=hello2") + require.NoError(t, err) + + _, err = http.Get(ep2 + "/stdout?echo=there2") + require.NoError(t, err) + + // Handling the termination of the containers + defer func() { + shutdownCtx, shutdownCancel := context.WithTimeout( + context.Background(), 60*time.Second, + ) + defer shutdownCancel() + _ = c.Terminate(shutdownCtx) + _ = c2.Terminate(shutdownCtx) + }() + + // Deliberately calling context cancel + cancel() + + // We check log size due to context cancellation causing + // varying message counts, leading to test failure. + assert.GreaterOrEqual(t, len(first.Msgs), 2) + assert.GreaterOrEqual(t, len(second.Msgs), 2) + + // Restore stderr + w.Close() + os.Stderr = oldStderr + + // Read the stderr output from the buffer + var buf bytes.Buffer + _, _ = buf.ReadFrom(r) + + // Check the stderr message + actual := buf.String() + + // The context cancel shouldn't cause the system to throw a + // logStoppedForOutOfSyncMessage, as it hangs the system with + // the multiple containers. + assert.False(t, strings.Contains(actual, logStoppedForOutOfSyncMessage)) +} diff --git a/logconsumer_test.go b/logconsumer_test.go index 9c9b25fa09..1d98e2d3cb 100644 --- a/logconsumer_test.go +++ b/logconsumer_test.go @@ -1,22 +1,18 @@ -package testcontainers +package testcontainers_test import ( - "bytes" "context" - "errors" "fmt" "io" "net/http" - "os" "strings" "testing" "time" - "github.com/docker/docker/client" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/testcontainers/testcontainers-go/internal/config" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) @@ -32,7 +28,7 @@ type TestLogConsumer struct { Accepted chan string } -func (g *TestLogConsumer) Accept(l Log) { +func (g *TestLogConsumer) Accept(l testcontainers.Log) { s := string(l.Content) if s == fmt.Sprintf("echo %s\n", lastMessage) { g.Done <- true @@ -62,24 +58,24 @@ func Test_LogConsumerGetsCalled(t *testing.T) { Accepted: devNullAcceptorChan(), } - req := ContainerRequest{ - FromDockerfile: FromDockerfile{ + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "./testdata/", Dockerfile: "echoserver.Dockerfile", }, ExposedPorts: []string{"8080/tcp"}, WaitingFor: wait.ForLog("ready"), - LogConsumerCfg: &LogConsumerConfig{ - Consumers: []LogConsumer{&g}, + LogConsumerCfg: &testcontainers.LogConsumerConfig{ + Consumers: []testcontainers.LogConsumer{&g}, }, } - gReq := GenericContainerRequest{ + gReq := testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, } - c, err := GenericContainer(ctx, gReq) + c, err := testcontainers.GenericContainer(ctx, gReq) require.NoError(t, err) ep, err := c.Endpoint(ctx, "http") @@ -110,7 +106,7 @@ type TestLogTypeConsumer struct { Ack chan bool } -func (t *TestLogTypeConsumer) Accept(l Log) { +func (t *TestLogTypeConsumer) Accept(l testcontainers.Log) { if string(l.Content) == fmt.Sprintf("echo %s\n", lastMessage) { t.Ack <- true return @@ -127,24 +123,24 @@ func Test_ShouldRecognizeLogTypes(t *testing.T) { Ack: make(chan bool), } - req := ContainerRequest{ - FromDockerfile: FromDockerfile{ + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "./testdata/", Dockerfile: "echoserver.Dockerfile", }, ExposedPorts: []string{"8080/tcp"}, WaitingFor: wait.ForLog("ready"), - LogConsumerCfg: &LogConsumerConfig{ - Consumers: []LogConsumer{&g}, + LogConsumerCfg: &testcontainers.LogConsumerConfig{ + Consumers: []testcontainers.LogConsumer{&g}, }, } - gReq := GenericContainerRequest{ + gReq := testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, } - c, err := GenericContainer(ctx, gReq) + c, err := testcontainers.GenericContainer(ctx, gReq) require.NoError(t, err) terminateContainerOnEnd(t, ctx, c) @@ -163,8 +159,8 @@ func Test_ShouldRecognizeLogTypes(t *testing.T) { <-g.Ack assert.Equal(t, map[string]string{ - StdoutLog: "echo this-is-stdout\n", - StderrLog: "echo this-is-stderr\n", + testcontainers.StdoutLog: "echo this-is-stdout\n", + testcontainers.StderrLog: "echo this-is-stderr\n", }, g.LogTypes) } @@ -182,24 +178,24 @@ func Test_MultipleLogConsumers(t *testing.T) { Accepted: devNullAcceptorChan(), } - req := ContainerRequest{ - FromDockerfile: FromDockerfile{ + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "./testdata/", Dockerfile: "echoserver.Dockerfile", }, ExposedPorts: []string{"8080/tcp"}, WaitingFor: wait.ForLog("ready"), - LogConsumerCfg: &LogConsumerConfig{ - Consumers: []LogConsumer{&first, &second}, + LogConsumerCfg: &testcontainers.LogConsumerConfig{ + Consumers: []testcontainers.LogConsumer{&first, &second}, }, } - gReq := GenericContainerRequest{ + gReq := testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, } - c, err := GenericContainer(ctx, gReq) + c, err := testcontainers.GenericContainer(ctx, gReq) require.NoError(t, err) ep, err := c.Endpoint(ctx, "http") @@ -219,154 +215,14 @@ func Test_MultipleLogConsumers(t *testing.T) { require.NoError(t, c.Terminate(ctx)) } -func TestContainerLogWithErrClosed(t *testing.T) { - if os.Getenv("XDG_RUNTIME_DIR") != "" { - t.Skip("Skipping as flaky on GitHub Actions, Please see https://github.com/testcontainers/testcontainers-go/issues/1924") - } - - t.Cleanup(func() { - config.Reset() - }) - - if providerType == ProviderPodman { - t.Skip("Docker-in-Docker does not work with rootless Podman") - } - // First spin up a docker-in-docker container, then spin up an inner container within that dind container - // Logs are being read from the inner container via the dind container's tcp port, which can be briefly - // closed to test behaviour in connection-closed situations. - ctx := context.Background() - - dind, err := GenericContainer(ctx, GenericContainerRequest{ - Started: true, - ContainerRequest: ContainerRequest{ - Image: "docker.io/docker:dind", - ExposedPorts: []string{"2375/tcp"}, - Env: map[string]string{"DOCKER_TLS_CERTDIR": ""}, - WaitingFor: wait.ForListeningPort("2375/tcp"), - Privileged: true, - }, - }) - - require.NoError(t, err) - terminateContainerOnEnd(t, ctx, dind) - - var remoteDocker string - - ctxEndpoint, cancel := context.WithTimeout(ctx, 5*time.Second) - defer cancel() - - // todo: remove this temporary fix (test is flaky). - for { - remoteDocker, err = dind.Endpoint(ctxEndpoint, "2375/tcp") - if err == nil { - break - } - if errors.Is(err, context.DeadlineExceeded) { - break - } - time.Sleep(100 * time.Microsecond) - t.Log("retrying get endpoint") - } - if err != nil { - t.Fatal("get endpoint:", err) - } - - opts := []client.Opt{client.WithHost(remoteDocker), client.WithAPIVersionNegotiation()} - - client, err := NewDockerClientWithOpts(ctx, opts...) - if err != nil { - t.Fatal(err) - } - defer client.Close() - - provider := &DockerProvider{ - client: client, - config: ReadConfig(), - DockerProviderOptions: &DockerProviderOptions{ - GenericProviderOptions: &GenericProviderOptions{ - Logger: TestLogger(t), - }, - }, - } - - consumer := TestLogConsumer{ - Msgs: []string{}, - Done: make(chan bool), - Accepted: devNullAcceptorChan(), - } - - nginx, err := provider.CreateContainer(ctx, ContainerRequest{ - Image: "nginx", - ExposedPorts: []string{"80/tcp"}, - LogConsumerCfg: &LogConsumerConfig{ - Consumers: []LogConsumer{&consumer}, - }, - }) - if err != nil { - t.Fatal(err) - } - if err := nginx.Start(ctx); err != nil { - t.Fatal(err) - } - terminateContainerOnEnd(t, ctx, nginx) - - port, err := nginx.MappedPort(ctx, "80/tcp") - if err != nil { - t.Fatal(err) - } - - // Gather the initial container logs - time.Sleep(time.Second * 1) - existingLogs := len(consumer.Msgs) - - hitNginx := func() { - i, _, err := dind.Exec(ctx, []string{"wget", "--spider", "localhost:" + port.Port()}) - if err != nil || i > 0 { - t.Fatalf("Can't make request to nginx container from dind container") - } - } - - hitNginx() - time.Sleep(time.Second * 1) - if len(consumer.Msgs)-existingLogs != 1 { - t.Fatalf("logConsumer should have 1 new log message, instead has: %v", consumer.Msgs[existingLogs:]) - } - existingLogs = len(consumer.Msgs) - - iptableArgs := []string{ - "INPUT", "-p", "tcp", "--dport", "2375", - "-j", "REJECT", "--reject-with", "tcp-reset", - } - // Simulate a transient closed connection to the docker daemon - i, _, err := dind.Exec(ctx, append([]string{"iptables", "-A"}, iptableArgs...)) - if err != nil || i > 0 { - t.Fatalf("Failed to close connection to dind daemon: i(%d), err %v", i, err) - } - i, _, err = dind.Exec(ctx, append([]string{"iptables", "-D"}, iptableArgs...)) - if err != nil || i > 0 { - t.Fatalf("Failed to re-open connection to dind daemon: i(%d), err %v", i, err) - } - time.Sleep(time.Second * 3) - - hitNginx() - hitNginx() - time.Sleep(time.Second * 1) - if len(consumer.Msgs)-existingLogs != 2 { - t.Fatalf( - "LogConsumer should have 2 new log messages after detecting closed connection and"+ - " re-requesting logs. Instead has:\n%s", consumer.Msgs[existingLogs:], - ) - } -} - func TestContainerLogsShouldBeWithoutStreamHeader(t *testing.T) { ctx := context.Background() - req := ContainerRequest{ + req := testcontainers.ContainerRequest{ Image: "alpine:latest", Cmd: []string{"sh", "-c", "id -u"}, WaitingFor: wait.ForExit(), } - container, err := GenericContainer(ctx, GenericContainerRequest{ + container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, }) @@ -395,26 +251,26 @@ func TestContainerLogsEnableAtStart(t *testing.T) { } // logConsumersAtRequest { - req := ContainerRequest{ - FromDockerfile: FromDockerfile{ + req := testcontainers.ContainerRequest{ + FromDockerfile: testcontainers.FromDockerfile{ Context: "./testdata/", Dockerfile: "echoserver.Dockerfile", }, ExposedPorts: []string{"8080/tcp"}, WaitingFor: wait.ForLog("ready"), - LogConsumerCfg: &LogConsumerConfig{ - Opts: []LogProductionOption{WithLogProductionTimeout(10 * time.Second)}, - Consumers: []LogConsumer{&g}, + LogConsumerCfg: &testcontainers.LogConsumerConfig{ + Opts: []testcontainers.LogProductionOption{testcontainers.WithLogProductionTimeout(10 * time.Second)}, + Consumers: []testcontainers.LogConsumer{&g}, }, } // } - gReq := GenericContainerRequest{ + gReq := testcontainers.GenericContainerRequest{ ContainerRequest: req, Started: true, } - c, err := GenericContainer(ctx, gReq) + c, err := testcontainers.GenericContainer(ctx, gReq) require.NoError(t, err) ep, err := c.Endpoint(ctx, "http") @@ -438,188 +294,3 @@ func TestContainerLogsEnableAtStart(t *testing.T) { terminateContainerOnEnd(t, ctx, c) } - -func Test_StartLogProductionStillStartsWithTooLowTimeout(t *testing.T) { - ctx := context.Background() - - g := TestLogConsumer{ - Msgs: []string{}, - Done: make(chan bool), - Accepted: devNullAcceptorChan(), - } - - req := ContainerRequest{ - FromDockerfile: FromDockerfile{ - Context: "./testdata/", - Dockerfile: "echoserver.Dockerfile", - }, - ExposedPorts: []string{"8080/tcp"}, - WaitingFor: wait.ForLog("ready"), - LogConsumerCfg: &LogConsumerConfig{ - Opts: []LogProductionOption{WithLogProductionTimeout(4 * time.Second)}, - Consumers: []LogConsumer{&g}, - }, - } - - gReq := GenericContainerRequest{ - ContainerRequest: req, - Started: true, - } - - c, err := GenericContainer(ctx, gReq) - require.NoError(t, err) - terminateContainerOnEnd(t, ctx, c) -} - -func Test_StartLogProductionStillStartsWithTooHighTimeout(t *testing.T) { - ctx := context.Background() - - g := TestLogConsumer{ - Msgs: []string{}, - Done: make(chan bool), - Accepted: devNullAcceptorChan(), - } - - req := ContainerRequest{ - FromDockerfile: FromDockerfile{ - Context: "./testdata/", - Dockerfile: "echoserver.Dockerfile", - }, - ExposedPorts: []string{"8080/tcp"}, - WaitingFor: wait.ForLog("ready"), - LogConsumerCfg: &LogConsumerConfig{ - Opts: []LogProductionOption{WithLogProductionTimeout(61 * time.Second)}, - Consumers: []LogConsumer{&g}, - }, - } - - gReq := GenericContainerRequest{ - ContainerRequest: req, - Started: true, - } - - c, err := GenericContainer(ctx, gReq) - require.NoError(t, err) - require.NotNil(t, c) - - // because the log production timeout is too high, the container should have already been terminated - // so no need to terminate it again with "terminateContainerOnEnd(t, ctx, c)" - dc := c.(*DockerContainer) - require.NoError(t, dc.stopLogProduction()) - - terminateContainerOnEnd(t, ctx, c) -} - -func Test_MultiContainerLogConsumer_CancelledContext(t *testing.T) { - // Redirect stderr to a buffer - oldStderr := os.Stderr - r, w, _ := os.Pipe() - os.Stderr = w - - // Context with cancellation functionality for simulating user interruption - ctx, cancel := context.WithCancel(context.Background()) - - first := TestLogConsumer{ - Msgs: []string{}, - Done: make(chan bool), - Accepted: devNullAcceptorChan(), - } - - containerReq1 := ContainerRequest{ - FromDockerfile: FromDockerfile{ - Context: "./testdata/", - Dockerfile: "echoserver.Dockerfile", - }, - ExposedPorts: []string{"8080/tcp"}, - WaitingFor: wait.ForLog("ready"), - LogConsumerCfg: &LogConsumerConfig{ - Consumers: []LogConsumer{&first}, - }, - } - - genericReq1 := GenericContainerRequest{ - ContainerRequest: containerReq1, - Started: true, - } - - c, err := GenericContainer(ctx, genericReq1) - require.NoError(t, err) - - ep1, err := c.Endpoint(ctx, "http") - require.NoError(t, err) - - _, err = http.Get(ep1 + "/stdout?echo=hello1") - require.NoError(t, err) - - _, err = http.Get(ep1 + "/stdout?echo=there1") - require.NoError(t, err) - - second := TestLogConsumer{ - Msgs: []string{}, - Done: make(chan bool), - Accepted: devNullAcceptorChan(), - } - - containerReq2 := ContainerRequest{ - FromDockerfile: FromDockerfile{ - Context: "./testdata/", - Dockerfile: "echoserver.Dockerfile", - }, - ExposedPorts: []string{"8080/tcp"}, - WaitingFor: wait.ForLog("ready"), - LogConsumerCfg: &LogConsumerConfig{ - Consumers: []LogConsumer{&second}, - }, - } - - genericReq2 := GenericContainerRequest{ - ContainerRequest: containerReq2, - Started: true, - } - - c2, err := GenericContainer(ctx, genericReq2) - require.NoError(t, err) - - ep2, err := c2.Endpoint(ctx, "http") - require.NoError(t, err) - - _, err = http.Get(ep2 + "/stdout?echo=hello2") - require.NoError(t, err) - - _, err = http.Get(ep2 + "/stdout?echo=there2") - require.NoError(t, err) - - // Handling the termination of the containers - defer func() { - shutdownCtx, shutdownCancel := context.WithTimeout( - context.Background(), 60*time.Second, - ) - defer shutdownCancel() - _ = c.Terminate(shutdownCtx) - _ = c2.Terminate(shutdownCtx) - }() - - // Deliberately calling context cancel - cancel() - - // We check log size due to context cancellation causing - // varying message counts, leading to test failure. - assert.GreaterOrEqual(t, len(first.Msgs), 2) - assert.GreaterOrEqual(t, len(second.Msgs), 2) - - // Restore stderr - w.Close() - os.Stderr = oldStderr - - // Read the stderr output from the buffer - var buf bytes.Buffer - _, _ = buf.ReadFrom(r) - - // Check the stderr message - actual := buf.String() - - // The context cancel shouldn't cause the system to throw a - // logStoppedForOutOfSyncMessage, as it hangs the system with - // the multiple containers. - assert.False(t, strings.Contains(actual, logStoppedForOutOfSyncMessage)) -} diff --git a/logger_test.go b/logger_test.go index d4debef016..aba2789d08 100644 --- a/logger_test.go +++ b/logger_test.go @@ -1,3 +1,5 @@ +// This test is testing very internal logic that should not be exported away from this package. We'll +// leave it in the main testcontainers package. Do not use for user facing examples. package testcontainers import ( diff --git a/parallel_test.go b/parallel_test.go index 122f59a4f7..4e7938140c 100644 --- a/parallel_test.go +++ b/parallel_test.go @@ -1,4 +1,4 @@ -package testcontainers +package testcontainers_test import ( "context" @@ -9,21 +9,22 @@ import ( "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" "github.com/testcontainers/testcontainers-go/wait" ) func TestParallelContainers(t *testing.T) { tests := []struct { name string - reqs ParallelContainerRequest + reqs testcontainers.ParallelContainerRequest resLen int expErrors int }{ { name: "running two containers (one error)", - reqs: ParallelContainerRequest{ + reqs: testcontainers.ParallelContainerRequest{ { - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "nginx", ExposedPorts: []string{ "10080/tcp", @@ -32,7 +33,7 @@ func TestParallelContainers(t *testing.T) { Started: true, }, { - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "bad bad bad", ExposedPorts: []string{ "10081/tcp", @@ -46,9 +47,9 @@ func TestParallelContainers(t *testing.T) { }, { name: "running two containers (all errors)", - reqs: ParallelContainerRequest{ + reqs: testcontainers.ParallelContainerRequest{ { - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "bad bad bad", ExposedPorts: []string{ "10081/tcp", @@ -57,7 +58,7 @@ func TestParallelContainers(t *testing.T) { Started: true, }, { - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "bad bad bad", ExposedPorts: []string{ "10081/tcp", @@ -71,9 +72,9 @@ func TestParallelContainers(t *testing.T) { }, { name: "running two containers (success)", - reqs: ParallelContainerRequest{ + reqs: testcontainers.ParallelContainerRequest{ { - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "nginx", ExposedPorts: []string{ "10080/tcp", @@ -82,7 +83,7 @@ func TestParallelContainers(t *testing.T) { Started: true, }, { - ContainerRequest: ContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "nginx", ExposedPorts: []string{ "10081/tcp", @@ -98,10 +99,10 @@ func TestParallelContainers(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - res, err := ParallelContainers(context.Background(), tc.reqs, ParallelContainersOptions{}) + res, err := testcontainers.ParallelContainers(context.Background(), tc.reqs, testcontainers.ParallelContainersOptions{}) if err != nil { require.NotZero(t, tc.expErrors) - var e ParallelContainersError + var e testcontainers.ParallelContainersError errors.As(err, &e) if len(e.Errors) != tc.expErrors { t.Fatalf("expected errors: %d, got: %d\n", tc.expErrors, len(e.Errors)) @@ -130,8 +131,8 @@ func TestParallelContainersWithReuse(t *testing.T) { natPort := fmt.Sprintf("%d/tcp", postgresPort) - req := GenericContainerRequest{ - ContainerRequest: ContainerRequest{ + req := testcontainers.GenericContainerRequest{ + ContainerRequest: testcontainers.ContainerRequest{ Image: "postgis/postgis", Name: "test-postgres", ExposedPorts: []string{natPort}, @@ -148,7 +149,7 @@ func TestParallelContainersWithReuse(t *testing.T) { Reuse: true, } - parallelRequest := ParallelContainerRequest{ + parallelRequest := testcontainers.ParallelContainerRequest{ req, req, req, @@ -156,9 +157,9 @@ func TestParallelContainersWithReuse(t *testing.T) { ctx := context.Background() - res, err := ParallelContainers(ctx, parallelRequest, ParallelContainersOptions{}) + res, err := testcontainers.ParallelContainers(ctx, parallelRequest, testcontainers.ParallelContainersOptions{}) if err != nil { - var e ParallelContainersError + var e testcontainers.ParallelContainersError errors.As(err, &e) t.Fatalf("expected errors: %d, got: %d\n", 0, len(e.Errors)) } diff --git a/testcontainers_test.go b/testcontainers_test.go index fe5af71e89..d92a3bebad 100644 --- a/testcontainers_test.go +++ b/testcontainers_test.go @@ -1,3 +1,5 @@ +// This test is testing very internal logic that should not be exported away from this package. We'll +// leave it in the main testcontainers package. Do not use for user facing examples. package testcontainers import ( diff --git a/testing_test.go b/testing_test.go index 56817d655a..d185033b78 100644 --- a/testing_test.go +++ b/testing_test.go @@ -1,3 +1,5 @@ +// This test is testing very internal logic that should not be exported away from this package. We'll +// leave it in the main testcontainers package. Do not use for user facing examples. package testcontainers import "testing"