fix(test): store tests take an OS-assigned free port, not a PID-derived one
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:
@@ -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) })
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user