chore: import upstream snapshot with attribution
This commit is contained in:
+272
@@ -0,0 +1,272 @@
|
||||
// Copyright 2023 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 harness
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dolthub/sqllogictest/go/logictest"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
// sqllogictest harness for postgres databases.
|
||||
type PostgresqlServerHarness struct {
|
||||
dsn string
|
||||
db *sql.DB
|
||||
timeout int64
|
||||
}
|
||||
|
||||
// compile check for interface compliance
|
||||
var _ logictest.Harness = &PostgresqlServerHarness{}
|
||||
|
||||
// NewPostgresqlHarness returns a new Postgres test harness for the data source name given. Panics if it cannot open a
|
||||
// connection using the DSN.
|
||||
func NewPostgresqlHarness(dsn string, t int64) *PostgresqlServerHarness {
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return &PostgresqlServerHarness{
|
||||
dsn: dsn,
|
||||
db: db,
|
||||
timeout: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *PostgresqlServerHarness) EngineStr() string {
|
||||
return "postgresql"
|
||||
}
|
||||
|
||||
func (h *PostgresqlServerHarness) Init() error {
|
||||
if err := h.dropAllTables(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.dropAllViews()
|
||||
}
|
||||
|
||||
// See Harness.ExecuteStatement
|
||||
func (h *PostgresqlServerHarness) ExecuteStatement(statement string) error {
|
||||
_, err := h.db.Exec(statement)
|
||||
return err
|
||||
}
|
||||
|
||||
// See Harness.ExecuteQuery
|
||||
func (h *PostgresqlServerHarness) ExecuteQuery(statement string) (schema string, results []string, err error) {
|
||||
rows, err := h.db.Query(statement)
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return h.getSchemaAndResults(rows)
|
||||
}
|
||||
|
||||
func (h *PostgresqlServerHarness) ExecuteStatementContext(ctx context.Context, statement string) error {
|
||||
_, err := h.db.ExecContext(ctx, statement)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *PostgresqlServerHarness) ExecuteQueryContext(ctx context.Context, statement string) (schema string, results []string, err error) {
|
||||
rows, err := h.db.QueryContext(ctx, statement)
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return h.getSchemaAndResults(rows)
|
||||
}
|
||||
|
||||
func (h *PostgresqlServerHarness) getSchemaAndResults(rows *sql.Rows) (schema string, results []string, err error) {
|
||||
schema, columns, err := columns(rows)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
err := rows.Scan(columns...)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
for _, col := range columns {
|
||||
results = append(results, stringVal(col))
|
||||
}
|
||||
}
|
||||
|
||||
if rows.Err() != nil {
|
||||
return "", nil, rows.Err()
|
||||
}
|
||||
|
||||
return schema, results, nil
|
||||
}
|
||||
|
||||
func (h *PostgresqlServerHarness) GetTimeout() int64 {
|
||||
return h.timeout
|
||||
}
|
||||
|
||||
func (h *PostgresqlServerHarness) dropAllTables() error {
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
rows, err = h.db.QueryContext(context.Background(), "SELECT table_name FROM information_schema.tables WHERE table_schema = 'sqllogictest' AND table_type = 'BASE TABLE';")
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, columns, err := columns(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var tableNames []string
|
||||
for rows.Next() {
|
||||
err := rows.Scan(columns...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tableName := columns[0].(*sql.NullString)
|
||||
tableNames = append(tableNames, tableName.String)
|
||||
}
|
||||
|
||||
if len(tableNames) > 0 {
|
||||
dropTables := "drop table if exists " + strings.Join(tableNames, ",")
|
||||
_, err = h.db.Exec(dropTables)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *PostgresqlServerHarness) dropAllViews() error {
|
||||
rows, err := h.db.QueryContext(context.Background(), "select table_name from INFORMATION_SCHEMA.views")
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, columns, err := columns(rows)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var viewNames []string
|
||||
for rows.Next() {
|
||||
err := rows.Scan(columns...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
viewName := columns[0].(*sql.NullString)
|
||||
viewNames = append(viewNames, viewName.String)
|
||||
}
|
||||
|
||||
if len(viewNames) > 0 {
|
||||
dropView := "drop view if exists " + strings.Join(viewNames, ",")
|
||||
_, err = h.db.Exec(dropView)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Returns the string representation of the column value given
|
||||
func stringVal(col interface{}) string {
|
||||
switch v := col.(type) {
|
||||
case *sql.NullBool:
|
||||
if !v.Valid {
|
||||
return "NULL"
|
||||
}
|
||||
if v.Bool {
|
||||
return "1"
|
||||
} else {
|
||||
return "0"
|
||||
}
|
||||
case *sql.NullInt64:
|
||||
if !v.Valid {
|
||||
return "NULL"
|
||||
}
|
||||
return fmt.Sprintf("%d", v.Int64)
|
||||
case *sql.NullFloat64:
|
||||
if !v.Valid {
|
||||
return "NULL"
|
||||
}
|
||||
return fmt.Sprintf("%.3f", v.Float64)
|
||||
case *sql.NullString:
|
||||
if !v.Valid {
|
||||
return "NULL"
|
||||
}
|
||||
return v.String
|
||||
default:
|
||||
panic(fmt.Sprintf("unhandled type %T for value %v", v, v))
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the schema for the rows given, as well as a slice of columns suitable for scanning values into.
|
||||
func columns(rows *sql.Rows) (string, []interface{}, error) {
|
||||
types, err := rows.ColumnTypes()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
sb := strings.Builder{}
|
||||
var columns []interface{}
|
||||
for _, columnType := range types {
|
||||
switch columnType.DatabaseTypeName() {
|
||||
case "BIT", "BOOL":
|
||||
colVal := sql.NullBool{}
|
||||
columns = append(columns, &colVal)
|
||||
sb.WriteString("I")
|
||||
case "TEXT", "VARCHAR", "MEDIUMTEXT", "CHAR", "TINYTEXT", "NAME", "BYTEA":
|
||||
colVal := sql.NullString{}
|
||||
columns = append(columns, &colVal)
|
||||
sb.WriteString("T")
|
||||
case "DECIMAL", "DOUBLE", "FLOAT", "FLOAT4", "FLOAT8", "NUMERIC":
|
||||
colVal := sql.NullFloat64{}
|
||||
columns = append(columns, &colVal)
|
||||
sb.WriteString("R")
|
||||
case "MEDIUMINT", "INT", "BIGINT", "TINYINT", "SMALLINT", "INT2", "INT4", "INT8":
|
||||
colVal := sql.NullInt64{}
|
||||
columns = append(columns, &colVal)
|
||||
sb.WriteString("I")
|
||||
case "UNKNOWN": // used for NULL values
|
||||
colVal := sql.NullString{}
|
||||
columns = append(columns, &colVal)
|
||||
sb.WriteString("I") // is this right?
|
||||
default:
|
||||
return "", nil, fmt.Errorf("Unhandled type %s", columnType.DatabaseTypeName())
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String(), columns, nil
|
||||
}
|
||||
@@ -0,0 +1,415 @@
|
||||
// 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 harness
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/dolthub/sqllogictest/go/logictest"
|
||||
_ "github.com/jackc/pgx/v4/stdlib"
|
||||
)
|
||||
|
||||
const (
|
||||
doltgresDBDir = "doltgresDatabases"
|
||||
serverLogFile = "server.log"
|
||||
harnessLogFile = "harness.log"
|
||||
DefaultPort = 5432
|
||||
defaultDbName = "sqllogictest"
|
||||
)
|
||||
|
||||
func noDatabaseDSN(port int) string {
|
||||
return fmt.Sprintf("postgresql://postgres:password@127.0.0.1:%d/?sslmode=disable", port)
|
||||
}
|
||||
|
||||
func withDatabaseDSN(port int, dbName string) string {
|
||||
return fmt.Sprintf("postgresql://postgres:password@127.0.0.1:%d/%s?sslmode=disable", port, dbName)
|
||||
}
|
||||
|
||||
var _ logictest.Harness = &DoltgresHarness{}
|
||||
|
||||
// DoltgresHarness is a sqllogictest harness for doltgres databases.
|
||||
type DoltgresHarness struct {
|
||||
db *sql.DB
|
||||
doltgresExec string
|
||||
server *DoltgresServer
|
||||
serverDir string
|
||||
timeout int64
|
||||
port int
|
||||
dbName string
|
||||
harnessLog *os.File
|
||||
stashedLogOutput io.Writer
|
||||
}
|
||||
|
||||
// NewDoltgresHarness returns a harness that manages its own server lifecycle. The server is
|
||||
// started once at construction; Init resets the database without restarting the process.
|
||||
func NewDoltgresHarness(doltgresExec string, t int64) *DoltgresHarness {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
logErr(err, "getting cwd")
|
||||
}
|
||||
serverDir := filepath.Join(cwd, doltgresDBDir)
|
||||
err = os.RemoveAll(serverDir)
|
||||
if err != nil {
|
||||
logErr(err, fmt.Sprintf("running `RemoveAll` for '%s'", serverDir))
|
||||
}
|
||||
err = os.MkdirAll(serverDir, os.ModePerm)
|
||||
if err != nil {
|
||||
logErr(err, fmt.Sprintf("running `MkdirAll` for '%s'", serverDir))
|
||||
}
|
||||
hl, err := os.OpenFile(harnessLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
stashLogOutput := log.Writer()
|
||||
log.SetOutput(hl)
|
||||
logMsg("creating a new DoltgresHarness")
|
||||
|
||||
h := &DoltgresHarness{
|
||||
doltgresExec: doltgresExec,
|
||||
serverDir: serverDir,
|
||||
timeout: t,
|
||||
port: DefaultPort,
|
||||
dbName: defaultDbName,
|
||||
harnessLog: hl,
|
||||
stashedLogOutput: stashLogOutput,
|
||||
}
|
||||
h.server = startServerProcess(doltgresExec, serverDir)
|
||||
return h
|
||||
}
|
||||
|
||||
// NewDoltgresWorkerHarness returns a harness for use in a concurrent worker. It connects to an
|
||||
// already-running server on the given port using dbName as its isolated database. The caller is
|
||||
// responsible for starting and stopping the server (see StartSharedServer).
|
||||
func NewDoltgresWorkerHarness(port int, dbName string, t int64) *DoltgresHarness {
|
||||
return &DoltgresHarness{
|
||||
timeout: t,
|
||||
port: port,
|
||||
dbName: dbName,
|
||||
}
|
||||
}
|
||||
|
||||
// StartSharedServer starts a single doltgres server process for use by multiple concurrent
|
||||
// workers. The returned *DoltgresServer must be closed when testing is complete.
|
||||
func StartSharedServer(doltgresExec string) *DoltgresServer {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
logErr(err, "getting cwd")
|
||||
}
|
||||
serverDir := filepath.Join(cwd, doltgresDBDir)
|
||||
err = os.RemoveAll(serverDir)
|
||||
if err != nil {
|
||||
logErr(err, fmt.Sprintf("running `RemoveAll` for '%s'", serverDir))
|
||||
}
|
||||
err = os.MkdirAll(serverDir, os.ModePerm)
|
||||
if err != nil {
|
||||
logErr(err, fmt.Sprintf("running `MkdirAll` for '%s'", serverDir))
|
||||
}
|
||||
return startServerProcess(doltgresExec, serverDir)
|
||||
}
|
||||
|
||||
// startServerProcess starts a doltgres subprocess in serverDir and waits for it to be ready.
|
||||
func startServerProcess(doltgresExec string, serverDir string) *DoltgresServer {
|
||||
withKeyCtx, cancel := context.WithCancel(context.Background())
|
||||
gServer, serverCtx := errgroup.WithContext(withKeyCtx)
|
||||
|
||||
server := exec.CommandContext(serverCtx, doltgresExec, "--data-dir=.")
|
||||
server.Dir = serverDir
|
||||
|
||||
l, err := os.OpenFile(serverLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
logErr(err, fmt.Sprintf("opening %s file", serverLogFile))
|
||||
}
|
||||
server.Stdout = l
|
||||
server.Stderr = l
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
<-quit
|
||||
defer wg.Done()
|
||||
signal.Stop(quit)
|
||||
cancel()
|
||||
}()
|
||||
|
||||
ds := &DoltgresServer{
|
||||
dir: serverDir,
|
||||
quit: quit,
|
||||
wg: &wg,
|
||||
gServer: gServer,
|
||||
server: server,
|
||||
serverLog: l,
|
||||
}
|
||||
ds.Start()
|
||||
return ds
|
||||
}
|
||||
|
||||
func (h *DoltgresHarness) EngineStr() string {
|
||||
return "postgresql"
|
||||
}
|
||||
|
||||
// Init resets the harness to a clean state for the next test file.
|
||||
// For worker harnesses (no server process), it keeps a persistent connection and resets the
|
||||
// public schema via DROP+CREATE — avoiding database-level connection races entirely.
|
||||
// For server-owning harnesses, it drops and recreates the database (safe because only one worker
|
||||
// uses each database in the non-concurrent path).
|
||||
func (h *DoltgresHarness) Init() error {
|
||||
if h.server == nil {
|
||||
return h.initWorker()
|
||||
}
|
||||
return h.initServer()
|
||||
}
|
||||
|
||||
// initWorker resets a worker harness between test files.
|
||||
//
|
||||
// When useDropSchema is true (PostgreSQL), it wipes and recreates the public schema on the
|
||||
// persistent connection — no reconnect needed, no races.
|
||||
func (h *DoltgresHarness) initWorker() error {
|
||||
ctx := context.Background()
|
||||
|
||||
if h.db != nil {
|
||||
h.db.Close()
|
||||
h.db = nil
|
||||
}
|
||||
noDB, err := sql.Open("pgx", noDatabaseDSN(h.port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = noDB.ExecContext(ctx, "DROP DATABASE IF EXISTS "+h.dbName)
|
||||
if err == nil {
|
||||
_, err = noDB.ExecContext(ctx, "CREATE DATABASE "+h.dbName)
|
||||
}
|
||||
noDB.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("resetting database %s: %w", h.dbName, err)
|
||||
}
|
||||
return h.openWorkerDB(ctx)
|
||||
}
|
||||
|
||||
// openWorkerDB establishes a connection to the worker's database, creating it if necessary.
|
||||
func (h *DoltgresHarness) openWorkerDB(ctx context.Context) error {
|
||||
db, err := sql.Open("pgx", withDatabaseDSN(h.port, h.dbName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pingErr := db.Ping(); pingErr == nil {
|
||||
h.db = db
|
||||
return nil
|
||||
}
|
||||
db.Close()
|
||||
|
||||
// Database does not exist yet; create it.
|
||||
noDB, err := sql.Open("pgx", noDatabaseDSN(h.port))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, createErr := noDB.ExecContext(ctx, "CREATE DATABASE "+h.dbName)
|
||||
noDB.Close()
|
||||
if createErr != nil {
|
||||
logErr(createErr, "creating database "+h.dbName)
|
||||
return createErr
|
||||
}
|
||||
|
||||
db, err = sql.Open("pgx", withDatabaseDSN(h.port, h.dbName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return err
|
||||
}
|
||||
h.db = db
|
||||
return nil
|
||||
}
|
||||
|
||||
// initServer resets the server-owning harness by dropping and recreating the database.
|
||||
// This is only used in the non-concurrent (single-worker) path.
|
||||
func (h *DoltgresHarness) initServer() error {
|
||||
ctx := context.Background()
|
||||
|
||||
if h.db != nil {
|
||||
h.db.Close()
|
||||
h.db = nil
|
||||
}
|
||||
|
||||
noDB, openErr := sql.Open("pgx", noDatabaseDSN(h.port))
|
||||
if openErr != nil {
|
||||
logErr(openErr, "opening no-db connection")
|
||||
return openErr
|
||||
}
|
||||
_, err := noDB.ExecContext(ctx, "DROP DATABASE IF EXISTS "+h.dbName)
|
||||
if err == nil {
|
||||
_, err = noDB.ExecContext(ctx, "CREATE DATABASE "+h.dbName)
|
||||
}
|
||||
noDB.Close()
|
||||
if err != nil {
|
||||
logErr(err, "resetting database "+h.dbName)
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := sql.Open("pgx", withDatabaseDSN(h.port, h.dbName))
|
||||
if err != nil {
|
||||
logErr(err, "opening connection to "+h.dbName)
|
||||
return err
|
||||
}
|
||||
if err = db.Ping(); err != nil {
|
||||
db.Close()
|
||||
return err
|
||||
}
|
||||
h.db = db
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *DoltgresHarness) Close() error {
|
||||
if h.db != nil {
|
||||
h.db.Close()
|
||||
h.db = nil
|
||||
}
|
||||
if h.server != nil {
|
||||
h.server.Close()
|
||||
h.server = nil
|
||||
}
|
||||
if h.harnessLog != nil {
|
||||
h.harnessLog.Close()
|
||||
log.SetOutput(h.stashedLogOutput)
|
||||
}
|
||||
return os.RemoveAll(h.serverDir)
|
||||
}
|
||||
|
||||
func (h *DoltgresHarness) ExecuteStatement(statement string) error {
|
||||
_, err := h.db.Exec(statement)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *DoltgresHarness) ExecuteQuery(statement string) (schema string, results []string, err error) {
|
||||
rows, err := h.db.Query(statement)
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return h.getSchemaAndResults(rows)
|
||||
}
|
||||
|
||||
func (h *DoltgresHarness) ExecuteStatementContext(ctx context.Context, statement string) error {
|
||||
_, err := h.db.ExecContext(ctx, statement)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *DoltgresHarness) ExecuteQueryContext(ctx context.Context, statement string) (schema string, results []string, err error) {
|
||||
rows, err := h.db.QueryContext(ctx, statement)
|
||||
if rows != nil {
|
||||
defer rows.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return h.getSchemaAndResults(rows)
|
||||
}
|
||||
|
||||
func (h *DoltgresHarness) GetTimeout() int64 {
|
||||
return h.timeout
|
||||
}
|
||||
|
||||
func (h *DoltgresHarness) getSchemaAndResults(rows *sql.Rows) (schema string, results []string, err error) {
|
||||
schema, cols, err := columns(rows)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
for rows.Next() {
|
||||
if err = rows.Scan(cols...); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
for _, col := range cols {
|
||||
results = append(results, stringVal(col))
|
||||
}
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
return "", nil, rows.Err()
|
||||
}
|
||||
return schema, results, nil
|
||||
}
|
||||
|
||||
type DoltgresServer struct {
|
||||
dir string
|
||||
quit chan os.Signal
|
||||
wg *sync.WaitGroup
|
||||
gServer *errgroup.Group
|
||||
server *exec.Cmd
|
||||
serverLog *os.File
|
||||
}
|
||||
|
||||
func (s *DoltgresServer) Start() {
|
||||
logMsg("starting doltgres server")
|
||||
var err error
|
||||
s.gServer.Go(func() error {
|
||||
err = s.server.Run()
|
||||
return err
|
||||
})
|
||||
// Allow the server time to start accepting connections.
|
||||
time.Sleep(3 * time.Second)
|
||||
if err != nil {
|
||||
logErr(err, "from server.Start()")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DoltgresServer) Stop() {
|
||||
select {
|
||||
case <-s.quit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
s.quit <- syscall.SIGTERM
|
||||
err := s.gServer.Wait()
|
||||
if err != nil {
|
||||
if err.Error() == "signal: killed" {
|
||||
logMsg("doltgres server stopped successfully")
|
||||
} else {
|
||||
logErr(err, "from server.Stop()")
|
||||
}
|
||||
}
|
||||
close(s.quit)
|
||||
s.wg.Wait()
|
||||
}
|
||||
|
||||
func (s *DoltgresServer) Close() {
|
||||
s.Stop()
|
||||
if err := s.serverLog.Close(); err != nil {
|
||||
logErr(err, "closing server.log")
|
||||
}
|
||||
}
|
||||
|
||||
func logErr(err error, cause string) {
|
||||
log.Printf("ERROR: %s received from %s", err.Error(), cause)
|
||||
}
|
||||
|
||||
func logMsg(msg string) {
|
||||
log.Println(msg)
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
// Copyright 2023 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 main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/dolthub/sqllogictest/go/logictest"
|
||||
|
||||
"github.com/dolthub/doltgresql/testing/logictest/harness"
|
||||
)
|
||||
|
||||
var resultFormat = flag.String("r", "json", "format of parsed results")
|
||||
var manageServer = flag.Bool("server", false, "whether the runner should launch the server process")
|
||||
var doltgres = flag.String("doltgres", "", "local doltgres binary")
|
||||
var timeout = flag.Int64("timeout", 0, "set a timeout (in seconds) for each test query")
|
||||
var workers = flag.Int("workers", 1, "number of parallel workers")
|
||||
|
||||
// Runs all sqllogictest test files (or directories containing them) given as arguments.
|
||||
// Usage: $command (run|parse) [version] [file1.test dir1/ dir2/]
|
||||
// In run mode, runs the tests and prints results to stdout.
|
||||
// In parse mode, parses test results from the file given and prints them to STDOUT in a format to be imported by dolt.
|
||||
func main() {
|
||||
flag.Parse()
|
||||
args := flag.Args()
|
||||
|
||||
if len(args) < 1 {
|
||||
panic("Usage: logictest (run|parse) [version] file1 file2 ...")
|
||||
}
|
||||
|
||||
if args[0] == "run" {
|
||||
if *manageServer {
|
||||
if *doltgres == "" {
|
||||
log.Fatal("Must supply --doltgres=<path to doltgres> with --server=true")
|
||||
}
|
||||
if *workers > 1 {
|
||||
srv := harness.StartSharedServer(*doltgres)
|
||||
defer srv.Close()
|
||||
runConcurrent(*workers, *timeout, args[1:]...)
|
||||
} else {
|
||||
dh := harness.NewDoltgresHarness(*doltgres, *timeout)
|
||||
defer dh.Close()
|
||||
logictest.RunTestFiles(dh, args[1:]...)
|
||||
}
|
||||
} else {
|
||||
if *workers > 1 {
|
||||
runConcurrent(*workers, *timeout, args[1:]...)
|
||||
} else {
|
||||
h := harness.NewPostgresqlHarness("postgresql://postgres:password@localhost:5432/sqllogictest?sslmode=disable", *timeout)
|
||||
logictest.RunTestFiles(h, args[1:]...)
|
||||
}
|
||||
}
|
||||
} else if args[0] == "parse" {
|
||||
if len(args) < 3 {
|
||||
panic("Usage: logictest [-r(csv|json)] parse <version> (file | dir/)")
|
||||
}
|
||||
parseTestResults(args[1], args[2])
|
||||
} else {
|
||||
panic("Unrecognized command " + args[0])
|
||||
}
|
||||
}
|
||||
|
||||
// runConcurrent fans out |numWorkers| goroutines over the test files specified in |paths|.
|
||||
// Each worker gets its own isolated database (sqllogictest_1, sqllogictest_2, …).
|
||||
func runConcurrent(numWorkers int, queryTimeout int64, paths ...string) {
|
||||
var workerIdx int32
|
||||
logictest.RunTestFilesConcurrently(func() logictest.Harness {
|
||||
n := atomic.AddInt32(&workerIdx, 1)
|
||||
dbName := fmt.Sprintf("sqllogictest_%d", n)
|
||||
return harness.NewDoltgresWorkerHarness(harness.DefaultPort, dbName, queryTimeout)
|
||||
}, numWorkers, paths...)
|
||||
}
|
||||
|
||||
func parseTestResults(version, f string) {
|
||||
entries, err := logictest.ParseResultFile(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
records := make([]*DoltResultRecord, len(entries))
|
||||
for i, e := range entries {
|
||||
records[i] = NewDoltRecordResult(e, version)
|
||||
}
|
||||
|
||||
if *resultFormat == "csv" {
|
||||
err := writeResultsCsv(records)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
b, err := JSONMarshal(records)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
_, err = os.Stdout.Write(b)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Custom json marshalling function is necessary to avoid escaping <, > and & to html unicode escapes
|
||||
func JSONMarshal(records []*DoltResultRecord) ([]byte, error) {
|
||||
rows := &TestResultArray{Rows: records}
|
||||
buffer := &bytes.Buffer{}
|
||||
encoder := json.NewEncoder(buffer)
|
||||
encoder.SetEscapeHTML(false)
|
||||
err := encoder.Encode(rows)
|
||||
return buffer.Bytes(), err
|
||||
}
|
||||
|
||||
func NewDoltRecordResult(e *logictest.ResultLogEntry, version string) *DoltResultRecord {
|
||||
var result string
|
||||
switch e.Result {
|
||||
case logictest.Ok:
|
||||
result = "ok"
|
||||
case logictest.NotOk:
|
||||
result = "not ok"
|
||||
case logictest.Skipped:
|
||||
result = "skipped"
|
||||
case logictest.Timeout:
|
||||
result = "timeout"
|
||||
case logictest.DidNotRun:
|
||||
result = "did not run"
|
||||
}
|
||||
return &DoltResultRecord{
|
||||
Version: version,
|
||||
TestFile: e.TestFile,
|
||||
LineNum: e.LineNum,
|
||||
Query: e.Query,
|
||||
Duration: e.Duration.Milliseconds(),
|
||||
Result: result,
|
||||
ErrorMessage: e.ErrorMessage,
|
||||
}
|
||||
}
|
||||
|
||||
type TestResultArray struct {
|
||||
Rows []*DoltResultRecord `json:"rows"`
|
||||
}
|
||||
|
||||
type DoltResultRecord struct {
|
||||
Version string `json:"version"`
|
||||
TestFile string `json:"test_file"`
|
||||
LineNum int `json:"line_num"`
|
||||
Query string `json:"query_string"`
|
||||
Duration int64 `json:"duration"`
|
||||
Result string `json:"result"`
|
||||
ErrorMessage string `json:"error_message,omitempty"`
|
||||
}
|
||||
|
||||
// fromResultCsvHeaders returns supported csv headers for a Result
|
||||
func fromResultCsvHeaders() []string {
|
||||
return []string{
|
||||
"version",
|
||||
"test_file",
|
||||
"line_num",
|
||||
"query_string",
|
||||
"duration",
|
||||
"result",
|
||||
"error_message",
|
||||
}
|
||||
}
|
||||
|
||||
// fromHeaderColumnValue returns the value from the DoltResultRecord for the given
|
||||
// header field
|
||||
func fromHeaderColumnValue(h string, r *DoltResultRecord) (string, error) {
|
||||
var val string
|
||||
switch h {
|
||||
case "version":
|
||||
val = r.Version
|
||||
case "test_file":
|
||||
val = r.TestFile
|
||||
case "line_num":
|
||||
val = fmt.Sprintf("%d", r.LineNum)
|
||||
case "query_string":
|
||||
val = r.Query
|
||||
case "duration":
|
||||
val = fmt.Sprintf("%d", r.Duration)
|
||||
case "result":
|
||||
val = r.Result
|
||||
case "error_message":
|
||||
val = r.ErrorMessage
|
||||
default:
|
||||
return "", errors.Errorf("unsupported header field")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// writeResultsCsv writes []*DoltResultRecord to stdout in csv format
|
||||
func writeResultsCsv(results []*DoltResultRecord) (err error) {
|
||||
csvWriter := csv.NewWriter(os.Stdout)
|
||||
|
||||
// write header
|
||||
headers := fromResultCsvHeaders()
|
||||
if err := csvWriter.Write(headers); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// write rows
|
||||
for _, r := range results {
|
||||
row := make([]string, 0)
|
||||
for _, field := range headers {
|
||||
val, err := fromHeaderColumnValue(field, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row = append(row, val)
|
||||
}
|
||||
err = csvWriter.Write(row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
csvWriter.Flush()
|
||||
if err := csvWriter.Error(); err != nil {
|
||||
return err
|
||||
}
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user