Skip to content

Commit 03369a7

Browse files
committed
fix: handle stopped Docker containers in test setup
The docker test helpers checked if a container existed via `docker container inspect`, but this returns true for stopped/exited containers too. When a container existed but wasn't running, the code skipped creation and went straight to pinging — which timed out after 10s. Extract an ensureContainer helper that: 1. Inspects the container state (not just existence) 2. Runs `docker start` if the container exists but is stopped 3. Creates via `docker run` if the container doesn't exist 4. Handles the race where a parallel test process creates the container between our inspect and run calls 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> https://claude.ai/code/session_01WLKDmB7kemgPsjsj9KFEXa
1 parent 885349d commit 03369a7

File tree

3 files changed

+59
-51
lines changed

3 files changed

+59
-51
lines changed

internal/sqltest/docker/enabled.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package docker
33
import (
44
"fmt"
55
"os/exec"
6+
"strings"
67

78
"golang.org/x/sync/singleflight"
89
)
@@ -15,3 +16,44 @@ func Installed() error {
1516
}
1617
return nil
1718
}
19+
20+
// ensureContainer makes sure a Docker container with the given name is running.
21+
// It handles three cases:
22+
// 1. Container doesn't exist: create it with docker run
23+
// 2. Container exists but stopped: start it with docker start
24+
// 3. Container exists and running: do nothing
25+
//
26+
// It also handles the race condition where a parallel test process creates the
27+
// container between our inspect and run calls.
28+
func ensureContainer(name string, runArgs ...string) error {
29+
// Check if container exists and whether it's running
30+
output, err := exec.Command("docker", "container", "inspect",
31+
"-f", "{{.State.Running}}", name).CombinedOutput()
32+
33+
if err == nil {
34+
// Container exists — check if it's running
35+
if strings.TrimSpace(string(output)) != "true" {
36+
// Container exists but is stopped, start it
37+
if startOut, startErr := exec.Command("docker", "start", name).CombinedOutput(); startErr != nil {
38+
return fmt.Errorf("docker start %s: %s: %w", name, string(startOut), startErr)
39+
}
40+
}
41+
return nil
42+
}
43+
44+
// Container doesn't exist, create it
45+
args := append([]string{"run", "--name", name}, runArgs...)
46+
createOut, createErr := exec.Command("docker", args...).CombinedOutput()
47+
48+
if createErr != nil {
49+
// Handle race: another process may have created the container between
50+
// our inspect and run calls
51+
conflictMsg := fmt.Sprintf(`Conflict. The container name "/%s" is already in use`, name)
52+
if strings.Contains(string(createOut), conflictMsg) {
53+
return nil
54+
}
55+
return fmt.Errorf("docker run %s: %s: %w", name, string(createOut), createErr)
56+
}
57+
58+
return nil
59+
}

internal/sqltest/docker/mysql.go

Lines changed: 8 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"database/sql"
66
"fmt"
77
"os/exec"
8-
"strings"
98
"time"
109

1110
_ "github.com/go-sql-driver/mysql"
@@ -46,30 +45,14 @@ func startMySQLServer(c context.Context) (string, error) {
4645
}
4746
}
4847

49-
var exists bool
50-
{
51-
cmd := exec.Command("docker", "container", "inspect", "sqlc_sqltest_docker_mysql")
52-
// This means we've already started the container
53-
exists = cmd.Run() == nil
54-
}
55-
56-
if !exists {
57-
cmd := exec.Command("docker", "run",
58-
"--name", "sqlc_sqltest_docker_mysql",
59-
"-e", "MYSQL_ROOT_PASSWORD=mysecretpassword",
60-
"-e", "MYSQL_DATABASE=dinotest",
61-
"-p", "3306:3306",
62-
"-d",
63-
"mysql:9",
64-
)
65-
66-
output, err := cmd.CombinedOutput()
67-
fmt.Println(string(output))
68-
69-
msg := `Conflict. The container name "/sqlc_sqltest_docker_mysql" is already in use by container`
70-
if !strings.Contains(string(output), msg) && err != nil {
71-
return "", err
72-
}
48+
if err := ensureContainer("sqlc_sqltest_docker_mysql",
49+
"-e", "MYSQL_ROOT_PASSWORD=mysecretpassword",
50+
"-e", "MYSQL_DATABASE=dinotest",
51+
"-p", "3306:3306",
52+
"-d",
53+
"mysql:9",
54+
); err != nil {
55+
return "", err
7356
}
7457

7558
ctx, cancel := context.WithTimeout(c, 10*time.Second)

internal/sqltest/docker/postgres.go

Lines changed: 9 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"fmt"
66
"log/slog"
77
"os/exec"
8-
"strings"
98
"time"
109

1110
"github.com/jackc/pgx/v5"
@@ -48,31 +47,15 @@ func startPostgreSQLServer(c context.Context) (string, error) {
4847

4948
uri := "postgres://postgres:mysecretpassword@localhost:5432/postgres?sslmode=disable"
5049

51-
var exists bool
52-
{
53-
cmd := exec.Command("docker", "container", "inspect", "sqlc_sqltest_docker_postgres")
54-
// This means we've already started the container
55-
exists = cmd.Run() == nil
56-
}
57-
58-
if !exists {
59-
cmd := exec.Command("docker", "run",
60-
"--name", "sqlc_sqltest_docker_postgres",
61-
"-e", "POSTGRES_PASSWORD=mysecretpassword",
62-
"-e", "POSTGRES_USER=postgres",
63-
"-p", "5432:5432",
64-
"-d",
65-
"postgres:16",
66-
"-c", "max_connections=200",
67-
)
68-
69-
output, err := cmd.CombinedOutput()
70-
fmt.Println(string(output))
71-
72-
msg := `Conflict. The container name "/sqlc_sqltest_docker_postgres" is already in use by container`
73-
if !strings.Contains(string(output), msg) && err != nil {
74-
return "", err
75-
}
50+
if err := ensureContainer("sqlc_sqltest_docker_postgres",
51+
"-e", "POSTGRES_PASSWORD=mysecretpassword",
52+
"-e", "POSTGRES_USER=postgres",
53+
"-p", "5432:5432",
54+
"-d",
55+
"postgres:16",
56+
"-c", "max_connections=200",
57+
); err != nil {
58+
return "", err
7659
}
7760

7861
ctx, cancel := context.WithTimeout(c, 5*time.Second)

0 commit comments

Comments
 (0)