pgx/pgxtest/pgxtest.go

174 lines
5.0 KiB
Go

// Package pgxtest provides utilities for testing pgx and packages that integrate with pgx.
package pgxtest
import (
"context"
"fmt"
"reflect"
"regexp"
"strconv"
"testing"
"github.com/jackc/pgx/v5"
)
var AllQueryExecModes = []pgx.QueryExecMode{
pgx.QueryExecModeCacheStatement,
pgx.QueryExecModeCacheDescribe,
pgx.QueryExecModeDescribeExec,
pgx.QueryExecModeExec,
pgx.QueryExecModeSimpleProtocol,
}
// KnownOIDQueryExecModes is a slice of all query exec modes where the param and result OIDs are known before sending the query.
var KnownOIDQueryExecModes = []pgx.QueryExecMode{
pgx.QueryExecModeCacheStatement,
pgx.QueryExecModeCacheDescribe,
pgx.QueryExecModeDescribeExec,
}
// ConnTestRunner controls how a *pgx.Conn is created and closed by tests. All fields are required. Use DefaultConnTestRunner to get a
// ConnTestRunner with reasonable default values.
type ConnTestRunner struct {
// CreateConfig returns a *pgx.ConnConfig suitable for use with pgx.ConnectConfig.
CreateConfig func(ctx context.Context, t testing.TB) *pgx.ConnConfig
// AfterConnect is called after conn is established. It allows for arbitrary connection setup before a test begins.
AfterConnect func(ctx context.Context, t testing.TB, conn *pgx.Conn)
// AfterTest is called after the test is run. It allows for validating the state of the connection before it is closed.
AfterTest func(ctx context.Context, t testing.TB, conn *pgx.Conn)
// CloseConn closes conn.
CloseConn func(ctx context.Context, t testing.TB, conn *pgx.Conn)
}
// DefaultConnTestRunner returns a new ConnTestRunner with all fields set to reasonable default values.
func DefaultConnTestRunner() ConnTestRunner {
return ConnTestRunner{
CreateConfig: func(ctx context.Context, t testing.TB) *pgx.ConnConfig {
config, err := pgx.ParseConfig("")
if err != nil {
t.Fatalf("ParseConfig failed: %v", err)
}
return config
},
AfterConnect: func(ctx context.Context, t testing.TB, conn *pgx.Conn) {},
AfterTest: func(ctx context.Context, t testing.TB, conn *pgx.Conn) {},
CloseConn: func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
err := conn.Close(ctx)
if err != nil {
t.Errorf("Close failed: %v", err)
}
},
}
}
func (ctr *ConnTestRunner) RunTest(ctx context.Context, t testing.TB, f func(ctx context.Context, t testing.TB, conn *pgx.Conn)) {
t.Helper()
config := ctr.CreateConfig(ctx, t)
conn, err := pgx.ConnectConfig(ctx, config)
if err != nil {
t.Fatalf("ConnectConfig failed: %v", err)
}
defer ctr.CloseConn(ctx, t, conn)
ctr.AfterConnect(ctx, t, conn)
f(ctx, t, conn)
ctr.AfterTest(ctx, t, conn)
}
// RunWithQueryExecModes runs a f in a new test for each element of modes with a new connection created using connector.
// If modes is nil all pgx.QueryExecModes are tested.
func RunWithQueryExecModes(ctx context.Context, t *testing.T, ctr ConnTestRunner, modes []pgx.QueryExecMode, f func(ctx context.Context, t testing.TB, conn *pgx.Conn)) {
if modes == nil {
modes = AllQueryExecModes
}
for _, mode := range modes {
ctrWithMode := ctr
ctrWithMode.CreateConfig = func(ctx context.Context, t testing.TB) *pgx.ConnConfig {
config := ctr.CreateConfig(ctx, t)
config.DefaultQueryExecMode = mode
return config
}
t.Run(mode.String(),
func(t *testing.T) {
ctrWithMode.RunTest(ctx, t, f)
},
)
}
}
type ValueRoundTripTest struct {
Param any
Result any
Test func(any) bool
}
func RunValueRoundTripTests(
ctx context.Context,
t testing.TB,
ctr ConnTestRunner,
modes []pgx.QueryExecMode,
pgTypeName string,
tests []ValueRoundTripTest,
) {
t.Helper()
if modes == nil {
modes = AllQueryExecModes
}
ctr.RunTest(ctx, t, func(ctx context.Context, t testing.TB, conn *pgx.Conn) {
t.Helper()
sql := fmt.Sprintf("select $1::%s", pgTypeName)
for i, tt := range tests {
for _, mode := range modes {
err := conn.QueryRow(ctx, sql, mode, tt.Param).Scan(tt.Result)
if err != nil {
t.Errorf("%d. %v: %v", i, mode, err)
}
result := reflect.ValueOf(tt.Result)
if result.Kind() == reflect.Ptr {
result = result.Elem()
}
if !tt.Test(result.Interface()) {
t.Errorf("%d. %v: unexpected result for %v: %v", i, mode, tt.Param, result.Interface())
}
}
}
})
}
// SkipCockroachDB calls Skip on t with msg if the connection is to a CockroachDB server.
func SkipCockroachDB(t testing.TB, conn *pgx.Conn, msg string) {
if conn.PgConn().ParameterStatus("crdb_version") != "" {
t.Skip(msg)
}
}
func SkipPostgreSQLVersionLessThan(t testing.TB, conn *pgx.Conn, minVersion int64) {
serverVersionStr := conn.PgConn().ParameterStatus("server_version")
serverVersionStr = regexp.MustCompile(`^[0-9]+`).FindString(serverVersionStr)
// if not PostgreSQL do nothing
if serverVersionStr == "" {
return
}
serverVersion, err := strconv.ParseInt(serverVersionStr, 10, 64)
if err != nil {
t.Fatalf("postgres version parsed failed: %s", err)
}
if serverVersion < minVersion {
t.Skipf("Test requires PostgreSQL v%d+", minVersion)
}
}