From 9eea435ec5ae4af917a5a4910dbe1d1542dfe58f Mon Sep 17 00:00:00 2001 From: Ivan Sitkin Date: Fri, 23 Feb 2024 08:02:44 +0100 Subject: [PATCH 1/2] bug: allow start container with reuse in different test package (#2247) * feat: allow start container with reuse in different test package * feat(reuse): add defaultLogConsumersHook in reuse container --------- Co-authored-by: Ivan Sitkin --- docker.go | 53 ++++++++++++++++++++++++++++++++++-- generic_test.go | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ reaper.go | 3 --- 3 files changed, 123 insertions(+), 5 deletions(-) diff --git a/docker.go b/docker.go index a559b8d7ea..33a73cae73 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(), + defaultLogConsumersHook(req.LogConsumerCfg), + } + dc := &DockerContainer{ ID: c.ID, WaitingFor: req.WaitingFor, @@ -1187,7 +1224,19 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain terminationSignal: termSignal, stopLogProductionCh: nil, logger: p.Logger, - isRunning: c.State == "running", + lifecycleHooks: []ContainerLifecycleHooks{combineContainerHooks(defaultHooks, req.LifecycleHooks)}, + } + + 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 8085e2f1ce96439386130c946261e2c806cba7c2 Mon Sep 17 00:00:00 2001 From: Steven Hartland Date: Fri, 23 Feb 2024 07:05:01 +0000 Subject: [PATCH 2/2] docs: fix comment corruption (#2262) Fix doc generation comments being included package documentation by ensuring they are separated by a blank line. --- internal/config/config.go | 3 ++- logconsumer.go | 2 ++ modules/couchbase/couchbase.go | 2 ++ modules/couchbase/storage_mode.go | 1 + modules/neo4j/config.go | 2 ++ options.go | 1 + testing.go | 1 + 7 files changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index 08d784b7ed..7471cf0de5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -18,8 +18,9 @@ var ( tcConfigOnce *sync.Once = new(sync.Once) ) -// Config represents the configuration for Testcontainers // testcontainersConfig { + +// Config represents the configuration for Testcontainers type Config struct { Host string `properties:"docker.host,default="` TLSVerify int `properties:"docker.tls.verify,default=0"` diff --git a/logconsumer.go b/logconsumer.go index 0b9b1bd025..95bf111983 100644 --- a/logconsumer.go +++ b/logconsumer.go @@ -7,6 +7,7 @@ const StdoutLog = "STDOUT" const StderrLog = "STDERR" // logStruct { + // Log represents a message that was created by a process, // LogType is either "STDOUT" or "STDERR", // Content is the byte contents of the message itself @@ -18,6 +19,7 @@ type Log struct { // } // logConsumerInterface { + // LogConsumer represents any object that can // handle a Log, it is up to the LogConsumer instance // what to do with the log diff --git a/modules/couchbase/couchbase.go b/modules/couchbase/couchbase.go index f13378ab43..c81c420c8b 100644 --- a/modules/couchbase/couchbase.go +++ b/modules/couchbase/couchbase.go @@ -20,6 +20,7 @@ import ( const ( // containerPorts { + MGMT_PORT = "8091" MGMT_SSL_PORT = "18091" @@ -40,6 +41,7 @@ const ( KV_PORT = "11210" KV_SSL_PORT = "11207" + // } ) diff --git a/modules/couchbase/storage_mode.go b/modules/couchbase/storage_mode.go index 2f65886688..6957c7fbef 100644 --- a/modules/couchbase/storage_mode.go +++ b/modules/couchbase/storage_mode.go @@ -6,6 +6,7 @@ package couchbase type indexStorageMode string // storageTypes { + const ( // MemoryOptimized sets the cluster-wide index storage mode to use memory optimized global // secondary indexes which can perform index maintenance and index scan faster at in-memory speeds. diff --git a/modules/neo4j/config.go b/modules/neo4j/config.go index 5c868ce3aa..9d0bc70816 100644 --- a/modules/neo4j/config.go +++ b/modules/neo4j/config.go @@ -12,12 +12,14 @@ type LabsPlugin string const ( // labsPlugins { + Apoc LabsPlugin = "apoc" ApocCore LabsPlugin = "apoc-core" Bloom LabsPlugin = "bloom" GraphDataScience LabsPlugin = "graph-data-science" NeoSemantics LabsPlugin = "n10s" Streams LabsPlugin = "streams" + // } ) diff --git a/options.go b/options.go index 34881dba85..391d3e1a82 100644 --- a/options.go +++ b/options.go @@ -82,6 +82,7 @@ func WithImage(image string) CustomizeRequestOption { } // imageSubstitutor { + // ImageSubstitutor represents a way to substitute container image names type ImageSubstitutor interface { // Description returns the name of the type and a short description of how it modifies the image. diff --git a/testing.go b/testing.go index 6d23952952..eab23cb805 100644 --- a/testing.go +++ b/testing.go @@ -41,6 +41,7 @@ func SkipIfDockerDesktop(t *testing.T, ctx context.Context) { } // exampleLogConsumer { + // StdoutLogConsumer is a LogConsumer that prints the log to stdout type StdoutLogConsumer struct{}