Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow use reuse containers in parrallel packages tests #1572

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
71 changes: 59 additions & 12 deletions docker.go
Expand Up @@ -40,7 +40,8 @@ var (
// Implement interfaces
_ Container = (*DockerContainer)(nil)

ErrDuplicateMountTarget = errors.New("duplicate mount target detected")
ErrDuplicateMountTarget = errors.New("duplicate mount target detected")
ErrContainerAlreadyExists = errors.New("container already exists")
)

const (
Expand Down Expand Up @@ -838,6 +839,22 @@ func (p *DockerProvider) BuildImage(ctx context.Context, img ImageBuildInfo) (st
return repoTag, nil
}

func waitForContainerReady(ctx context.Context, container *DockerContainer) error {
if container.WaitingFor == nil {
return nil
}

container.logger.Printf(
"🚧 Waiting for container id %s image: %s. Waiting for: %+v",
container.ID[:12], container.Image, container.WaitingFor,
)
if err := container.WaitingFor.WaitUntilReady(ctx, container); err != nil {
return err
}

return nil
}

// CreateContainer fulfills a request for a container without starting it
func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerRequest) (Container, error) {
var err error
Expand Down Expand Up @@ -1038,14 +1055,8 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
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
}
if err := waitForContainerReady(ctx, dockerContainer); err != nil {
return err
}

dockerContainer.isRunning = true
Expand All @@ -1065,7 +1076,10 @@ func (p *DockerProvider) CreateContainer(ctx context.Context, req ContainerReque
}

resp, err := p.client.ContainerCreate(ctx, dockerInput, hostConfig, networkingConfig, platform, req.Name)
if err != nil {
switch {
case errdefs.IsConflict(err):
return nil, fmt.Errorf("%w: %w", ErrContainerAlreadyExists, err)
case err != nil:
return nil, err
}

Expand Down Expand Up @@ -1131,13 +1145,41 @@ 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)
switch {
case errors.Is(err, ErrContainerAlreadyExists):
c, err = p.waitContainerCreation(ctx, req.Name)
if err != nil {
return nil, err
}
case err != nil:
return nil, err
default:
return createdContainer, nil
}
}

sessionID := testcontainerssession.SessionID()
Expand Down Expand Up @@ -1165,9 +1207,14 @@ func (p *DockerProvider) ReuseOrCreateContainer(ctx context.Context, req Contain
terminationSignal: termSignal,
stopProducer: nil,
logger: p.Logger,
isRunning: c.State == "running",
}

if err := waitForContainerReady(ctx, dc); err != nil {
return nil, err
}

dc.isRunning = true

return dc, nil
}

Expand Down
72 changes: 72 additions & 0 deletions generic_test.go
Expand Up @@ -3,6 +3,11 @@ package testcontainers
import (
"context"
"errors"
"net/http"
"os"
stdexec "os/exec"
"strings"
"sync"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -96,3 +101,70 @@ func TestGenericReusableContainer(t *testing.T) {
})
}
}

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.
contains := strings.Contains(output, "🚧 Waiting for container id")
require.True(t, contains)
}()
}

wg.Wait()
}

func createReuseContainerInSubprocess(t *testing.T) string {
cmd := stdexec.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)
}