From 8871cb8f2b8163d803f8f5ad2c2dc2a3f345bb0b Mon Sep 17 00:00:00 2001 From: Stefan Majewsky Date: Fri, 31 Jul 2026 00:23:28 +0200 Subject: add test coverage --- interface_test.go | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 183 insertions(+) create mode 100644 interface_test.go (limited to 'interface_test.go') diff --git a/interface_test.go b/interface_test.go new file mode 100644 index 0000000..68f1dc2 --- /dev/null +++ b/interface_test.go @@ -0,0 +1,183 @@ +// SPDX-FileCopyrightText: 2026 Stefan Majewsky +// SPDX-License-Identifier: Apache-2.0 + +package gg_pgx_test + +import ( + "context" + "database/sql" + "errors" + "testing" + + "go.xyrillian.de/gg/assert" + "go.xyrillian.de/gg/errext" + "go.xyrillian.de/gg/gsql" + "go.xyrillian.de/gg/pgruntime" + + gg_pgx "go.xyrillian.de/gg-pgx" +) + +// NOTE: There is not a lot of test coverage for the methods in `interface.go`, which is intentional: +// These are just boilerplate implementations where, if it compiles at all, it is all but guaranteed to work as intended. + +var defaultBehavior pgruntime.ConnectionBehavior + +func TestMain(m *testing.M) { + pgruntime.WithTestDB(m, m.Run) +} + +func TestConn(t *testing.T) { + conn, _ := gg_pgx.SingleConnector().ConnectForTest(t, defaultBehavior) + testConnectionHandle(t, conn) +} + +func TestPool(t *testing.T) { + pool, _ := gg_pgx.PoolConnector().ConnectForTest(t, defaultBehavior) + testConnectionHandle(t, pool) +} + +func TestPoolConn(t *testing.T) { + pool, _ := gg_pgx.PoolConnector().ConnectForTest(t, defaultBehavior) + t.Cleanup(pool.Close) + + conn, err := pool.Acquire(t.Context()) + if assert.ErrEqual(t, err, nil) { + testConnectionHandle(t, conn) + } +} + +func testConnectionHandle[H gsql.ConnectionHandle](t *testing.T, conn H) { + ctx := t.Context() + t.Cleanup(func() { + assert.ErrEqual(t, conn.GSQLClose(ctx), nil) + }) + + // test GSQLPrepare() + Exec() + _, err := execQuery(ctx, conn, `CREATE TABLE people (id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL)`, nil) + if !assert.ErrEqual(t, err, nil) { + t.FailNow() + } + for _, name := range []string{"Alice", "Bob", "Carol"} { + _, err = execQuery(ctx, conn, `INSERT INTO people (name) VALUES ($1)`, []any{name}) + if !assert.ErrEqual(t, err, nil) { + t.FailNow() + } + } + + // test GSQLPrepare() + QueryRow() + var id int64 + err = queryRow(ctx, conn, `SELECT id FROM people WHERE name = $1`, []any{"Bob"}, []any{&id}) + if !assert.ErrEqual(t, err, nil) { + t.FailNow() + } + assert.Equal(t, id, 2) + + // test GSQLQuery() + type person struct { + ID int64 + Name string + } + var people []person + err = foreachRow(t, ctx, conn, `SELECT id, name FROM people ORDER BY id`, nil, []string{"id", "name"}, func(scan func(slots ...any) error) error { + var p person + err := scan(&p.ID, &p.Name) + if err != nil { + return err + } + people = append(people, p) + return nil + }) + if !assert.ErrEqual(t, err, nil) { + t.FailNow() + } + assert.Equal(t, people, []person{ + {1, "Alice"}, + {2, "Bob"}, + {3, "Carol"}, + }) + + // test GSQLTransact() + success + err = conn.GSQLTransact(ctx, func(tx gsql.Handle) error { + _, err := execQuery(ctx, tx, `UPDATE people SET name = $1 WHERE id = $2`, []any{"Carolin", 3}) + return err + }) + if !assert.ErrEqual(t, err, nil) { + t.FailNow() + } + + // check that the transaction was committed + var name string + err = queryRow(ctx, conn, `SELECT name FROM people WHERE id = $1`, []any{3}, []any{&name}) + if !assert.ErrEqual(t, err, nil) { + t.FailNow() + } + assert.Equal(t, name, "Carolin") + + // test GSQLTransact() + error + err = conn.GSQLTransact(ctx, func(tx gsql.Handle) error { + _, err := execQuery(ctx, tx, `UPDATE people SET name = $1 WHERE id = $2`, []any{"Allen", 1}) + if err != nil { + return err + } + + // check that the change is visible within the transaction + var name string + err = queryRow(ctx, tx, `SELECT name FROM people WHERE id = $1`, []any{1}, []any{&name}) + if !assert.ErrEqual(t, err, nil) { + t.FailNow() + } + assert.Equal(t, name, "Allen") + + return errors.New("this was a bad idea") + }) + if !assert.ErrEqual(t, err, "this was a bad idea") { + t.FailNow() + } + + // check that the transaction was *not* committed + err = queryRow(ctx, conn, `SELECT name FROM people WHERE id = $1`, []any{1}, []any{&name}) + if !assert.ErrEqual(t, err, nil) { + t.FailNow() + } + assert.Equal(t, name, "Alice") +} + +// Convenience function for executing a one-off SQL query returning no rows. +func execQuery(ctx context.Context, db gsql.Handle, query string, args []any) (sql.Result, error) { + stmt, err := db.GSQLPrepare(ctx, query, false) + if err != nil { + return nil, err + } + result, err := stmt.Exec(ctx, args) + return result, errext.WithCleanup(err, "stmt.Close", stmt.Close()) +} + +// Convenience function for executing a one-off SQL query returning one row. +func queryRow(ctx context.Context, db gsql.Handle, query string, args, slots []any) error { + stmt, err := db.GSQLPrepare(ctx, query, true) // repeated=true does not technically make sense, but provides code coverage + if err != nil { + return err + } + err = stmt.QueryRow(ctx, args, slots) + return errext.WithCleanup(err, "stmt.Close", stmt.Close()) +} + +// Convenience function for executing a one-off SQL query returning several rows. +func foreachRow(t *testing.T, ctx context.Context, db gsql.Handle, query string, args []any, expectedColumnNames []string, action func(scan func(slots ...any) error) error) error { + t.Helper() + rows, err := db.GSQLQuery(ctx, query, args) + if err != nil { + return err + } + columns, err := rows.Columns() + if assert.ErrEqual(t, err, nil) { + assert.Equal(t, columns, expectedColumnNames) + } + for rows.Next() { + err := action(rows.Scan) + if err != nil { + return errext.WithCleanup(err, "rows.Close", rows.Close()) + } + } + return rows.Close() +} -- cgit v1.3.1