diff --git a/Makefile b/Makefile index 823c348..6de0199 100644 --- a/Makefile +++ b/Makefile @@ -29,7 +29,7 @@ psql: .PHONY: test test: - go test ./... + go run ./internal/pgdocker/cmd/pgdocker -- go test ./... .PHONY: acceptance-test acceptance-test: diff --git a/internal/pgdocker/cmd/pgdocker/main.go b/internal/pgdocker/cmd/pgdocker/main.go new file mode 100644 index 0000000..765acec --- /dev/null +++ b/internal/pgdocker/cmd/pgdocker/main.go @@ -0,0 +1,59 @@ +// pgdocker starts a Postgres Docker container and exposes it to a wrapped +// command through the PGURL environment variable. +package main + +import ( + "context" + "errors" + "fmt" + "log" + "os" + "os/exec" + "time" + + "github.com/jschaf/pggen/internal/errs" + "github.com/jschaf/pggen/internal/pgdocker" +) + +func main() { + if err := run(); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) { + fmt.Fprintln(os.Stderr, err) + os.Exit(exitErr.ExitCode()) + } + log.Fatal(err) + } +} + +func run() (mErr error) { + if len(os.Args) < 3 || os.Args[1] != "--" { + return fmt.Errorf("usage: %s -- [args...]", os.Args[0]) + } + + ctx := context.Background() + docker, err := pgdocker.Start(ctx, nil) + if err != nil { + return fmt.Errorf("start postgres: %w", err) + } + defer errs.Capture(&mErr, func() error { + stopCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + return docker.Stop(stopCtx) + }, "stop postgres") + + connStr, err := docker.ConnString() + if err != nil { + return fmt.Errorf("postgres connection string: %w", err) + } + + wrapped := exec.Command(os.Args[2], os.Args[3:]...) //nolint:gosec // Runs the command explicitly supplied after --. + wrapped.Stdin = os.Stdin + wrapped.Stdout = os.Stdout + wrapped.Stderr = os.Stderr + wrapped.Env = append(os.Environ(), "PGURL="+connStr) + if err := wrapped.Run(); err != nil { + return fmt.Errorf("%v: %w", wrapped.Args, err) + } + return nil +} diff --git a/internal/pgtest/pg_test_db.go b/internal/pgtest/pg_test_db.go index 1ee8388..42e4172 100644 --- a/internal/pgtest/pg_test_db.go +++ b/internal/pgtest/pg_test_db.go @@ -3,6 +3,7 @@ package pgtest import ( "context" "math/rand/v2" + "net/url" "os" "strconv" "strings" @@ -22,7 +23,7 @@ type Option func(config *pgx.ConnConfig) func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Conn, CleanupFunc) { t.Helper() // Create a new schema. - connStr := "user=postgres password=hunter2 host=localhost port=5555 dbname=pggen" + connStr := postgresConnString() ctx, cancel := context.WithTimeout(t.Context(), 10*time.Second) defer cancel() conn, err := pgx.Connect(ctx, connStr) @@ -36,10 +37,10 @@ func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Con t.Logf("created schema: %s", schema) // Load SQL files into new schema. - connStr += " search_path=" + schema - connCfg, err := pgx.ParseConfig(connStr) + schemaConnStr := postgresSchemaConnString(connStr, schema) + connCfg, err := pgx.ParseConfig(schemaConnStr) if err != nil { - t.Fatalf("parse config: %q: %s", connStr, err) + t.Fatalf("parse config: %q: %s", schemaConnStr, err) } for _, opt := range opts { opt(connCfg) @@ -69,6 +70,23 @@ func NewPostgresSchemaString(t *testing.T, sql string, opts ...Option) (*pgx.Con return schemaConn, cleanup } +func postgresConnString() string { + if connStr := os.Getenv("PGURL"); connStr != "" { + return connStr + } + return "user=postgres password=hunter2 host=localhost port=5555 dbname=pggen" +} + +func postgresSchemaConnString(connStr string, schema string) string { + if u, err := url.Parse(connStr); err == nil && (u.Scheme == "postgres" || u.Scheme == "postgresql") { + q := u.Query() + q.Set("search_path", schema) + u.RawQuery = q.Encode() + return u.String() + } + return connStr + " search_path=" + schema +} + // NewPostgresSchema opens a connection with search_path set to a randomly // named, new schema and loads all sqlFiles. func NewPostgresSchema(t *testing.T, sqlFiles []string, opts ...Option) (*pgx.Conn, CleanupFunc) {