chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
// 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 driver
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/stdlib"
|
||||
)
|
||||
|
||||
var DoltgresPath string
|
||||
var DelvePath string
|
||||
|
||||
const TestUserName = "Bats Tests"
|
||||
const TestEmailAddress = "bats@email.fake"
|
||||
|
||||
const ConnectAttempts = 50
|
||||
const RetrySleepDuration = 50 * time.Millisecond
|
||||
|
||||
// EnvDoltgresBinPath is the environment variable used to locate the doltgres
|
||||
// binary used by these tests. If unset, "doltgres" on the PATH is used.
|
||||
const EnvDoltgresBinPath = "DOLTGRES_BIN_PATH"
|
||||
|
||||
func init() {
|
||||
path := os.Getenv(EnvDoltgresBinPath)
|
||||
if path == "" {
|
||||
path = "doltgres"
|
||||
}
|
||||
path = filepath.Clean(path)
|
||||
var err error
|
||||
|
||||
DoltgresPath, err = exec.LookPath(path)
|
||||
if err != nil {
|
||||
log.Printf("did not find doltgres binary: %v\n", err.Error())
|
||||
}
|
||||
|
||||
DelvePath, _ = exec.LookPath("dlv")
|
||||
}
|
||||
|
||||
// DoltUser is an abstraction for a user account that calls the `doltgres`
|
||||
// server. All of our doltgres binary invocations are done through DoltUser.
|
||||
//
|
||||
// For our purposes, it does the following:
|
||||
//
|
||||
// - owns a tmpdir, to which it sets DOLT_ROOT_PATH when invoking doltgres.
|
||||
//
|
||||
// - writes some initial dolt global config (user.name, user.email,
|
||||
// metrics.disabled = true) into that root path. Doltgres has no `config`
|
||||
// CLI command, so unlike Dolt we write the config file directly.
|
||||
//
|
||||
// - can create repo stores, which will be a tmpdir to store a data-dir
|
||||
// containing one or more databases.
|
||||
type DoltUser struct {
|
||||
tmpdir string
|
||||
}
|
||||
|
||||
var _ DoltCmdable = DoltUser{}
|
||||
var _ DoltDebuggable = DoltUser{}
|
||||
|
||||
func NewDoltUser() (DoltUser, error) {
|
||||
tmpdir, err := os.MkdirTemp("", "go-sql-server-driver-")
|
||||
if err != nil {
|
||||
return DoltUser{}, err
|
||||
}
|
||||
res := DoltUser{tmpdir}
|
||||
// Doltgres has no `config --global` CLI command, so we write the global
|
||||
// config file directly. Doltgres uses Dolt's env loading, which reads the
|
||||
// global config from $DOLT_ROOT_PATH/.dolt/config_global.json.
|
||||
cfgDir := filepath.Join(tmpdir, ".dolt")
|
||||
if err := os.MkdirAll(cfgDir, 0750); err != nil {
|
||||
return DoltUser{}, err
|
||||
}
|
||||
contents := fmt.Sprintf(`{"metrics.disabled":"true","user.name":%q,"user.email":%q}`+"\n", TestUserName, TestEmailAddress)
|
||||
if err := os.WriteFile(filepath.Join(cfgDir, "config_global.json"), []byte(contents), 0640); err != nil {
|
||||
return DoltUser{}, err
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (u DoltUser) DoltCmd(args ...string) *exec.Cmd {
|
||||
cmd := exec.Command(DoltgresPath, args...)
|
||||
cmd.Dir = u.tmpdir
|
||||
cmd.Env = append(os.Environ(), "DOLT_ROOT_PATH="+u.tmpdir)
|
||||
ApplyCmdAttributes(cmd)
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (u DoltUser) DoltDebug(debuggerPort int, args ...string) *exec.Cmd {
|
||||
if DelvePath != "" {
|
||||
dlvArgs := []string{
|
||||
fmt.Sprintf("--listen=:%d", debuggerPort),
|
||||
"--headless",
|
||||
"--api-version=2",
|
||||
"--accept-multiclient",
|
||||
"exec",
|
||||
DoltgresPath,
|
||||
"--",
|
||||
}
|
||||
cmd := exec.Command(DelvePath, append(dlvArgs, args...)...)
|
||||
cmd.Dir = u.tmpdir
|
||||
cmd.Env = append(os.Environ(), "DOLT_ROOT_PATH="+u.tmpdir)
|
||||
ApplyCmdAttributes(cmd)
|
||||
return cmd
|
||||
} else {
|
||||
panic("dlv not found")
|
||||
}
|
||||
}
|
||||
|
||||
func (u DoltUser) MakeRepoStore() (RepoStore, error) {
|
||||
tmpdir, err := os.MkdirTemp(u.tmpdir, "repo-store-")
|
||||
if err != nil {
|
||||
return RepoStore{}, err
|
||||
}
|
||||
return RepoStore{u, tmpdir}, nil
|
||||
}
|
||||
|
||||
func (u DoltUser) Cleanup() error {
|
||||
return os.RemoveAll(u.tmpdir)
|
||||
}
|
||||
|
||||
type RepoStore struct {
|
||||
user DoltUser
|
||||
Dir string
|
||||
}
|
||||
|
||||
var _ DoltCmdable = RepoStore{}
|
||||
var _ DoltDebuggable = RepoStore{}
|
||||
|
||||
// MakeRepo creates a new doltgres database named |name| within this store's
|
||||
// data-dir. Because doltgres has no `init` CLI command, the database is
|
||||
// initialized by briefly running a doltgres server with DOLTGRES_DB set to the
|
||||
// database name and waiting for the on-disk database to be created.
|
||||
func (rs RepoStore) MakeRepo(name string) (Repo, error) {
|
||||
ret := Repo{rs.user, rs, filepath.Join(rs.Dir, name), name}
|
||||
err := rs.initDatabase(name, nil)
|
||||
if err != nil {
|
||||
return Repo{}, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (rs RepoStore) DoltCmd(args ...string) *exec.Cmd {
|
||||
cmd := rs.user.DoltCmd(args...)
|
||||
cmd.Dir = rs.Dir
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (rs RepoStore) DoltDebug(debuggerPort int, args ...string) *exec.Cmd {
|
||||
cmd := rs.user.DoltDebug(debuggerPort, args...)
|
||||
cmd.Dir = rs.Dir
|
||||
return cmd
|
||||
}
|
||||
|
||||
// initDatabase starts a short-lived doltgres server bound to a free port with
|
||||
// the data-dir set to this store, creates the database |name| if it does not
|
||||
// already exist, runs the optional |fn| against a connection to that database,
|
||||
// and then shuts the server down. This is the doltgres equivalent of
|
||||
// `dolt init` plus any pre-server setup such as adding remotes.
|
||||
func (rs RepoStore) initDatabase(name string, fn func(db *sql.DB) error) error {
|
||||
port, err := freePort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfgPath := filepath.Join(rs.Dir, fmt.Sprintf(".init-%s-config.yaml", sanitize(name)))
|
||||
// The unix socket path must stay well under the OS limit (~108 chars), so
|
||||
// it is placed directly in the temp dir keyed by the (unique) port rather
|
||||
// than inside the potentially-long data-dir path.
|
||||
sockPath := filepath.Join(os.TempDir(), fmt.Sprintf("dg-init-%d.sock", port))
|
||||
cfgContents := fmt.Sprintf("log_level: warn\nlistener:\n host: 127.0.0.1\n port: %d\n socket: %s\n", port, sockPath)
|
||||
if err := os.WriteFile(cfgPath, []byte(cfgContents), 0640); err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(cfgPath)
|
||||
|
||||
cmd := rs.DoltCmd("--data-dir=.", "--config="+cfgPath)
|
||||
cmd.Env = append(cmd.Env, "DOLTGRES_DB="+name)
|
||||
output := new(bytes.Buffer)
|
||||
cmd.Stdout = output
|
||||
cmd.Stderr = output
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_ = cmd.Process.Signal(os.Interrupt)
|
||||
_, _ = cmd.Process.Wait()
|
||||
}()
|
||||
|
||||
db, err := ConnectDB("postgres", "password", name, "127.0.0.1", port, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not connect to init server for %s: %w (output: %s)", name, err, output.String())
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
if _, err := db.Exec(fmt.Sprintf(`CREATE DATABASE IF NOT EXISTS %s`, name)); err != nil {
|
||||
return err
|
||||
}
|
||||
if fn != nil {
|
||||
if err := fn(db); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func sanitize(s string) string {
|
||||
out := make([]rune, 0, len(s))
|
||||
for _, r := range s {
|
||||
if r == '/' || r == '\\' || r == ' ' {
|
||||
out = append(out, '_')
|
||||
} else {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// freePort asks the OS for an available TCP port on the loopback interface.
|
||||
func freePort() (int, error) {
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer l.Close()
|
||||
return l.Addr().(*net.TCPAddr).Port, nil
|
||||
}
|
||||
|
||||
type Repo struct {
|
||||
user DoltUser
|
||||
rs RepoStore
|
||||
Dir string
|
||||
Name string
|
||||
}
|
||||
|
||||
func (r Repo) DoltCmd(args ...string) *exec.Cmd {
|
||||
cmd := r.user.DoltCmd(args...)
|
||||
cmd.Dir = r.Dir
|
||||
return cmd
|
||||
}
|
||||
|
||||
// CreateRemote adds a remote named |name| with the given |url| to this
|
||||
// database. Doltgres has no `remote` CLI command, so this is done by briefly
|
||||
// running a server and calling the dolt_remote() stored function.
|
||||
func (r Repo) CreateRemote(name, url string) error {
|
||||
return r.rs.initDatabase(r.Name, func(db *sql.DB) error {
|
||||
_, err := db.Exec(`SELECT dolt_remote('add', $1, $2)`, name, url)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
type SqlServer struct {
|
||||
Name string
|
||||
Done chan struct{}
|
||||
Cmd *exec.Cmd
|
||||
CmdWaitErr error
|
||||
Port int
|
||||
DebugPort int
|
||||
Output *bytes.Buffer
|
||||
DBName string
|
||||
RecreateCmd func(args ...string) *exec.Cmd
|
||||
|
||||
// Where to write server log output for display. If nil,
|
||||
// defaults to os.Stdout.
|
||||
LogWriter io.Writer
|
||||
|
||||
// If non-nil, called with each complete line of server output.
|
||||
OutputVisitor func(string)
|
||||
}
|
||||
|
||||
type SqlServerOpt func(s *SqlServer)
|
||||
|
||||
func WithArgs(args ...string) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.Cmd.Args = append(s.Cmd.Args, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithName(name string) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func WithEnvs(envs ...string) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.Cmd.Env = append(s.Cmd.Env, envs...)
|
||||
}
|
||||
}
|
||||
|
||||
func WithPort(port int) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
func WithOutputVisitor(f func(string)) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.OutputVisitor = f
|
||||
}
|
||||
}
|
||||
|
||||
func WithLogWriter(w io.Writer) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.LogWriter = w
|
||||
}
|
||||
}
|
||||
|
||||
func WithDebugPort(port int) SqlServerOpt {
|
||||
return func(s *SqlServer) {
|
||||
s.DebugPort = port
|
||||
}
|
||||
}
|
||||
|
||||
type DoltCmdable interface {
|
||||
DoltCmd(args ...string) *exec.Cmd
|
||||
}
|
||||
|
||||
type DoltDebuggable interface {
|
||||
DoltDebug(debuggerPort int, args ...string) *exec.Cmd
|
||||
}
|
||||
|
||||
// StartSqlServer starts a doltgres server. Unlike Dolt, doltgres has no
|
||||
// `sql-server` subcommand; running the binary itself starts the server.
|
||||
func StartSqlServer(dc DoltCmdable, opts ...SqlServerOpt) (*SqlServer, error) {
|
||||
cmd := dc.DoltCmd()
|
||||
return runSqlServerCommand(dc, opts, cmd)
|
||||
}
|
||||
|
||||
func DebugSqlServer(dc DoltCmdable, debuggerPort int, opts ...SqlServerOpt) (*SqlServer, error) {
|
||||
ddb, ok := dc.(DoltDebuggable)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%T does not implement DoltDebuggable", dc)
|
||||
}
|
||||
|
||||
cmd := ddb.DoltDebug(debuggerPort)
|
||||
return runSqlServerCommand(dc, append(opts, WithDebugPort(debuggerPort)), cmd)
|
||||
}
|
||||
|
||||
func runSqlServerCommand(dc DoltCmdable, opts []SqlServerOpt, cmd *exec.Cmd) (*SqlServer, error) {
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmd.Stderr = cmd.Stdout
|
||||
output := new(bytes.Buffer)
|
||||
done := make(chan struct{})
|
||||
|
||||
server := &SqlServer{
|
||||
Done: done,
|
||||
Cmd: cmd,
|
||||
Port: 5432,
|
||||
Output: output,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(server)
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
server.CmdWaitErr = server.Cmd.Wait()
|
||||
close(done)
|
||||
}()
|
||||
logw := server.LogWriter
|
||||
if logw == nil {
|
||||
logw = os.Stdout
|
||||
}
|
||||
multiCopyWithNamePrefix(logw, output, stdout, server.Name, server.OutputVisitor)
|
||||
}()
|
||||
|
||||
server.RecreateCmd = func(args ...string) *exec.Cmd {
|
||||
if server.DebugPort > 0 {
|
||||
ddb, ok := dc.(DoltDebuggable)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("%T does not implement DoltDebuggable", dc))
|
||||
}
|
||||
return ddb.DoltDebug(server.DebugPort, args...)
|
||||
} else {
|
||||
return dc.DoltCmd(args...)
|
||||
}
|
||||
}
|
||||
|
||||
err = server.Cmd.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func (s *SqlServer) ErrorStop() error {
|
||||
<-s.Done
|
||||
return s.CmdWaitErr
|
||||
}
|
||||
|
||||
func multiCopyWithNamePrefix(stdout, captured io.Writer, in io.Reader, name string, visitor func(string)) {
|
||||
reader := bufio.NewReader(in)
|
||||
multiOut := io.MultiWriter(stdout, captured)
|
||||
var lineBuf []byte
|
||||
wantsPrefix := true
|
||||
for {
|
||||
line, isPrefix, err := reader.ReadLine()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if wantsPrefix && name != "" {
|
||||
stdout.Write([]byte("["))
|
||||
stdout.Write([]byte(name))
|
||||
stdout.Write([]byte("] "))
|
||||
}
|
||||
multiOut.Write(line)
|
||||
if isPrefix {
|
||||
if visitor != nil {
|
||||
lineBuf = append(lineBuf, line...)
|
||||
}
|
||||
wantsPrefix = false
|
||||
} else {
|
||||
multiOut.Write([]byte("\n"))
|
||||
if visitor != nil {
|
||||
if lineBuf != nil {
|
||||
lineBuf = append(lineBuf, line...)
|
||||
visitor(string(lineBuf))
|
||||
lineBuf = nil
|
||||
} else {
|
||||
visitor(string(line))
|
||||
}
|
||||
}
|
||||
wantsPrefix = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SqlServer) Restart(newargs *[]string, newenvs *[]string) error {
|
||||
err := s.GracefulStop()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
args := s.Cmd.Args[1:]
|
||||
if newargs != nil {
|
||||
args = *newargs
|
||||
}
|
||||
s.Cmd = s.RecreateCmd(args...)
|
||||
if newenvs != nil {
|
||||
s.Cmd.Env = append(s.Cmd.Env, (*newenvs)...)
|
||||
}
|
||||
stdout, err := s.Cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.CmdWaitErr = nil
|
||||
s.Cmd.Stderr = s.Cmd.Stdout
|
||||
s.Done = make(chan struct{})
|
||||
go func() {
|
||||
defer func() {
|
||||
s.CmdWaitErr = s.Cmd.Wait()
|
||||
close(s.Done)
|
||||
}()
|
||||
logw := s.LogWriter
|
||||
if logw == nil {
|
||||
logw = os.Stdout
|
||||
}
|
||||
multiCopyWithNamePrefix(logw, s.Output, stdout, s.Name, s.OutputVisitor)
|
||||
}()
|
||||
return s.Cmd.Start()
|
||||
}
|
||||
|
||||
func (s *SqlServer) DB(c Connection) (*sql.DB, error) {
|
||||
connector, err := s.Connector(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return OpenDB(connector)
|
||||
}
|
||||
|
||||
// Connector returns a database/sql driver.Connector for a connection to this
|
||||
// server using the pgx stdlib driver.
|
||||
func (s *SqlServer) Connector(c Connection) (driver.Connector, error) {
|
||||
pass, err := c.Password()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
user := c.User
|
||||
if user == "" {
|
||||
user = "postgres"
|
||||
}
|
||||
dsn := GetDSN(user, pass, s.DBName, "127.0.0.1", s.Port, c.DriverParams)
|
||||
cfg, err := pgx.ParseConfig(dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return stdlib.GetConnector(*cfg), nil
|
||||
}
|
||||
|
||||
// GetDSN builds a postgres connection URL.
|
||||
func GetDSN(user, password, name, host string, port int, driverParams map[string]string) string {
|
||||
params := make(url.Values)
|
||||
params.Set("sslmode", "prefer")
|
||||
for k, v := range driverParams {
|
||||
params.Set(k, v)
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "postgres",
|
||||
User: url.UserPassword(user, password),
|
||||
Host: fmt.Sprintf("%s:%d", host, port),
|
||||
Path: "/" + name,
|
||||
RawQuery: params.Encode(),
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func OpenDB(connector driver.Connector) (*sql.DB, error) {
|
||||
db := sql.OpenDB(connector)
|
||||
var err error
|
||||
for i := 0; i < ConnectAttempts; i++ {
|
||||
err = db.Ping()
|
||||
if err == nil {
|
||||
return db, nil
|
||||
}
|
||||
time.Sleep(RetrySleepDuration)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func ConnectDB(user, password, name, host string, port int, params map[string]string) (*sql.DB, error) {
|
||||
dsn := GetDSN(user, password, name, host, port, params)
|
||||
db, err := sql.Open("pgx", dsn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := 0; i < ConnectAttempts; i++ {
|
||||
err = db.Ping()
|
||||
if err == nil {
|
||||
return db, nil
|
||||
}
|
||||
time.Sleep(RetrySleepDuration)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return db, nil
|
||||
}
|
||||
|
||||
// WithConnectRetriesDisabled is retained for API-compatibility with the Dolt
|
||||
// test driver. The mysql driver performed automatic connection retries that
|
||||
// some tests needed to circumvent; the pgx stdlib driver does not perform the
|
||||
// same retries, so this currently returns the context unchanged.
|
||||
//
|
||||
// TODO: If we add tests that require precise control over connection-retry
|
||||
// behavior against doltgres (e.g. max-connection tests), implement an
|
||||
// equivalent hook here.
|
||||
func WithConnectRetriesDisabled(ctx context.Context) context.Context {
|
||||
return ctx
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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.
|
||||
|
||||
//go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd
|
||||
// +build darwin dragonfly freebsd linux netbsd openbsd
|
||||
|
||||
package driver
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func ApplyCmdAttributes(cmd *exec.Cmd) {
|
||||
// nothing to do on unix / darwin
|
||||
}
|
||||
|
||||
func (s *SqlServer) GracefulStop() error {
|
||||
select {
|
||||
case <-s.Done:
|
||||
return s.CmdWaitErr
|
||||
default:
|
||||
}
|
||||
err := s.Cmd.Process.Signal(syscall.SIGTERM)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
<-s.Done
|
||||
return s.CmdWaitErr
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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 driver
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func ApplyCmdAttributes(cmd *exec.Cmd) {
|
||||
// Creating a new process group for the process will allow GracefulStop to send the break signal to that process
|
||||
// without also killing the parent process
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
CreationFlags: syscall.CREATE_NEW_PROCESS_GROUP,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SqlServer) GracefulStop() error {
|
||||
err := windows.GenerateConsoleCtrlEvent(windows.CTRL_BREAK_EVENT, uint32(s.Cmd.Process.Pid))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
<-s.Done
|
||||
|
||||
_, err = s.Cmd.Process.Wait()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
// 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 driver
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/creasty/defaults"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// |Connection| represents a single connection to a sql-server instance defined
|
||||
// in the test. The connection will be established and every |Query| in
|
||||
// |Queries| will be run against it. At the end, the connection will be torn down.
|
||||
// If |RestartServer| is non-nil, the server which the connection targets will
|
||||
// be restarted after the connection is terminated.
|
||||
type Connection struct {
|
||||
On string `yaml:"on"`
|
||||
Queries []Query `yaml:"queries"`
|
||||
RestartServer *RestartArgs `yaml:"restart_server"`
|
||||
|
||||
// Rarely needed, allows the entire connection assertion to be retried
|
||||
// on an assertion failure. Use this is only for idempotent connection
|
||||
// interactions and only if the sql-server is prone to tear down the
|
||||
// connection based on things that are happening, such as cluster role
|
||||
// transitions.
|
||||
RetryAttempts int `yaml:"retry_attempts"`
|
||||
|
||||
// The user to connect as. For Doltgres this defaults to the Postgres
|
||||
// superuser, "postgres".
|
||||
User string `default:"postgres" yaml:"user"`
|
||||
// The password to connect with.
|
||||
Pass string `yaml:"password"`
|
||||
PassFile string `yaml:"password_file"`
|
||||
// Any driver params to pass in the connection string.
|
||||
DriverParams map[string]string `yaml:"driver_params"`
|
||||
}
|
||||
|
||||
func (c *Connection) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
defaults.Set(c)
|
||||
type plain Connection
|
||||
if err := unmarshal((*plain)(c)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Connection) Password() (string, error) {
|
||||
if c.PassFile != "" {
|
||||
passFile := c.PassFile
|
||||
if v := os.Getenv("TESTGENDIR"); v != "" {
|
||||
passFile = strings.ReplaceAll(passFile, "$TESTGENDIR", v)
|
||||
}
|
||||
bs, err := os.ReadFile(passFile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(bs)), nil
|
||||
}
|
||||
// Doltgres connections default to the well-known development password
|
||||
// "password" when none is specified, matching DefaultPass in the
|
||||
// doltgres server config.
|
||||
if c.Pass == "" {
|
||||
return "password", nil
|
||||
}
|
||||
return c.Pass, nil
|
||||
}
|
||||
|
||||
// |RestartArgs| are possible arguments, to change the arguments which are
|
||||
// provided to the sql-server process when it is restarted. This is used, for
|
||||
// example, to change server config on a restart.
|
||||
type RestartArgs struct {
|
||||
Args *[]string `yaml:"args"`
|
||||
Envs *[]string `yaml:"envs"`
|
||||
}
|
||||
|
||||
// |TestRepo| represents an init'd doltgres database that is available to a
|
||||
// server instance. It can be created with some files and with remotes defined.
|
||||
// |Name| can include path components separated by `/`, which will create the
|
||||
// repository in a subdirectory.
|
||||
type TestRepo struct {
|
||||
Name string `yaml:"name"`
|
||||
WithFiles []WithFile `yaml:"with_files"`
|
||||
WithRemotes []WithRemote `yaml:"with_remotes"`
|
||||
|
||||
// Only valid on Test.Repos, not in Test.MultiRepos.Repos. If set, a
|
||||
// sql-server process will be run against this TestRepo. It will be
|
||||
// available as TestRepo.Name.
|
||||
Server *Server `yaml:"server"`
|
||||
ExternalServer *ExternalServer `yaml:"external-server"`
|
||||
}
|
||||
|
||||
// |MultiRepo| is a subdirectory where many |TestRepo|s can be defined. You can
|
||||
// start a sql-server on a |MultiRepo|, in which case there will be no default
|
||||
// database to connect to.
|
||||
type MultiRepo struct {
|
||||
Name string `yaml:"name"`
|
||||
Repos []TestRepo `yaml:"repos"`
|
||||
WithFiles []WithFile `yaml:"with_files"`
|
||||
|
||||
// If set, a sql-server process will be run against this TestRepo. It
|
||||
// will be available as MultiRepo.Name.
|
||||
Server *Server `yaml:"server"`
|
||||
}
|
||||
|
||||
// |WithRemote| defines remotes which should be defined on the repository
|
||||
// before the sql-server is started.
|
||||
type WithRemote struct {
|
||||
Name string `yaml:"name"`
|
||||
URL string `yaml:"url"`
|
||||
}
|
||||
|
||||
// |WithFile| defines a file and its contents to be created in a |Repo| or
|
||||
// |MultiRepo| before the servers are started.
|
||||
type WithFile struct {
|
||||
Name string `yaml:"name"`
|
||||
|
||||
// The contents of the file, provided inline in the YAML.
|
||||
Contents string `yaml:"contents"`
|
||||
|
||||
// A source file path to copy to |Name|. Mutually exclusive with
|
||||
// Contents.
|
||||
SourcePath string `yaml:"source_path"`
|
||||
|
||||
// If this is non-nil, the template will be applied to the
|
||||
// contents of the file as they are written through |WriteAtDir|.
|
||||
Template func(string) string
|
||||
}
|
||||
|
||||
func (f WithFile) WriteAtDir(dir string) error {
|
||||
path := filepath.Join(dir, f.Name)
|
||||
d := filepath.Dir(path)
|
||||
err := os.MkdirAll(d, 0750)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.SourcePath != "" {
|
||||
sourcePath := f.SourcePath
|
||||
if v := os.Getenv("TESTGENDIR"); v != "" {
|
||||
sourcePath = strings.ReplaceAll(sourcePath, "$TESTGENDIR", v)
|
||||
}
|
||||
source, err := os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer source.Close()
|
||||
dest, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0550)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
contents, err := io.ReadAll(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.Template != nil {
|
||||
str := f.Template(string(contents))
|
||||
contents = []byte(str)
|
||||
}
|
||||
_, err = dest.Write(contents)
|
||||
return err
|
||||
} else {
|
||||
contents := f.Contents
|
||||
if f.Template != nil {
|
||||
contents = f.Template(contents)
|
||||
}
|
||||
return os.WriteFile(path, []byte(contents), 0550)
|
||||
}
|
||||
}
|
||||
|
||||
// |Server| defines a sql-server process to start. |Name| must match the
|
||||
// top-level |Name| of a |TestRepo| or |MultiRepo|.
|
||||
type Server struct {
|
||||
Name string `yaml:"name"`
|
||||
Args []string `yaml:"args"`
|
||||
Envs []string `yaml:"envs"`
|
||||
|
||||
// The |Port| which the server will be running on. For now, it is up to
|
||||
// the |Args| or config file to make sure this is true. Defaults to 5432.
|
||||
Port int `yaml:"port"`
|
||||
|
||||
// This can be used with templating of dynamic ports to
|
||||
// specify the SQL listener port which will have been filled
|
||||
// in by a call to `{{get_port "server_name"}}` within the
|
||||
// config or args of the server.
|
||||
//
|
||||
// A |Port| != 0 with a |DynamicPort| != "" is an error.
|
||||
DynamicPort string `yaml:"dynamic_port"`
|
||||
|
||||
// DebugPort if set to a non-zero value will cause this server to be started with |dlv| listening for a debugger
|
||||
// connection on the port given.
|
||||
DebugPort int `yaml:"debug_port"`
|
||||
|
||||
// Assertions to be run against the log output of the server process
|
||||
// after the server process successfully terminates.
|
||||
LogMatches []string `yaml:"log_matches"`
|
||||
|
||||
// Assertions to be run against the log output of the server process
|
||||
// after the server process successfully terminates. Each entry is a
|
||||
// regex that must NOT match. The test fails if any pattern matches.
|
||||
LogNotMatches []string `yaml:"log_not_matches"`
|
||||
|
||||
// Assertions to be run against the log output of the server process
|
||||
// after the server process exits with an error. If |ErrorMatches| is
|
||||
// defined, then the server process must exit with a non-0 exit code
|
||||
// after it is launched. This will be asserted before any |Connections|
|
||||
// interactions are performed.
|
||||
ErrorMatches []string `yaml:"error_matches"`
|
||||
}
|
||||
|
||||
type ExternalServer struct {
|
||||
Name string `yaml:"name"`
|
||||
Host string `yaml:"host"`
|
||||
User string `yaml:"user"`
|
||||
Password string `yaml:"password"`
|
||||
// The |Port| which the server will be running on. For now, it is up to
|
||||
// the |Args| to make sure this is true. Defaults to 5432.
|
||||
Port int `yaml:"port"`
|
||||
}
|
||||
|
||||
// The primary interaction of a |Connection|. Either |Query| or |Exec| should
|
||||
// be set, not both.
|
||||
type Query struct {
|
||||
// Run a query against the connection.
|
||||
Query string `yaml:"query"`
|
||||
|
||||
// Run a command against the connection.
|
||||
Exec string `yaml:"exec"`
|
||||
|
||||
// Args to be passed as query parameters to either Query or Exec.
|
||||
Args []string `yaml:"args"`
|
||||
|
||||
// This can only be non-empty for a |Query|. Asserts the results of the
|
||||
// |Query|.
|
||||
Result QueryResult `yaml:"result"`
|
||||
|
||||
// If this is non-empty, asserts the |Query| or the |Exec|
|
||||
// generates an error that matches this string.
|
||||
ErrorMatch string `yaml:"error_match"`
|
||||
|
||||
// If this is non-zero, it represents the number of times to try the
|
||||
// |Query| or the |Exec| and to check its assertions before we fail the
|
||||
// test as a result of failed assertions. When interacting with queries
|
||||
// that introspect things like replication state, this can be used to
|
||||
// wait for quiescence in an inherently racey process. Interactions
|
||||
// will be delayed slightly between each failure.
|
||||
RetryAttempts int `yaml:"retry_attempts"`
|
||||
}
|
||||
|
||||
// |QueryResult| specifies assertions on the results of a |Query|. Columns must
|
||||
// be specified for a |Query| and the query results must fully match. If Rows
|
||||
// are omitted, anything is allowed as long as all rows are read successfully.
|
||||
// All assertions here are string equality.
|
||||
type QueryResult struct {
|
||||
Columns []string `yaml:"columns"`
|
||||
Rows ResultRows `yaml:"rows"`
|
||||
}
|
||||
|
||||
type ResultRows struct {
|
||||
Or *[][][]string
|
||||
}
|
||||
|
||||
func (r *ResultRows) UnmarshalYAML(value *yaml.Node) error {
|
||||
if value.Kind == yaml.SequenceNode {
|
||||
res := make([][][]string, 1)
|
||||
r.Or = &res
|
||||
return value.Decode(&(*r.Or)[0])
|
||||
}
|
||||
var or struct {
|
||||
Or *[][][]string `yaml:"or"`
|
||||
}
|
||||
err := value.Decode(&or)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.Or = or.Or
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user