179 lines
4.2 KiB
Go
179 lines
4.2 KiB
Go
package testutil
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"os/exec"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type DockerRunRequest struct {
|
|
NamePrefix string
|
|
Image string
|
|
Env map[string]string
|
|
ExposedPorts []string
|
|
Command []string
|
|
}
|
|
|
|
type DockerContainer struct {
|
|
name string
|
|
}
|
|
|
|
func SkipIfDockerUnavailable(t *testing.T, skipOnWindows bool) {
|
|
t.Helper()
|
|
|
|
if testing.Short() {
|
|
t.Skip("skipping docker-backed integration test in short mode")
|
|
}
|
|
if skipOnWindows && runtime.GOOS == "windows" {
|
|
t.Skip("skipping docker-backed integration test on windows")
|
|
}
|
|
|
|
cmd := exec.Command("docker", "info")
|
|
cmd.Stdout = io.Discard
|
|
cmd.Stderr = io.Discard
|
|
if err := cmd.Run(); err != nil {
|
|
t.Skipf("skipping docker-backed integration test: %v", err)
|
|
}
|
|
}
|
|
|
|
func StartDockerContainer(t *testing.T, ctx context.Context, request DockerRunRequest) *DockerContainer {
|
|
t.Helper()
|
|
|
|
name := uniqueContainerName(request.NamePrefix)
|
|
args := []string{"run", "-d", "--rm", "--name", name}
|
|
|
|
if len(request.Env) > 0 {
|
|
keys := make([]string, 0, len(request.Env))
|
|
for key := range request.Env {
|
|
keys = append(keys, key)
|
|
}
|
|
slices.Sort(keys)
|
|
for _, key := range keys {
|
|
args = append(args, "-e", fmt.Sprintf("%s=%s", key, request.Env[key]))
|
|
}
|
|
}
|
|
|
|
for _, port := range request.ExposedPorts {
|
|
containerPort := strings.TrimSuffix(port, "/tcp")
|
|
args = append(args, "-p", fmt.Sprintf("127.0.0.1::%s", containerPort))
|
|
}
|
|
|
|
args = append(args, request.Image)
|
|
args = append(args, request.Command...)
|
|
|
|
cmd := exec.CommandContext(ctx, "docker", args...)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("docker run failed: %v\n%s", err, strings.TrimSpace(string(output)))
|
|
}
|
|
|
|
container := &DockerContainer{name: name}
|
|
t.Cleanup(func() {
|
|
terminateCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
|
defer cancel()
|
|
if err := container.Terminate(terminateCtx); err != nil {
|
|
t.Fatalf("failed to terminate docker container %s: %v", name, err)
|
|
}
|
|
})
|
|
|
|
return container
|
|
}
|
|
|
|
func (container *DockerContainer) PublishedPort(t *testing.T, ctx context.Context, port string) string {
|
|
t.Helper()
|
|
|
|
cmd := exec.CommandContext(ctx, "docker", "port", container.name, port)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("docker port failed: %v\n%s", err, strings.TrimSpace(string(output)))
|
|
}
|
|
|
|
publishedPort, err := parsePublishedPort(string(output))
|
|
if err != nil {
|
|
t.Fatalf("failed to parse published port for %s: %v", port, err)
|
|
}
|
|
|
|
return publishedPort
|
|
}
|
|
|
|
func (container *DockerContainer) Terminate(ctx context.Context) error {
|
|
cmd := exec.CommandContext(ctx, "docker", "rm", "-f", container.name)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
trimmed := strings.TrimSpace(string(output))
|
|
if strings.Contains(trimmed, "No such container") {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("docker rm -f %s: %w: %s", container.name, err, trimmed)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func WaitForCondition(ctx context.Context, timeout time.Duration, interval time.Duration, check func() error) error {
|
|
deadline := time.Now().Add(timeout)
|
|
var lastErr error
|
|
|
|
for {
|
|
if err := check(); err == nil {
|
|
return nil
|
|
} else {
|
|
lastErr = err
|
|
}
|
|
|
|
if time.Now().After(deadline) {
|
|
break
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(interval):
|
|
}
|
|
}
|
|
|
|
if lastErr == nil {
|
|
lastErr = fmt.Errorf("condition did not become ready")
|
|
}
|
|
|
|
return fmt.Errorf("timed out waiting for condition: %w", lastErr)
|
|
}
|
|
|
|
func parsePublishedPort(output string) (string, error) {
|
|
line := strings.TrimSpace(output)
|
|
if line == "" {
|
|
return "", fmt.Errorf("empty docker port output")
|
|
}
|
|
|
|
if newline := strings.Index(line, "\n"); newline >= 0 {
|
|
line = line[:newline]
|
|
}
|
|
|
|
if _, port, err := net.SplitHostPort(line); err == nil {
|
|
return port, nil
|
|
}
|
|
|
|
lastColon := strings.LastIndex(line, ":")
|
|
if lastColon >= 0 && lastColon < len(line)-1 {
|
|
return line[lastColon+1:], nil
|
|
}
|
|
|
|
return "", fmt.Errorf("unexpected docker port output %q", output)
|
|
}
|
|
|
|
func uniqueContainerName(prefix string) string {
|
|
cleanPrefix := strings.TrimSpace(prefix)
|
|
if cleanPrefix == "" {
|
|
cleanPrefix = "xdrop-test"
|
|
}
|
|
|
|
return fmt.Sprintf("%s-%d", cleanPrefix, time.Now().UnixNano())
|
|
}
|