319 строки
12 KiB
Go
319 строки
12 KiB
Go
package workspace
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/skeema/mybase"
|
|
"github.com/skeema/skeema/fs"
|
|
"github.com/skeema/skeema/util"
|
|
"github.com/skeema/tengo"
|
|
)
|
|
|
|
func TestMain(m *testing.M) {
|
|
// Suppress packet error output when attempting to connect to a Dockerized
|
|
// mysql-server which is still starting up
|
|
tengo.UseFilteredDriverLogger()
|
|
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
func TestIntegration(t *testing.T) {
|
|
images := tengo.SplitEnv("SKEEMA_TEST_IMAGES")
|
|
if len(images) == 0 {
|
|
fmt.Println("SKEEMA_TEST_IMAGES env var is not set, so integration tests will be skipped!")
|
|
fmt.Println("To run integration tests, you may set SKEEMA_TEST_IMAGES to a comma-separated")
|
|
fmt.Println("list of Docker images. Example:\n# SKEEMA_TEST_IMAGES=\"mysql:5.6,mysql:5.7\" go test")
|
|
}
|
|
manager, err := tengo.NewDockerClient(tengo.DockerClientOptions{})
|
|
if err != nil {
|
|
t.Errorf("Unable to create sandbox manager: %s", err)
|
|
}
|
|
suite := &WorkspaceIntegrationSuite{manager: manager}
|
|
tengo.RunSuite(suite, t, images)
|
|
}
|
|
|
|
type WorkspaceIntegrationSuite struct {
|
|
manager *tengo.DockerClient
|
|
d *tengo.DockerizedInstance
|
|
}
|
|
|
|
func (s WorkspaceIntegrationSuite) TestExecLogicalSchema(t *testing.T) {
|
|
// Test with just valid CREATE TABLEs
|
|
dirPath := "../testdata/golden/init/mydb/product"
|
|
if major, minor, _ := s.d.Version(); major == 5 && minor == 5 {
|
|
dirPath = strings.Replace(dirPath, "golden", "golden-mysql55", 1)
|
|
}
|
|
dir := s.getParsedDir(t, dirPath, "")
|
|
opts, err := OptionsForDir(dir, s.d.Instance)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from OptionsForDir: %s", err)
|
|
}
|
|
opts.LockWaitTimeout = 100 * time.Millisecond
|
|
wsSchema, err := ExecLogicalSchema(dir.LogicalSchemas[0], opts)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from ExecLogicalSchema: %s", err)
|
|
}
|
|
if len(wsSchema.Failures) > 0 {
|
|
t.Errorf("Expected no StatementErrors, instead found %d", len(wsSchema.Failures))
|
|
}
|
|
if len(wsSchema.Tables) < 4 {
|
|
t.Errorf("Expected at least 4 tables, but instead found %d", len(wsSchema.Tables))
|
|
}
|
|
|
|
// Test with a valid ALTER involved
|
|
oldUserColumnCount := len(wsSchema.Table("users").Columns)
|
|
dir.LogicalSchemas[0].AddStatement(&fs.Statement{
|
|
Type: fs.StatementTypeAlter,
|
|
ObjectType: tengo.ObjectTypeTable,
|
|
ObjectName: "users",
|
|
Text: "ALTER TABLE users ADD COLUMN foo int",
|
|
})
|
|
wsSchema, err = ExecLogicalSchema(dir.LogicalSchemas[0], opts)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from ExecLogicalSchema: %s", err)
|
|
}
|
|
if len(wsSchema.Failures) > 0 {
|
|
t.Errorf("Expected no StatementErrors, instead found %d", len(wsSchema.Failures))
|
|
}
|
|
if expected := oldUserColumnCount + 1; len(wsSchema.Table("users").Columns) != expected {
|
|
t.Errorf("Expected table users to now have %d columns, instead found %d", expected, len(wsSchema.Table("users").Columns))
|
|
}
|
|
}
|
|
|
|
func (s WorkspaceIntegrationSuite) TestExecLogicalSchemaErrors(t *testing.T) {
|
|
dirPath := "../testdata/golden/init/mydb/product"
|
|
if major, minor, _ := s.d.Version(); major == 5 && minor == 5 {
|
|
dirPath = strings.Replace(dirPath, "golden", "golden-mysql55", 1)
|
|
}
|
|
dir := s.getParsedDir(t, dirPath, "")
|
|
opts, err := OptionsForDir(dir, s.d.Instance)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from OptionsForDir: %s", err)
|
|
}
|
|
opts.LockWaitTimeout = 100 * time.Millisecond
|
|
|
|
// Test with invalid ALTER (valid syntax but nonexistent table)
|
|
dir.LogicalSchemas[0].AddStatement(&fs.Statement{
|
|
Type: fs.StatementTypeAlter,
|
|
ObjectType: tengo.ObjectTypeTable,
|
|
ObjectName: "nopenopenope",
|
|
Text: "ALTER TABLE nopenopenope ADD COLUMN foo int",
|
|
})
|
|
wsSchema, err := ExecLogicalSchema(dir.LogicalSchemas[0], opts)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from ExecLogicalSchema: %s", err)
|
|
}
|
|
if len(wsSchema.Failures) == 1 {
|
|
if wsSchema.Failures[0].Statement != dir.LogicalSchemas[0].Alters[0] {
|
|
t.Error("Unexpected Statement pointed to by StatementError")
|
|
} else if !strings.Contains(wsSchema.Failures[0].String(), dir.LogicalSchemas[0].Alters[0].Text) {
|
|
t.Error("StatementError did not contain full SQL of erroring statement")
|
|
}
|
|
} else {
|
|
t.Errorf("Expected one StatementError, instead found %d", len(wsSchema.Failures))
|
|
}
|
|
dir.LogicalSchemas[0].Alters = []*fs.Statement{}
|
|
|
|
// Introduce an intentional syntax error
|
|
key := tengo.ObjectKey{Type: tengo.ObjectTypeTable, Name: "posts"}
|
|
stmt := dir.LogicalSchemas[0].Creates[key]
|
|
stmt.Text = strings.Replace(stmt.Text, "PRIMARY KEY", "PIRMRAY YEK", 1)
|
|
wsSchema, err = ExecLogicalSchema(dir.LogicalSchemas[0], opts)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from ExecLogicalSchema: %s", err)
|
|
}
|
|
if len(wsSchema.Tables) < 3 {
|
|
t.Errorf("Expected at least 3 tables, but instead found %d", len(wsSchema.Tables))
|
|
}
|
|
if len(wsSchema.Failures) != 1 {
|
|
t.Errorf("Expected 1 StatementError, instead found %d", len(wsSchema.Failures))
|
|
} else if wsSchema.Failures[0].ObjectName != "posts" {
|
|
t.Errorf("Expected 1 StatementError for table `posts`; instead found it is for table `%s`", wsSchema.Failures[0].ObjectName)
|
|
} else if !strings.HasPrefix(wsSchema.Failures[0].Error(), stmt.Location()) {
|
|
t.Error("StatementError did not contain the location of the invalid statement")
|
|
}
|
|
err = wsSchema.Failures[0] // compile-time check of satisfying interface
|
|
if errorText := err.Error(); errorText == "" {
|
|
t.Error("Unexpectedly found blank error text")
|
|
}
|
|
|
|
// Test handling of fatal error
|
|
opts.Type = Type(999)
|
|
if _, err := ExecLogicalSchema(dir.LogicalSchemas[0], opts); err == nil {
|
|
t.Error("Expected error from invalid options.Type, but instead err is nil")
|
|
}
|
|
}
|
|
|
|
// TestExecLogicalSchemaFK confirms that ExecLogicalSchema does not choke on
|
|
// concurrent table creation involving cross-referencing foreign keys. This
|
|
// situation, if not specially handled, is known to cause random deadlock
|
|
// errors with MySQL 8.0's new data dictionary.
|
|
func (s WorkspaceIntegrationSuite) TestExecLogicalSchemaFK(t *testing.T) {
|
|
if !s.d.Flavor().HasDataDictionary() {
|
|
t.Skip("Test only relevant for flavors that have the new data dictionary")
|
|
}
|
|
|
|
dir := s.getParsedDir(t, "testdata/manyfk", "")
|
|
opts, err := OptionsForDir(dir, s.d.Instance)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from OptionsForDir: %s", err)
|
|
}
|
|
opts.LockWaitTimeout = 100 * time.Millisecond
|
|
|
|
// Test multiple times, since the problem isn't deterministic
|
|
for n := 0; n < 3; n++ {
|
|
wsSchema, err := ExecLogicalSchema(dir.LogicalSchemas[0], opts)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from ExecLogicalSchema: %s", err)
|
|
}
|
|
if len(wsSchema.Failures) > 0 {
|
|
t.Errorf("Expected no StatementErrors, instead found %d; first err %v from %s", len(wsSchema.Failures), wsSchema.Failures[0].Err, wsSchema.Failures[0].Statement.Location())
|
|
} else if len(wsSchema.Tables) < 6 {
|
|
t.Errorf("Expected at least 6 tables, but instead found %d", len(wsSchema.Tables))
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s WorkspaceIntegrationSuite) TestOptionsForDir(t *testing.T) {
|
|
getOpts := func(cliFlags string) Options {
|
|
t.Helper()
|
|
dir := s.getParsedDir(t, "../testdata/golden/init/mydb/product", cliFlags)
|
|
opts, err := OptionsForDir(dir, s.d.Instance)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from OptionsForDir: %s", err)
|
|
}
|
|
return opts
|
|
}
|
|
assertOptsError := func(cliFlags string) {
|
|
t.Helper()
|
|
dir := s.getParsedDir(t, "../testdata/golden/init/mydb/product", cliFlags)
|
|
if _, err := OptionsForDir(dir, s.d.Instance); err == nil {
|
|
t.Errorf("Expected non-nil error from OptionsForDir with CLI flags %s, but err was nil", cliFlags)
|
|
}
|
|
}
|
|
|
|
// Test error conditions
|
|
assertOptsError("--workspace=invalid")
|
|
assertOptsError("--workspace=docker --docker-cleanup=invalid")
|
|
assertOptsError("--workspace=docker --connect-options='autocommit=0'")
|
|
assertOptsError("--workspace=temp-schema --temp-schema-threads=0")
|
|
assertOptsError("--workspace=temp-schema --temp-schema-threads=-20")
|
|
assertOptsError("--workspace=temp-schema --temp-schema-threads=banana")
|
|
assertOptsError("--workspace=temp-schema --temp-schema-binlog=potato")
|
|
|
|
// Test default configuration, which should use temp-schema with drop cleanup
|
|
if opts := getOpts(""); opts.Type != TypeTempSchema || opts.CleanupAction != CleanupActionDrop {
|
|
t.Errorf("Unexpected type %v returned", opts.Type)
|
|
}
|
|
|
|
// Test temp-schema with some non-default options
|
|
opts := getOpts("--workspace=temp-schema --temp-schema=override --reuse-temp-schema")
|
|
if opts.Type != TypeTempSchema || opts.CleanupAction != CleanupActionNone || opts.SchemaName != "override" {
|
|
t.Errorf("Unexpected return from OptionsForDir: %+v", opts)
|
|
}
|
|
|
|
// Test docker with defaults, which should have no cleanup action, and match
|
|
// flavor of suite's DockerizedInstance
|
|
opts = getOpts("--workspace=docker")
|
|
if opts.Type != TypeLocalDocker || opts.CleanupAction != CleanupActionNone || opts.Flavor.String() != s.d.Flavor().Family().String() {
|
|
t.Errorf("Unexpected return from OptionsForDir: %+v", opts)
|
|
}
|
|
|
|
// Test docker with other cleanup actions
|
|
if opts = getOpts("--workspace=docker --docker-cleanup=StOp"); opts.CleanupAction != CleanupActionStop {
|
|
t.Errorf("Unexpected return from OptionsForDir: %+v", opts)
|
|
}
|
|
if opts = getOpts("--workspace=docker --docker-cleanup=destroy"); opts.CleanupAction != CleanupActionDestroy {
|
|
t.Errorf("Unexpected return from OptionsForDir: %+v", opts)
|
|
}
|
|
|
|
// Test docker with specific flavor
|
|
if opts = getOpts("--workspace=docker --flavor=mysql:5.5"); opts.Flavor.String() != "mysql:5.5" {
|
|
t.Errorf("Unexpected return from OptionsForDir: %+v", opts)
|
|
}
|
|
}
|
|
|
|
// TestPrefab confirms that ExecLogicalSchema still functions properly with a
|
|
// pre-supplied workspace. This provides a way of using Workspace providers from
|
|
// other packages with ExecLogicalSchema.
|
|
func (s WorkspaceIntegrationSuite) TestPrefab(t *testing.T) {
|
|
dirPath := "../testdata/golden/init/mydb/product"
|
|
if major, minor, _ := s.d.Version(); major == 5 && minor == 5 {
|
|
dirPath = strings.Replace(dirPath, "golden", "golden-mysql55", 1)
|
|
}
|
|
dir := s.getParsedDir(t, dirPath, "")
|
|
opts, err := OptionsForDir(dir, s.d.Instance)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from OptionsForDir: %s", err)
|
|
}
|
|
opts.LockWaitTimeout = 100 * time.Millisecond
|
|
|
|
ws, err := New(opts)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from New: %s", err)
|
|
}
|
|
|
|
// Confirm the schema exists after the call to New
|
|
if _, err := ws.IntrospectSchema(); err != nil {
|
|
t.Errorf("Expected IntrospectSchema returned unexpected error %s", err)
|
|
}
|
|
|
|
opts = Options{
|
|
Type: TypePrefab,
|
|
PrefabWorkspace: ws,
|
|
Concurrency: 10,
|
|
}
|
|
wsSchema, err := ExecLogicalSchema(dir.LogicalSchemas[0], opts)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error from ExecLogicalSchema: %s", err)
|
|
}
|
|
if len(wsSchema.Failures) > 0 {
|
|
t.Errorf("Expected no StatementErrors, instead found %d", len(wsSchema.Failures))
|
|
}
|
|
if len(wsSchema.Tables) < 4 {
|
|
t.Errorf("Expected at least 4 tables, but instead found %d", len(wsSchema.Tables))
|
|
}
|
|
|
|
// Confirm that Cleanup still ran, removing the schema, causing
|
|
// IntrospectSchema to now fail
|
|
if _, err := ws.IntrospectSchema(); err == nil {
|
|
t.Error("Expected IntrospectSchema to return an error, but it did not")
|
|
}
|
|
}
|
|
|
|
func (s *WorkspaceIntegrationSuite) Setup(backend string) (err error) {
|
|
s.d, err = s.manager.GetOrCreateInstance(tengo.DockerizedInstanceOptions{
|
|
Name: fmt.Sprintf("skeema-test-%s", strings.Replace(backend, ":", "-", -1)),
|
|
Image: backend,
|
|
RootPassword: "fakepw",
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (s *WorkspaceIntegrationSuite) Teardown(backend string) error {
|
|
return s.d.Stop()
|
|
}
|
|
|
|
func (s *WorkspaceIntegrationSuite) BeforeTest(backend string) error {
|
|
return s.d.NukeData()
|
|
}
|
|
|
|
func (s *WorkspaceIntegrationSuite) getParsedDir(t *testing.T, dirPath, cliFlags string) *fs.Dir {
|
|
t.Helper()
|
|
cmd := mybase.NewCommand("workspacetest", "", "", nil)
|
|
util.AddGlobalOptions(cmd)
|
|
cmd.AddArg("environment", "production", false)
|
|
commandLine := fmt.Sprintf("workspacetest --host=%s --port=%d --password=fakepw %s", s.d.Instance.Host, s.d.Instance.Port, cliFlags)
|
|
cfg := mybase.ParseFakeCLI(t, cmd, commandLine)
|
|
|
|
dir, err := fs.ParseDir(dirPath, cfg)
|
|
if err != nil {
|
|
t.Fatalf("Unexpectedly cannot parse working dir: %s", err)
|
|
}
|
|
return dir
|
|
}
|