Files
dolthub--doltgresql/testing/generation/function_coverage/output/framework_test.go
T
2026-07-13 12:32:25 +08:00

323 lines
10 KiB
Go

// Copyright 2024 Dolthub, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package output
import (
"context"
"errors"
"fmt"
"math"
"os"
"reflect"
"strconv"
"testing"
"time"
"github.com/cockroachdb/apd/v3"
"github.com/dolthub/dolt/go/libraries/utils/svcs"
"github.com/dolthub/go-mysql-server/sql"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
dserver "github.com/dolthub/doltgresql/server"
pgtypes "github.com/dolthub/doltgresql/server/types"
"github.com/dolthub/doltgresql/servercfg"
"github.com/dolthub/doltgresql/servercfg/cfgdetails"
"github.com/dolthub/doltgresql/utils"
)
var (
// Inf represent positive infinity as a float64
Inf = math.Inf(0)
// EquivalenceThresholdFloat64 represents the allowable delta for values to be considered equivalent.
// For now, we do not expect all non-integer values to be exactly equivalent due to small computational differences
// between Go and C/C++, so this threshold allows us to check that non-integer values are close enough. Over time,
// we may reduce or remove this value as we become more accurate.
EquivalenceThresholdFloat64 = 0.001
)
// ScriptTest defines a consistent structure for testing queries.
type ScriptTest struct {
// Name of the script.
Name string
// The database to create and use. If not provided, then it defaults to "postgres".
Database string
// The SQL statements to execute as setup, in order. Results are not checked, but statements must not error.
SetUpScript []string
// The set of assertions to make after setup, in order
Assertions []ScriptTestAssertion
// When using RunScripts, setting this on one (or more) tests causes RunScripts to ignore all tests that have this
// set to false (which is the default value). This allows a developer to easily "focus" on a specific test without
// having to comment out other tests, pull it into a different function, etc. In addition, CI ensures that this is
// false before passing, meaning this prevents the commented-out situation where the developer forgets to uncomment
// their code.
Focus bool
// Skip is used to completely skip a test including setup
Skip bool
}
// ScriptTestAssertion are the assertions upon which the script executes its main "testing" logic.
type ScriptTestAssertion struct {
Query string
Expected []sql.Row
ExpectedErr bool
// Skip is used to completely skip a test, not execute its query at all, and record it as a skipped test
// in the test suite results.
Skip bool
}
// RunScript runs the given script.
func RunScript(t *testing.T, script ScriptTest) {
scriptDatabase := script.Database
if len(scriptDatabase) == 0 {
scriptDatabase = "postgres"
}
ctx, conn, controller := CreateServer(t, scriptDatabase)
defer func() {
conn.Close(ctx)
controller.Stop()
err := controller.WaitForStop()
require.NoError(t, err)
}()
t.Run(script.Name, func(t *testing.T) {
runScript(t, script, conn, ctx)
})
}
// runScript runs the script given on the postgres connection provided
func runScript(t *testing.T, script ScriptTest, conn *pgx.Conn, ctx context.Context) {
if script.Skip {
t.Skip("Skip has been set in the script")
}
// Run the setup
for _, query := range script.SetUpScript {
_, err := conn.Exec(ctx, query)
require.NoError(t, err)
}
// Run the assertions
for _, assertion := range script.Assertions {
t.Run(assertion.Query, func(t *testing.T) {
if assertion.Skip {
t.Skip("Skip has been set in the assertion")
}
// If we're skipping the results check, then we call Execute, as it uses a simplified message model.
if assertion.ExpectedErr {
_, err := conn.Exec(ctx, assertion.Query)
if assertion.ExpectedErr {
require.Error(t, err)
} else {
require.NoError(t, err)
}
} else {
rows, err := conn.Query(ctx, assertion.Query)
require.NoError(t, err)
readRows, _, err := ReadRows(rows)
require.NoError(t, err)
if !CompareResults(t, assertion.Expected, readRows) {
// We know it will fail, but this will print a prettier message
assert.Equal(t, assertion.Expected, readRows)
}
}
})
}
}
// RunScripts runs the given collection of scripts.
func RunScripts(t *testing.T, scripts []ScriptTest) {
// First, we'll run through the scripts to check for the Focus variable. If it's true, then append it to the new slice.
focusScripts := make([]ScriptTest, 0, len(scripts))
for _, script := range scripts {
if script.Focus {
// If this is running in GitHub Actions, then we'll panic, because someone forgot to disable it before committing
if _, ok := os.LookupEnv("GITHUB_ACTION"); ok {
panic(fmt.Sprintf("The script `%s` has Focus set to `true`. GitHub Actions requires that "+
"all tests are run, which Focus circumvents, leading to this error. Please disable Focus on "+
"all tests.", script.Name))
}
focusScripts = append(focusScripts, script)
}
}
// If we have scripts with Focus set, then we replace the normal script slice with the new slice.
if len(focusScripts) > 0 {
scripts = focusScripts
}
for _, script := range scripts {
RunScript(t, script)
}
}
func ptr[T any](val T) *T {
return &val
}
// CreateServer creates a server with the given database, returning a connection to the server. The server will close
// when the connection is closed (or loses its connection to the server). The accompanying WaitGroup may be used to wait
// until the server has closed.
func CreateServer(t *testing.T, database string) (context.Context, *pgx.Conn, *svcs.Controller) {
require.NotEmpty(t, database)
port, err := sql.GetEmptyPort()
require.NoError(t, err)
controller, err := dserver.RunInMemory(&servercfg.DoltgresConfig{
DoltgresConfig: cfgdetails.DoltgresConfig{
ListenerConfig: &cfgdetails.DoltgresListenerConfig{
PortNumber: &port,
HostStr: ptr("127.0.0.1"),
},
},
}, dserver.NewListener)
require.NoError(t, err)
ctx := context.Background()
err = func() error {
// The connection attempt may be made before the server has grabbed the port, so we'll retry the first
// connection a few times.
var conn *pgx.Conn
var err error
for i := 0; i < 3; i++ {
conn, err = pgx.Connect(ctx, fmt.Sprintf("postgres://postgres:password@127.0.0.1:%d/", port))
if err == nil {
break
} else {
time.Sleep(time.Second)
}
}
if err != nil {
return err
}
defer conn.Close(ctx)
if database != "postgres" {
_, err = conn.Exec(ctx, fmt.Sprintf("CREATE DATABASE %s;", database))
return err
}
return nil
}()
require.NoError(t, err)
conn, err := pgx.Connect(ctx, fmt.Sprintf("postgres://postgres:password@127.0.0.1:%d/%s", port, database))
require.NoError(t, err)
return ctx, conn, controller
}
// ReadRows reads all of the given rows into a slice, then closes the rows.
func ReadRows(rows pgx.Rows) (readRows []sql.Row, oids []uint32, err error) {
for _, desc := range rows.FieldDescriptions() {
oids = append(oids, desc.DataTypeOID)
}
defer func() {
err = errors.Join(err, rows.Err())
}()
for rows.Next() {
row, err := rows.Values()
if err != nil {
return nil, nil, err
}
readRows = append(readRows, row)
}
return readRows, oids, nil
}
// Numeric creates a numeric value from a string.
func Numeric(str string) pgtype.Numeric {
numeric := pgtype.Numeric{}
if err := numeric.Scan(str); err != nil {
panic(err)
}
return numeric
}
// NumericToDecimal converts a pgtype.Numeric value to a apd.Decimal value.
func NumericToDecimal(val pgtype.Numeric) *apd.Decimal {
if val.NaN {
return pgtypes.NumericNaN
} else if val.InfinityModifier == pgtype.Infinity {
return pgtypes.NumericInf
} else if val.InfinityModifier == pgtype.NegativeInfinity {
return pgtypes.NumericNegInf
}
return apd.New(val.Int.Int64(), val.Exp)
}
// CompareResults compares two sets of results, taking the equivalence thresholds into account when making the
// comparisons.
func CompareResults(t *testing.T, a []sql.Row, b []sql.Row) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if !CompareRows(t, a[i], b[i]) {
return false
}
}
return true
}
// CompareRows compares two rows, taking the equivalence thresholds into account when making the comparisons.
func CompareRows(t *testing.T, a sql.Row, b sql.Row) bool {
if len(a) != len(b) {
return false
}
for i := range a {
aVal := a[i]
bVal := b[i]
if aVal != bVal {
if reflect.TypeOf(aVal) != reflect.TypeOf(bVal) {
return false
}
switch aVal.(type) {
case float32:
if utils.Abs(aVal.(float32)-bVal.(float32)) > float32(EquivalenceThresholdFloat64) {
return false
}
case float64:
if utils.Abs(aVal.(float64)-bVal.(float64)) > EquivalenceThresholdFloat64 {
return false
}
case pgtype.Numeric:
aDec := NumericToDecimal(aVal.(pgtype.Numeric))
bDec := NumericToDecimal(bVal.(pgtype.Numeric))
p := aDec.NumDigits()
if p2 := bDec.NumDigits(); p < p2 {
p = p2
}
res := new(apd.Decimal)
_, err := sql.DecimalCtx.WithPrecision(uint32(p)).Sub(res, aDec, bDec)
if err != nil {
return false
}
// EquivalenceThresholdNumeric represents the allowable delta for values to be considered equivalent.
// This is computed using the float64 variant so that they're equivalent.
EquivalenceThresholdNumeric, _, _ := apd.NewFromString(strconv.FormatFloat(EquivalenceThresholdFloat64, 'f', -1, 64))
if res.Abs(res).Cmp(EquivalenceThresholdNumeric) == 1 {
return false
}
default:
return false
}
t.Log("values are not equal but fall within equivalence threshold")
}
}
return true
}