From ea6a015c8d603f4c214b0d01a57fee32ffdd9567 Mon Sep 17 00:00:00 2001 From: Ivan Sitkin Date: Tue, 20 Feb 2024 18:11:49 +0100 Subject: [PATCH 1/2] feat: allow start container with reuse in different test package --- docker.go | 58 +++++++++++++++++++++++++++++++++++++-- generic_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ reaper.go | 3 --- 3 files changed, 128 insertions(+), 5 deletions(-) diff --git a/docker.go b/docker.go index a559b8d7ea..1aef8c2b5f 100644 --- a/docker.go +++ b/docker.go @@ -14,6 +14,7 @@ import ( "net/url" "os" "path/filepath" + "regexp" "strings" "sync" "time" @@ -49,6 +50,8 @@ const ( logStoppedForOutOfSyncMessage = "Stopping log consumer: Headers out of sync" ) +var createContainerFailDueToNameConflictRegex = regexp.MustCompile("Conflict. The container name .* is already in use by container .*") + // DockerContainer represents a container started using Docker type DockerContainer struct { // Container ID from Docker @@ -1153,13 +1156,40 @@ func (p *DockerProvider) findContainerByName(ctx context.Context, name string) ( return nil, nil } +func (p *DockerProvider) waitContainerCreation(ctx context.Context, name string) (*types.Container, error) { + var container *types.Container + return container, backoff.Retry(func() error { + c, err := p.findContainerByName(ctx, name) + if err != nil { + return err + } + + if c == nil { + return fmt.Errorf("container %s not found", name) + } + + container = c + return nil + }, backoff.WithContext(backoff.NewExponentialBackOff(), ctx)) +} + func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req ContainerRequest) (Container, error) { c, err := p.findContainerByName(ctx, req.Name) if err != nil { return nil, err } if c == nil { - return p.CreateContainer(ctx, req) + createdContainer, err := p.CreateContainer(ctx, req) + if err == nil { + return createdContainer, nil + } + if !createContainerFailDueToNameConflictRegex.MatchString(err.Error()) { + return nil, err + } + c, err = p.waitContainerCreation(ctx, req.Name) + if err != nil { + return nil, err + } } sessionID := core.SessionID() @@ -1178,6 +1208,13 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain } } + // default hooks include logger hook and pre-create hook + defaultHooks := []ContainerLifecycleHooks{ + DefaultLoggingHook(p.Logger), + defaultReadinessHook(), + } + allHooks := combineContainerHooks(defaultHooks, req.LifecycleHooks) + dc := &DockerContainer{ ID: c.ID, WaitingFor: req.WaitingFor, @@ -1187,7 +1224,24 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain terminationSignal: termSignal, stopLogProductionCh: nil, logger: p.Logger, - isRunning: c.State == "running", + lifecycleHooks: []ContainerLifecycleHooks{ + { + PostStarts: allHooks.PostStarts, + PostReadies: allHooks.PostReadies, + }, + }, + } + + err = dc.startedHook(ctx) + if err != nil { + return nil, err + } + + dc.isRunning = true + + err = dc.readiedHook(ctx) + if err != nil { + return nil, err } return dc, nil diff --git a/generic_test.go b/generic_test.go index cb38e29faf..72688876ec 100644 --- a/generic_test.go +++ b/generic_test.go @@ -3,6 +3,11 @@ package testcontainers import ( "context" "errors" + "net/http" + "os" + "os/exec" + "strings" + "sync" "testing" "time" @@ -117,3 +122,70 @@ func TestGenericContainerShouldReturnRefOnError(t *testing.T) { require.NotNil(t, c) terminateContainerOnEnd(t, context.Background(), c) } + +func TestGenericReusableContainerInSubprocess(t *testing.T) { + wg := sync.WaitGroup{} + wg.Add(10) + for i := 0; i < 10; i++ { + go func() { + defer wg.Done() + + // create containers in subprocesses, as "go test ./..." does. + output := createReuseContainerInSubprocess(t) + + // check is reuse container with WaitingFor work correctly. + require.True(t, strings.Contains(output, "🚧 Waiting for container id")) + require.True(t, strings.Contains(output, "🔔 Container is ready")) + }() + } + + wg.Wait() +} + +func createReuseContainerInSubprocess(t *testing.T) string { + cmd := exec.Command(os.Args[0], "-test.run=TestHelperContainerStarterProcess") + cmd.Env = []string{"GO_WANT_HELPER_PROCESS=1"} + + output, err := cmd.CombinedOutput() + require.NoError(t, err, string(output)) + + return string(output) +} + +// TestHelperContainerStarterProcess is a helper function +// to start a container in a subprocess. It's not a real test. +func TestHelperContainerStarterProcess(t *testing.T) { + if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { + t.Skip("Skipping helper test function. It's not a real test") + } + + ctx := context.Background() + + nginxC, err := GenericContainer(ctx, GenericContainerRequest{ + ProviderType: providerType, + ContainerRequest: ContainerRequest{ + Image: nginxDelayedImage, + ExposedPorts: []string{nginxDefaultPort}, + WaitingFor: wait.ForListeningPort(nginxDefaultPort), // default startupTimeout is 60s + Name: reusableContainerName, + }, + Started: true, + Reuse: true, + }) + require.NoError(t, err) + require.True(t, nginxC.IsRunning()) + + origin, err := nginxC.PortEndpoint(ctx, nginxDefaultPort, "http") + require.NoError(t, err) + + // check is reuse container with WaitingFor work correctly. + req, err := http.NewRequestWithContext(ctx, http.MethodGet, origin, nil) + require.NoError(t, err) + req.Close = true + + resp, err := http.DefaultClient.Do(req) + require.NoError(t, err) + defer resp.Body.Close() + + require.Equal(t, http.StatusOK, resp.StatusCode) +} diff --git a/reaper.go b/reaper.go index 724d2635a1..3dff6a7c9d 100644 --- a/reaper.go +++ b/reaper.go @@ -6,7 +6,6 @@ import ( "fmt" "math/rand" "net" - "regexp" "strings" "sync" "time" @@ -186,8 +185,6 @@ func reuseOrCreateReaper(ctx context.Context, sessionID string, provider ReaperP return reaperInstance, nil } -var createContainerFailDueToNameConflictRegex = regexp.MustCompile("Conflict. The container name .* is already in use by container .*") - // reuseReaperContainer constructs a Reaper from an already running reaper // DockerContainer. func reuseReaperContainer(ctx context.Context, sessionID string, provider ReaperProvider, reaperContainer *DockerContainer) (*Reaper, error) { From 90a168f3e16476d25df469774048fe603e7578ab Mon Sep 17 00:00:00 2001 From: Ivan Sitkin Date: Wed, 21 Feb 2024 15:14:24 +0100 Subject: [PATCH 2/2] feat(reuse): add defaultLogConsumersHook in reuse container --- docker.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/docker.go b/docker.go index 1aef8c2b5f..33a73cae73 100644 --- a/docker.go +++ b/docker.go @@ -1212,8 +1212,8 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain defaultHooks := []ContainerLifecycleHooks{ DefaultLoggingHook(p.Logger), defaultReadinessHook(), + defaultLogConsumersHook(req.LogConsumerCfg), } - allHooks := combineContainerHooks(defaultHooks, req.LifecycleHooks) dc := &DockerContainer{ ID: c.ID, @@ -1224,12 +1224,7 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain terminationSignal: termSignal, stopLogProductionCh: nil, logger: p.Logger, - lifecycleHooks: []ContainerLifecycleHooks{ - { - PostStarts: allHooks.PostStarts, - PostReadies: allHooks.PostReadies, - }, - }, + lifecycleHooks: []ContainerLifecycleHooks{combineContainerHooks(defaultHooks, req.LifecycleHooks)}, } err = dc.startedHook(ctx)