chore: import upstream snapshot with attribution
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user