fix(test): store tests take an OS-assigned free port, not a PID-derived one
CI / Lint / Test / Vet (push) Successful in 36s
CI / Build & Import (push) Successful in 11s
CI / Deploy via GitOps (push) Skipped

PID%1000 collided with a concurrent job on the shared CI host (run on
e946709: process already listening on port 54000). freePort asks the OS;
Start is retried up to 3 times if the port is taken in between.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SZkhi49hfeSqBp5aZJNXsM
This commit is contained in:
2026-10-10 10:56:24 +02:00
co-authored by Claude Sonnet 5.5
parent e946709e82
commit c4233e3e05
2 changed files with 64 additions and 18 deletions
+19
View File
@@ -0,0 +1,19 @@
package store_test
import (
"fmt"
"os"
"testing"
"github.com/stretchr/testify/require"
)
// TestMain's Postgres already holds a port. A second instance started in the
// same process must still come up, i.e. the port is not derived from something
// two starters share (the PID). Observes: startTestPostgres succeeds while
// another instance from this process is listening.
func TestStartTestPostgres_SecondInstanceStartsWhileFirstHoldsItsPort(t *testing.T) {
pg, _, rt, err := startTestPostgres(fmt.Sprintf("tapir-epg-store-second-%d", os.Getpid()))
require.NoError(t, err)
t.Cleanup(func() { _ = pg.Stop(); _ = os.RemoveAll(rt) })
}
+45 -18
View File
@@ -3,6 +3,7 @@ package store_test
import ( import (
"context" "context"
"fmt" "fmt"
"net"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
@@ -25,27 +26,15 @@ var _ ports.Sink = (*store.Store)(nil)
var dsn string var dsn string
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
// Port + runtime/data dirs are per-process (PID-derived) so two concurrent // Runtime/data dirs are per-process (PID-named) and the port is OS-assigned,
// `go test` invocations — e.g. a push-run and a tag-run firing together in CI — // so concurrent `go test` runs on a shared CI host don't collide on either.
// don't collide on a fixed port or a shared data dir (which silently failed // CachePath is shared so the PG archive is downloaded once, not per process.
// both runs). CachePath is shared so the PG archive is downloaded once, not pg, port, rt, err := startTestPostgres(fmt.Sprintf("tapir-epg-store-%d", os.Getpid()))
// per process. Base 54000 keeps this package's range distinct from web's. if err != nil {
port := uint32(54000 + os.Getpid()%1000)
dsn = fmt.Sprintf("postgres://postgres:postgres@localhost:%d/postgres?sslmode=disable", port)
rt := filepath.Join(os.TempDir(), fmt.Sprintf("tapir-epg-store-%d", os.Getpid()))
pg := embeddedpostgres.NewDatabase(
embeddedpostgres.DefaultConfig().
Port(port).
RuntimePath(rt).
DataPath(filepath.Join(rt, "data")).
BinariesPath(filepath.Join(rt, "bin")).
CachePath(filepath.Join(os.TempDir(), "tapir-epg-cache")),
)
if err := pg.Start(); err != nil {
fmt.Fprintf(os.Stderr, "embedded-postgres start: %v\n", err) fmt.Fprintf(os.Stderr, "embedded-postgres start: %v\n", err)
os.Exit(1) os.Exit(1)
} }
dsn = fmt.Sprintf("postgres://postgres:postgres@localhost:%d/postgres?sslmode=disable", port)
code := m.Run() code := m.Run()
@@ -56,6 +45,44 @@ func TestMain(m *testing.M) {
os.Exit(code) os.Exit(code)
} }
// freePort asks the OS for an unused TCP port. The port is released before
// return, so another process can grab it first; startTestPostgres retries.
func freePort() (uint32, error) {
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return 0, fmt.Errorf("listen for free port: %w", err)
}
defer func() { _ = l.Close() }()
return uint32(l.Addr().(*net.TCPAddr).Port), nil
}
// startTestPostgres starts an embedded Postgres on an OS-assigned free port and
// returns it with that port and its runtime dir (caller removes it). A port
// taken between freePort and Start (shared CI host) is retried with a new one.
func startTestPostgres(name string) (*embeddedpostgres.EmbeddedPostgres, uint32, string, error) {
rt := filepath.Join(os.TempDir(), name)
var lastErr error
for range 3 {
port, err := freePort()
if err != nil {
return nil, 0, rt, err
}
pg := embeddedpostgres.NewDatabase(
embeddedpostgres.DefaultConfig().
Port(port).
RuntimePath(rt).
DataPath(filepath.Join(rt, "data")).
BinariesPath(filepath.Join(rt, "bin")).
CachePath(filepath.Join(os.TempDir(), "tapir-epg-cache")),
)
if lastErr = pg.Start(); lastErr == nil {
return pg, port, rt, nil
}
lastErr = fmt.Errorf("start on port %d: %w", port, lastErr)
}
return nil, 0, rt, lastErr
}
// uuids — fixed so tests are deterministic. user_id/video_id are UUID columns. // uuids — fixed so tests are deterministic. user_id/video_id are UUID columns.
const ( const (
userA = "11111111-1111-1111-1111-111111111111" userA = "11111111-1111-1111-1111-111111111111"