gh-ost/go/localtests/tester.go

199 lines
5.3 KiB
Go
Raw Normal View History

2022-12-04 00:40:53 +00:00
package localtests
import (
"database/sql"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/google/shlex"
)
const (
PrimaryHost = "primary"
DefaultHost = "replica"
DefaultPort int64 = 3306
DefaultUsername = "gh-ost"
DefaultPassword = "gh-ost"
testDatabase = "test"
testTable = "gh_ost_test"
testSocketFile = "/tmp/gh-ost.test.sock"
testChunkSize int64 = 10
testDefaultRetries int64 = 3
throttleFlagFile = "/tmp/gh-ost-test.ghost.throttle.flag"
throttleQuery = "select timestampdiff(second, min(last_update), now()) < 5 from _gh_ost_test_ghc"
//
failedEmoji = "\u274C"
successEmoji = "\u2705"
)
// Config represents the configuration.
type Config struct {
Host string
Port int64
Username string
Password string
GhostBinary string
MysqlBinary string
StorageEngine string
TestsDir string
}
type Tester struct {
config Config
primary *sql.DB
replica *sql.DB
}
func NewTester(config Config, primary, replica *sql.DB) *Tester {
return &Tester{
config: config,
primary: primary,
replica: replica,
}
}
// WaitForMySQLAvailable waits for MySQL to become ready for
// testing on both the primary and replica.
func (t *Tester) WaitForMySQLAvailable() error {
interval := 2 * time.Second
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-time.After(5 * time.Minute):
return errors.New("timed out waiting for mysql")
case <-ticker.C:
if err := func() error {
primaryGTIDExec, err := pingAndGetGTIDExecuted(t.primary, interval)
if err != nil {
return err
}
replicaGTIDExec, err := pingAndGetGTIDExecuted(t.replica, interval)
if err != nil {
return err
}
if !replicaGTIDExec.Contain(primaryGTIDExec) {
return fmt.Errorf("Replica/primary GTID not equal: %s != %s",
replicaGTIDExec.String(), primaryGTIDExec.String(),
)
}
return nil
}(); err != nil {
log.Printf("Waiting for MySQL primary/replica to init: %+v", err)
continue
}
log.Printf("MySQL primary/replica is available %s", successEmoji)
return nil
}
}
}
// ReadTests reads test configurations from a directory. As single test isee
// returned when specificTestName is specified.
func (t *Tester) ReadTests(specificTestName string) (tests []Test, err error) {
subdirs, err := ioutil.ReadDir(t.config.TestsDir)
if err != nil {
return tests, err
}
for _, subdir := range subdirs {
test := Test{
Name: subdir.Name(),
Path: filepath.Join(t.config.TestsDir, subdir.Name()),
}
stat, err := os.Stat(test.Path)
if err != nil || !stat.IsDir() {
continue
}
if specificTestName != "" && !strings.EqualFold(test.Name, specificTestName) {
continue
}
test.CreateSQLFile = filepath.Join(test.Path, "create.sql")
if _, err = os.Stat(test.CreateSQLFile); err != nil {
log.Printf("Failed to find create.sql file %q: %+v", test.CreateSQLFile, err)
return tests, err
}
destroySQLFile := filepath.Join(test.Path, "destroy.sql")
if _, err = os.Stat(destroySQLFile); err == nil {
test.DestroySQLFile = destroySQLFile
}
expectFailureFile := filepath.Join(test.Path, "expect_failure")
test.ExpectedFailure, _ = readTestFile(expectFailureFile)
sqlModeFile := filepath.Join(test.Path, "sql_mode")
if sqlMode, err := readTestFile(sqlModeFile); err == nil {
test.SQLMode = &sqlMode
}
orderByFile := filepath.Join(test.Path, "order_by")
test.ValidateOrderBy, _ = readTestFile(orderByFile)
origColumnsFile := filepath.Join(test.Path, "orig_columns")
if origColumns, err := readTestFile(origColumnsFile); err == nil {
origColumns = strings.Replace(origColumns, " ", "", -1)
test.ValidateOrigColumns = strings.Split(origColumns, ",")
}
ghostColumnsFile := filepath.Join(test.Path, "ghost_columns")
if ghostColumns, err := readTestFile(ghostColumnsFile); err == nil {
ghostColumns = strings.Replace(ghostColumns, " ", "", -1)
test.ValidateColumns = strings.Split(ghostColumns, ",")
}
extraArgsFile := filepath.Join(test.Path, "extra_args")
if _, err = os.Stat(extraArgsFile); err == nil {
extraArgsStr, err := readTestFile(extraArgsFile)
if err != nil {
log.Printf("Failed to read extra_args file %q: %+v", extraArgsFile, err)
return tests, err
}
if test.ExtraArgs, err = shlex.Split(extraArgsStr); err != nil {
log.Printf("Failed to read extra_args file %q: %+v", extraArgsFile, err)
return tests, err
}
}
tests = append(tests, test)
}
return tests, err
}
// RunTest prepares and runs a single test.
func (t *Tester) RunTest(test Test) (err error) {
if err = test.Prepare(t.config, t.primary); err != nil {
return err
}
log.Printf("[%s] prepared test %s", test.Name, successEmoji)
if err = test.Migrate(t.config, t.primary, t.replica); err != nil {
log.Printf("[%s] failed to migrate test %s%s%s", test.Name, failedEmoji,
failedEmoji, failedEmoji)
return err
}
log.Printf("[%s] successfully migrated test %s", test.Name, successEmoji)
if err = test.Validate(t.config, t.primary, t.replica); err != nil {
log.Printf("[%s] failed to validate test %s%s%s", test.Name, failedEmoji,
failedEmoji, failedEmoji)
return err
}
log.Printf("[%s] successfully validated test %s", test.Name, successEmoji)
return err
}