140 lines
4.3 KiB
Go
140 lines
4.3 KiB
Go
package harness
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// EngineImageRef is the canonical tag the lobbyrtm boundary suite (and
|
|
// any future suite that needs the galaxy/game engine binary) builds and
|
|
// runs against. The `-lobbyrtm-it` suffix differs from the
|
|
// `-rtm-it` tag the service-local rtmanager/integration harness uses, so
|
|
// an operator running both suites locally cannot accidentally consume
|
|
// the wrong image, and `docker image rm` of one suite's leftovers does
|
|
// not remove the other suite's tag.
|
|
const EngineImageRef = "galaxy/game:1.0.0-lobbyrtm-it"
|
|
|
|
const (
|
|
imageBuildTimeout = 10 * time.Minute
|
|
dockerDaemonPingTimeout = 5 * time.Second
|
|
)
|
|
|
|
var (
|
|
engineImageOnce sync.Once
|
|
engineImageErr error
|
|
|
|
dockerAvailableOnce sync.Once
|
|
dockerAvailableErr error
|
|
)
|
|
|
|
// RequireDockerDaemon skips the calling test when no Docker daemon is
|
|
// reachable from this process. Suites that need Docker but stand up
|
|
// testcontainers (Postgres/Redis) before any RTM-specific helper
|
|
// should call this helper first so the skip path runs *before* the
|
|
// testcontainer client probes the daemon and fails hard.
|
|
func RequireDockerDaemon(t testing.TB) {
|
|
t.Helper()
|
|
requireDockerDaemon(t)
|
|
}
|
|
|
|
// EnsureGalaxyGameImage builds the galaxy/game engine image from the
|
|
// workspace root once per test process and returns the canonical tag.
|
|
// On hosts without a reachable Docker daemon the helper calls `t.Skip`
|
|
// so suites stay green when `/var/run/docker.sock` is missing and
|
|
// `DOCKER_HOST` is unset.
|
|
//
|
|
// The build is wrapped in `sync.Once`; concurrent suite invocations
|
|
// share the same image. The Dockerfile path and build context match
|
|
// `rtmanager/integration/harness/docker.go::buildAndTagEngineImage` —
|
|
// galaxy's `go.work` resolves `galaxy/{model,error,...}` only when the
|
|
// workspace root is the build context.
|
|
func EnsureGalaxyGameImage(t testing.TB) string {
|
|
t.Helper()
|
|
requireDockerDaemon(t)
|
|
|
|
engineImageOnce.Do(func() {
|
|
engineImageErr = buildEngineImage()
|
|
})
|
|
if engineImageErr != nil {
|
|
t.Fatalf("integration harness: build galaxy/game image: %v", engineImageErr)
|
|
}
|
|
return EngineImageRef
|
|
}
|
|
|
|
func buildEngineImage() error {
|
|
root, err := workspaceRoot()
|
|
if err != nil {
|
|
return fmt.Errorf("resolve workspace root: %w", err)
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), imageBuildTimeout)
|
|
defer cancel()
|
|
|
|
dockerfilePath := filepath.Join("game", "Dockerfile")
|
|
cmd := exec.CommandContext(ctx, "docker", "build",
|
|
"-f", dockerfilePath,
|
|
"-t", EngineImageRef,
|
|
".",
|
|
)
|
|
cmd.Dir = root
|
|
cmd.Env = append(os.Environ(), "DOCKER_BUILDKIT=1")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("docker build (-f %s) in %s: %w; output:\n%s",
|
|
dockerfilePath, root, err, strings.TrimSpace(string(output)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// requireDockerDaemon skips the calling test when no Docker daemon is
|
|
// reachable from this process. The check runs once per process and
|
|
// caches the verdict so successive callers do not pay the ping cost.
|
|
func requireDockerDaemon(t testing.TB) {
|
|
t.Helper()
|
|
dockerAvailableOnce.Do(func() {
|
|
dockerAvailableErr = pingDockerDaemon()
|
|
})
|
|
if dockerAvailableErr != nil {
|
|
t.Skipf("integration harness: docker daemon unavailable: %v", dockerAvailableErr)
|
|
}
|
|
}
|
|
|
|
func pingDockerDaemon() error {
|
|
if os.Getenv("DOCKER_HOST") == "" {
|
|
if _, err := os.Stat("/var/run/docker.sock"); err != nil {
|
|
return fmt.Errorf("set DOCKER_HOST or expose /var/run/docker.sock: %w", err)
|
|
}
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), dockerDaemonPingTimeout)
|
|
defer cancel()
|
|
cmd := exec.CommandContext(ctx, "docker", "version", "--format", "{{.Server.Version}}")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("docker version: %w; output:\n%s", err, strings.TrimSpace(string(output)))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// workspaceRoot resolves the absolute path of the galaxy/ workspace
|
|
// root by anchoring on this file's location. The harness lives at
|
|
// `galaxy/integration/internal/harness/engineimage.go`; the workspace
|
|
// root is three directories up.
|
|
func workspaceRoot() (string, error) {
|
|
_, file, _, ok := runtime.Caller(0)
|
|
if !ok {
|
|
return "", errors.New("resolve runtime caller for workspace root")
|
|
}
|
|
dir := filepath.Dir(file)
|
|
root := filepath.Clean(filepath.Join(dir, "..", "..", ".."))
|
|
return root, nil
|
|
}
|