Files

193 lines
5.3 KiB
Go

package testcontainers
import (
"context"
"fmt"
"io"
"regexp"
"testing"
"github.com/containerd/errdefs"
"github.com/stretchr/testify/require"
)
// errAlreadyInProgress is a regular expression that matches the error for a container
// removal that is already in progress.
var errAlreadyInProgress = regexp.MustCompile(`removal of container .* is already in progress`)
// SkipIfProviderIsNotHealthy is a utility function capable of skipping tests
// if the provider is not healthy, or running at all.
// This is a function designed to be used in your test, when Docker is not mandatory for CI/CD.
// In this way tests that depend on Testcontainers won't run if the provider is provisioned correctly.
func SkipIfProviderIsNotHealthy(t *testing.T) {
t.Helper()
defer func() {
if r := recover(); r != nil {
t.Skipf("Recovered from panic: %v. Docker is not running. Testcontainers can't perform is work without it", r)
}
}()
ctx := context.Background()
provider, err := ProviderDocker.GetProvider()
if err != nil {
t.Skipf("Docker is not running. Testcontainers can't perform is work without it: %s", err)
}
err = provider.Health(ctx)
if err != nil {
t.Skipf("Docker is not running. Testcontainers can't perform is work without it: %s", err)
}
}
// SkipIfDockerDesktop is a utility function capable of skipping tests
// if tests are run using Docker Desktop.
func SkipIfDockerDesktop(t *testing.T, ctx context.Context) {
t.Helper()
cli, err := NewDockerClientWithOpts(ctx)
require.NoErrorf(t, err, "failed to create docker client: %s", err)
info, err := cli.Info(ctx)
require.NoErrorf(t, err, "failed to get docker info: %s", err)
if info.OperatingSystem == "Docker Desktop" {
t.Skip("Skipping test that requires host network access when running in Docker Desktop")
}
}
// SkipIfNotDockerDesktop is a utility function capable of skipping tests
// if tests are not run using Docker Desktop.
func SkipIfNotDockerDesktop(t *testing.T, ctx context.Context) {
t.Helper()
cli, err := NewDockerClientWithOpts(ctx)
require.NoErrorf(t, err, "failed to create docker client: %s", err)
info, err := cli.Info(ctx)
require.NoErrorf(t, err, "failed to get docker info: %s", err)
if info.OperatingSystem != "Docker Desktop" {
t.Skip("Skipping test that needs Docker Desktop")
}
}
// exampleLogConsumer {
// StdoutLogConsumer is a LogConsumer that prints the log to stdout
type StdoutLogConsumer struct{}
// Accept prints the log to stdout
func (lc *StdoutLogConsumer) Accept(l Log) {
fmt.Print(string(l.Content))
}
// }
// CleanupContainer is a helper function that schedules the container
// to be stopped / terminated when the test ends.
//
// This should be called as a defer directly after (before any error check)
// of [GenericContainer](...) or a modules Run(...) in a test to ensure the
// container is stopped when the function ends.
//
// before any error check. If container is nil, it's a no-op.
func CleanupContainer(tb testing.TB, ctr Container, options ...TerminateOption) {
tb.Helper()
tb.Cleanup(func() {
noErrorOrIgnored(tb, TerminateContainer(ctr, options...))
})
}
// CleanupNetwork is a helper function that schedules the network to be
// removed when the test ends.
// This should be the first call after NewNetwork(...) in a test before
// any error check. If network is nil, it's a no-op.
func CleanupNetwork(tb testing.TB, network Network) {
tb.Helper()
tb.Cleanup(func() {
if !isNil(network) {
noErrorOrIgnored(tb, network.Remove(context.Background()))
}
})
}
// noErrorOrIgnored is a helper function that checks if the error is nil or an error
// we can ignore.
func noErrorOrIgnored(tb testing.TB, err error) {
tb.Helper()
if isCleanupSafe(err) {
return
}
require.NoError(tb, err)
}
// causer is an interface that allows to get the cause of an error.
type causer interface {
Cause() error
}
// wrapErr is an interface that allows to unwrap an error.
type wrapErr interface {
Unwrap() error
}
// unwrapErrs is an interface that allows to unwrap multiple errors.
type unwrapErrs interface {
Unwrap() []error
}
// isCleanupSafe reports whether all errors in err's tree are one of the
// following, so can safely be ignored:
// - nil
// - not found
// - already in progress
func isCleanupSafe(err error) bool {
if err == nil {
return true
}
// First try with containerd's errdefs
switch {
case errdefs.IsNotFound(err):
return true
case errdefs.IsConflict(err):
// Terminating a container that is already terminating.
if errAlreadyInProgress.MatchString(err.Error()) {
return true
}
return false
}
switch x := err.(type) { //nolint:errorlint // We need to check for interfaces.
case causer:
return isCleanupSafe(x.Cause())
case wrapErr:
return isCleanupSafe(x.Unwrap())
case unwrapErrs:
for _, e := range x.Unwrap() {
if !isCleanupSafe(e) {
return false
}
}
return true
default:
return false
}
}
// RequireContainerExec is a helper function that executes a command in a container
// It insures that there is no error during the execution
// Finally returns the output of its execution
func RequireContainerExec(ctx context.Context, t *testing.T, container Container, cmd []string) string {
t.Helper()
code, out, err := container.Exec(ctx, cmd)
require.NoError(t, err)
require.Zero(t, code)
checkBytes, err := io.ReadAll(out)
require.NoError(t, err)
return string(checkBytes)
}