diff --git a/dbump_test.go b/dbump_test.go index ddd9f9e..8a2402f 100644 --- a/dbump_test.go +++ b/dbump_test.go @@ -1,4 +1,4 @@ -package dbump +package dbump_test import ( "context" @@ -7,247 +7,141 @@ import ( "reflect" "testing" "time" + + "github.com/cristalhq/dbump" + "github.com/cristalhq/dbump/tests" ) func TestRunCheck(t *testing.T) { testCases := []struct { testName string - cfg Config + cfg dbump.Config }{ { testName: "migrator is nil", - cfg: Config{}, + cfg: dbump.Config{}, }, { testName: "loader is nil", - cfg: Config{ - Migrator: &MockMigrator{}, + cfg: dbump.Config{ + Migrator: &tests.MockMigrator{}, }, }, { testName: "mode is ModeNotSet", - cfg: Config{ - Migrator: &MockMigrator{}, - Loader: NewSliceLoader(nil), + cfg: dbump.Config{ + Migrator: &tests.MockMigrator{}, + Loader: dbump.NewSliceLoader(nil), }, }, { testName: "bad mode", - cfg: Config{ - Migrator: &MockMigrator{}, - Loader: NewSliceLoader(nil), - Mode: modeMaxPossible + 1, + cfg: dbump.Config{ + Migrator: &tests.MockMigrator{}, + Loader: dbump.NewSliceLoader(nil), + Mode: 1000, }, }, { testName: "num not set", - cfg: Config{ - Migrator: &MockMigrator{}, - Loader: NewSliceLoader(nil), - Mode: ModeApplyN, + cfg: dbump.Config{ + Migrator: &tests.MockMigrator{}, + Loader: dbump.NewSliceLoader(nil), + Mode: dbump.ModeApplyN, }, }, { testName: "num not set", - cfg: Config{ - Migrator: &MockMigrator{}, - Loader: NewSliceLoader(nil), - Mode: ModeRevertN, + cfg: dbump.Config{ + Migrator: &tests.MockMigrator{}, + Loader: dbump.NewSliceLoader(nil), + Mode: dbump.ModeRevertN, }, }, } for _, tc := range testCases { - failIfOk(t, Run(context.Background(), tc.cfg)) + failIfOk(t, dbump.Run(context.Background(), tc.cfg)) } } -func TestMigrateApplyAll(t *testing.T) { - wantLog := []string{ - "lockdb", "init", "getversion", - "dostep", "{v:1 q:'SELECT 1;' notx:true}", - "dostep", "{v:2 q:'SELECT 2;' notx:true}", - "dostep", "{v:3 q:'SELECT 3;' notx:true}", - "dostep", "{v:4 q:'SELECT 4;' notx:true}", - "dostep", "{v:5 q:'SELECT 5;' notx:true}", - "unlockdb", - } - - mm := &MockMigrator{} - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, - DisableTx: true, // for shorter wantLog - } - - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) +func TestMigrate_ApplyAll(t *testing.T) { + suite := tests.NewMigratorSuite(&tests.MockMigrator{}) + suite.ApplyAll(t) } -func TestMigrateUpWhenFull(t *testing.T) { - wantLog := []string{ - "lockdb", "init", "getversion", "unlockdb", - } - - mm := &MockMigrator{ +func TestMigrate_ApplyOne(t *testing.T) { + mm := &tests.MockMigrator{ VersionFn: func(ctx context.Context) (version int, err error) { - return 5, nil + return 3, nil }, } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, - } + suite := tests.NewMigratorSuite(mm) - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + suite.ApplyOne(t) } -func TestMigrateApplyN(t *testing.T) { - currVersion := 3 - wantLog := []string{ - "lockdb", "init", "getversion", - "dostep", "{v:4 q:'SELECT 4;' notx:false}", - "unlockdb", - } - - mm := &MockMigrator{ +func TestMigrate_ApplyAllWhenFull(t *testing.T) { + mm := &tests.MockMigrator{ VersionFn: func(ctx context.Context) (version int, err error) { - return currVersion, nil + return 5, nil }, } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyN, - Num: 1, - } + suite := tests.NewMigratorSuite(mm) - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + suite.ApplyAllWhenFull(t) } -func TestMigrateRevertAll(t *testing.T) { - wantLog := []string{ - "lockdb", "init", "getversion", - "dostep", "{v:4 q:'SELECT 50;' notx:true}", - "dostep", "{v:3 q:'SELECT 40;' notx:true}", - "dostep", "{v:2 q:'SELECT 30;' notx:true}", - "dostep", "{v:1 q:'SELECT 20;' notx:true}", - "dostep", "{v:0 q:'SELECT 10;' notx:true}", - "unlockdb", - } - - mm := &MockMigrator{ +func TestMigrate_RevertOne(t *testing.T) { + mm := &tests.MockMigrator{ VersionFn: func(ctx context.Context) (version int, err error) { - return 5, nil + return 3, nil }, } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeRevertAll, - DisableTx: true, // for shorter wantLog - } + suite := tests.NewMigratorSuite(mm) - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + suite.RevertOne(t) } -func TestMigrateRevertAllWhenEmpty(t *testing.T) { - wantLog := []string{ - "lockdb", "init", "getversion", "unlockdb", - } - - mm := &MockMigrator{ +func TestMigrate_RevertAllWhenEmpty(t *testing.T) { + mm := &tests.MockMigrator{ VersionFn: func(ctx context.Context) (version int, err error) { return 0, nil }, } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeRevertAll, - } + suite := tests.NewMigratorSuite(mm) - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + suite.RevertAllWhenEmpty(t) } -func TestMigrateRevertN(t *testing.T) { - currVersion := 3 - wantLog := []string{ - "lockdb", "init", "getversion", - "dostep", "{v:2 q:'SELECT 30;' notx:false}", - "unlockdb", - } - - mm := &MockMigrator{ +func TestMigrate_RevertAll(t *testing.T) { + mm := &tests.MockMigrator{ VersionFn: func(ctx context.Context) (version int, err error) { - return currVersion, nil + return 5, nil }, } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeRevertN, - Num: 1, - } - - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + suite := tests.NewMigratorSuite(mm) + suite.RevertAll(t) } -func TestMigrateRedo(t *testing.T) { - currVersion := 3 - wantLog := []string{ - "lockdb", "init", "getversion", - "dostep", "{v:2 q:'SELECT 30;' notx:false}", - "dostep", "{v:3 q:'SELECT 3;' notx:false}", - "unlockdb", - } - - mm := &MockMigrator{ +func TestMigrate_Redo(t *testing.T) { + mm := &tests.MockMigrator{ VersionFn: func(ctx context.Context) (version int, err error) { - return currVersion, nil + return 3, nil }, } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeRedo, - } - - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + suite := tests.NewMigratorSuite(mm) + suite.Redo(t) } -func TestMigrateDrop(t *testing.T) { - currVersion := 3 - wantLog := []string{ - "lockdb", "init", "getversion", - "dostep", "{v:2 q:'SELECT 30;' notx:false}", - "dostep", "{v:1 q:'SELECT 20;' notx:false}", - "dostep", "{v:0 q:'SELECT 10;' notx:false}", - "drop", - "unlockdb", - } - - mm := &MockMigrator{ +func TestMigrate_Drop(t *testing.T) { + mm := &tests.MockMigrator{ VersionFn: func(ctx context.Context) (version int, err error) { - return currVersion, nil + return 5, nil }, } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeDrop, - } - - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + suite := tests.NewMigratorSuite(mm) + suite.Drop(t) } func TestBeforeAfterStep(t *testing.T) { @@ -263,25 +157,25 @@ func TestBeforeAfterStep(t *testing.T) { "unlockdb", } - mm := &MockMigrator{ + mm := &tests.MockMigrator{ VersionFn: func(ctx context.Context) (version int, err error) { return currVersion, nil }, } - cfg := Config{ + cfg := dbump.Config{ Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, - BeforeStep: func(ctx context.Context, step Step) { - mm.log = append(mm.log, "before", fmt.Sprintf("{v:%d q:'%s' notx:%v}", step.Version, step.Query, step.DisableTx)) + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, + BeforeStep: func(ctx context.Context, step dbump.Step) { + mm.LogAdd("before", fmt.Sprintf("{v:%d q:'%s' notx:%v}", step.Version, step.Query, step.DisableTx)) }, - AfterStep: func(ctx context.Context, step Step) { - mm.log = append(mm.log, "after", fmt.Sprintf("{v:%d q:'%s' notx:%v}", step.Version, step.Query, step.DisableTx)) + AfterStep: func(ctx context.Context, step dbump.Step) { + mm.LogAdd("after", fmt.Sprintf("{v:%d q:'%s' notx:%v}", step.Version, step.Query, step.DisableTx)) }, } - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfErr(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestTimeout(t *testing.T) { @@ -291,8 +185,8 @@ func TestTimeout(t *testing.T) { "unlockdb", } - mm := &MockMigrator{ - DoStepFn: func(ctx context.Context, step Step) error { + mm := &tests.MockMigrator{ + DoStepFn: func(ctx context.Context, step dbump.Step) error { select { case <-time.After(30 * time.Second): return nil @@ -301,15 +195,34 @@ func TestTimeout(t *testing.T) { } }, } - cfg := Config{ + cfg := dbump.Config{ Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, Timeout: 20 * time.Millisecond, } - failIfOk(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfOk(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) +} + +func TestDisableTx(t *testing.T) { + wantLog := []string{ + "lockdb", "init", "getversion", + "dostep", "{v:1 q:'SELECT 1;' notx:true}", + "unlockdb", + } + + mm := &tests.MockMigrator{} + cfg := dbump.Config{ + Migrator: mm, + Loader: dbump.NewSliceLoader(testdataMigrations[:1]), + Mode: dbump.ModeApplyAll, + DisableTx: true, + } + + failIfErr(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestLockless(t *testing.T) { @@ -323,29 +236,29 @@ func TestLockless(t *testing.T) { "dostep", "{v:5 q:'SELECT 5;' notx:false}", } - mm := &MockMigrator{} - cfg := Config{ - Migrator: AsLocklessMigrator(mm), - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, + mm := &tests.MockMigrator{} + cfg := dbump.Config{ + Migrator: dbump.AsLocklessMigrator(mm), + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, } - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfErr(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestUseForce(t *testing.T) { currVersion := 3 wantLog := []string{ "lockdb", "unlockdb", "lockdb", "init", "getversion", - "dostep", "{v:4 q:'SELECT 4;' notx:true}", - "dostep", "{v:5 q:'SELECT 5;' notx:true}", + "dostep", "{v:4 q:'SELECT 4;' notx:false}", + "dostep", "{v:5 q:'SELECT 5;' notx:false}", "unlockdb", } isLocked := true - mm := &MockMigrator{ + mm := &tests.MockMigrator{ LockDBFn: func(ctx context.Context) error { if isLocked { return errors.New("cannot get lock") @@ -360,88 +273,86 @@ func TestUseForce(t *testing.T) { return currVersion, nil }, } - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, - UseForce: true, - DisableTx: true, // for shorter wantLog + cfg := dbump.Config{ + Migrator: mm, + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, + UseForce: true, } - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfErr(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestZigZag(t *testing.T) { wantLog := []string{ "lockdb", "init", "getversion", - "dostep", "{v:1 q:'SELECT 1;' notx:true}", - "dostep", "{v:0 q:'SELECT 10;' notx:true}", - "dostep", "{v:1 q:'SELECT 1;' notx:true}", + "dostep", "{v:1 q:'SELECT 1;' notx:false}", + "dostep", "{v:0 q:'SELECT 10;' notx:false}", + "dostep", "{v:1 q:'SELECT 1;' notx:false}", - "dostep", "{v:2 q:'SELECT 2;' notx:true}", - "dostep", "{v:1 q:'SELECT 20;' notx:true}", - "dostep", "{v:2 q:'SELECT 2;' notx:true}", + "dostep", "{v:2 q:'SELECT 2;' notx:false}", + "dostep", "{v:1 q:'SELECT 20;' notx:false}", + "dostep", "{v:2 q:'SELECT 2;' notx:false}", - "dostep", "{v:3 q:'SELECT 3;' notx:true}", - "dostep", "{v:2 q:'SELECT 30;' notx:true}", - "dostep", "{v:3 q:'SELECT 3;' notx:true}", + "dostep", "{v:3 q:'SELECT 3;' notx:false}", + "dostep", "{v:2 q:'SELECT 30;' notx:false}", + "dostep", "{v:3 q:'SELECT 3;' notx:false}", - "dostep", "{v:4 q:'SELECT 4;' notx:true}", - "dostep", "{v:3 q:'SELECT 40;' notx:true}", - "dostep", "{v:4 q:'SELECT 4;' notx:true}", + "dostep", "{v:4 q:'SELECT 4;' notx:false}", + "dostep", "{v:3 q:'SELECT 40;' notx:false}", + "dostep", "{v:4 q:'SELECT 4;' notx:false}", - "dostep", "{v:5 q:'SELECT 5;' notx:true}", - "dostep", "{v:4 q:'SELECT 50;' notx:true}", - "dostep", "{v:5 q:'SELECT 5;' notx:true}", + "dostep", "{v:5 q:'SELECT 5;' notx:false}", + "dostep", "{v:4 q:'SELECT 50;' notx:false}", + "dostep", "{v:5 q:'SELECT 5;' notx:false}", "unlockdb", } - mm := &MockMigrator{} - cfg := Config{ - Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, - DisableTx: true, // for shorter wantLog - ZigZag: true, + mm := &tests.MockMigrator{} + cfg := dbump.Config{ + Migrator: mm, + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, + ZigZag: true, } - failIfErr(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfErr(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestFailOnInitError(t *testing.T) { wantLog := []string{"lockdb", "init", "unlockdb"} - mm := &MockMigrator{ + mm := &tests.MockMigrator{ InitFn: func(ctx context.Context) error { return errors.New("no access") }, } - cfg := Config{ + cfg := dbump.Config{ Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, } - failIfOk(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfOk(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestFailOnLockDB(t *testing.T) { wantLog := []string{"lockdb"} - mm := &MockMigrator{ + mm := &tests.MockMigrator{ LockDBFn: func(ctx context.Context) (err error) { return errors.New("no access") }, } - cfg := Config{ + cfg := dbump.Config{ Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, } - failIfOk(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfOk(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestFailOnUnlockDB(t *testing.T) { @@ -451,7 +362,7 @@ func TestFailOnUnlockDB(t *testing.T) { "dostep", "{v:5 q:'SELECT 5;' notx:false}", "unlockdb", } - mm := &MockMigrator{ + mm := &tests.MockMigrator{ UnlockDBFn: func(ctx context.Context) (err error) { return errors.New("no access") }, @@ -459,33 +370,33 @@ func TestFailOnUnlockDB(t *testing.T) { return currVersion, nil }, } - cfg := Config{ + cfg := dbump.Config{ Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, } - failIfOk(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfOk(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestFailOnGetVersionError(t *testing.T) { wantLog := []string{ "lockdb", "init", "getversion", "unlockdb", } - mm := &MockMigrator{ + mm := &tests.MockMigrator{ VersionFn: func(ctx context.Context) (version int, err error) { return 0, errors.New("no access") }, } - cfg := Config{ + cfg := dbump.Config{ Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, } - failIfOk(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfOk(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestFailOnDoStepError(t *testing.T) { @@ -494,88 +405,89 @@ func TestFailOnDoStepError(t *testing.T) { "dostep", "{v:1 q:'SELECT 1;' notx:false}", "unlockdb", } - mm := &MockMigrator{ - DoStepFn: func(ctx context.Context, step Step) error { + mm := &tests.MockMigrator{ + DoStepFn: func(ctx context.Context, step dbump.Step) error { return errors.New("no access") }, } - cfg := Config{ + cfg := dbump.Config{ Migrator: mm, - Loader: NewSliceLoader(testdataMigrations), - Mode: ModeApplyAll, + Loader: dbump.NewSliceLoader(testdataMigrations), + Mode: dbump.ModeApplyAll, } - failIfOk(t, Run(context.Background(), cfg)) - mustEqual(t, mm.log, wantLog) + failIfOk(t, dbump.Run(context.Background(), cfg)) + mustEqual(t, mm.Log(), wantLog) } func TestFailOnLoad(t *testing.T) { - cfg := Config{ - Migrator: &MockMigrator{}, + cfg := dbump.Config{ + Migrator: &tests.MockMigrator{}, Loader: &MockLoader{ - LoaderFn: func() ([]*Migration, error) { + LoaderFn: func() ([]*dbump.Migration, error) { return nil, errors.New("forgot to commit") }, }, - Mode: ModeApplyAll, + Mode: dbump.ModeApplyAll, } - failIfOk(t, Run(context.Background(), cfg)) + failIfOk(t, dbump.Run(context.Background(), cfg)) } -func Test_loadMigrations(t *testing.T) { +func TestLoad(t *testing.T) { testCases := []struct { - testName string - migrations []*Migration - wantMigrations []*Migration - wantErr error + testName string + migrations []*dbump.Migration + wantErr error }{ - { - "ok (migrations are sorted)", - []*Migration{ - {ID: 2}, - {ID: 1}, - }, - []*Migration{ - {ID: 1}, - {ID: 2}, - }, - nil, - }, - { "fail (missing migration)", - []*Migration{ + []*dbump.Migration{ {ID: 3}, {ID: 1}, }, - nil, - errors.New("missing migration number: 2 (have 3)"), + errors.New("load: missing migration number: 2 (have 3)"), }, { "fail (duplicate id)", - []*Migration{ + []*dbump.Migration{ {ID: 2, Name: "mig2"}, {ID: 2, Name: "mig2fix"}, {ID: 1}, }, - nil, - errors.New("duplicate migration number: 2 (mig2)"), + errors.New("load: duplicate migration number: 2 (mig2fix)"), }, } for _, tc := range testCases { - m := mig{ - Loader: NewSliceLoader(tc.migrations), + cfg := dbump.Config{ + Migrator: tests.NewMockMigrator(nil), + Loader: dbump.NewSliceLoader(tc.migrations), + Mode: dbump.ModeApplyAll, } - migs, err := m.load() - mustEqual(t, err != nil, tc.wantErr != nil) - mustEqual(t, migs, tc.wantMigrations) + err := dbump.Run(context.Background(), cfg) + + switch { + case (err != nil) && (tc.wantErr != nil): + mustEqual(t, err.Error(), tc.wantErr.Error()) + case err != nil: + failIfErr(t, err) + default: + t.Fatal("want error") + } } } -var testdataMigrations = []*Migration{ +type MockLoader struct { + LoaderFn func() ([]*dbump.Migration, error) +} + +func (ml *MockLoader) Load() ([]*dbump.Migration, error) { + return ml.LoaderFn() +} + +var testdataMigrations = []*dbump.Migration{ { ID: 1, Name: `0001_init.sql`, diff --git a/load_test.go b/load_test.go index 3b4266c..8a808e4 100644 --- a/load_test.go +++ b/load_test.go @@ -1,12 +1,14 @@ -package dbump +package dbump_test import ( "embed" "testing" + + "github.com/cristalhq/dbump" ) func TestDiskLoader(t *testing.T) { - loader := NewDiskLoader("./testdata") + loader := dbump.NewDiskLoader("./testdata") migs, err := loader.Load() failIfErr(t, err) @@ -19,7 +21,7 @@ func TestDiskLoader(t *testing.T) { } func TestDiskLoaderSubdir(t *testing.T) { - loader := NewDiskLoader("./testdata/subdir") + loader := dbump.NewDiskLoader("./testdata/subdir") migs, err := loader.Load() failIfErr(t, err) @@ -35,7 +37,7 @@ func TestDiskLoaderSubdir(t *testing.T) { var testdata embed.FS func TestEmbedLoader(t *testing.T) { - loader := NewFileSysLoader(testdata, "testdata") + loader := dbump.NewFileSysLoader(testdata, "testdata") migs, err := loader.Load() failIfErr(t, err) @@ -48,7 +50,7 @@ func TestEmbedLoader(t *testing.T) { } func TestEmbedLoaderSubdir(t *testing.T) { - loader := NewFileSysLoader(testdata, "testdata/subdir") + loader := dbump.NewFileSysLoader(testdata, "testdata/subdir") migs, err := loader.Load() failIfErr(t, err) @@ -62,7 +64,7 @@ func TestEmbedLoaderSubdir(t *testing.T) { func TestSliceLoader(t *testing.T) { size := len(testdataMigrations) - loader := NewSliceLoader(testdataMigrations[:size-1]) + loader := dbump.NewSliceLoader(testdataMigrations[:size-1]) loader.AddMigration(testdataMigrations[size-1]) migs, err := loader.Load() @@ -77,7 +79,7 @@ func TestSliceLoader(t *testing.T) { } func TestBadFormat(t *testing.T) { - loader := NewFileSysLoader(testdata, "testdata/bad") + loader := dbump.NewFileSysLoader(testdata, "testdata/bad") _, err := loader.Load() failIfOk(t, err) } diff --git a/mock_test.go b/tests/mocks.go similarity index 61% rename from mock_test.go rename to tests/mocks.go index 420508a..a699895 100644 --- a/mock_test.go +++ b/tests/mocks.go @@ -1,11 +1,15 @@ -package dbump +package tests import ( "context" "fmt" + + "github.com/cristalhq/dbump" ) -var _ Migrator = &MockMigrator{} +const mockDoStepFmt = "{v:%d q:'%s' notx:%v}" + +var _ dbump.Migrator = &MockMigrator{} type MockMigrator struct { log []string @@ -15,9 +19,26 @@ type MockMigrator struct { InitFn func(ctx context.Context) error DropFn func(ctx context.Context) error VersionFn func(ctx context.Context) (version int, err error) - DoStepFn func(ctx context.Context, step Step) error + DoStepFn func(ctx context.Context, step dbump.Step) error +} + +func NewMockMigrator(m dbump.Migrator) *MockMigrator { + if m == nil { + return &MockMigrator{} + } + return &MockMigrator{ + LockDBFn: m.LockDB, + UnlockDBFn: m.UnlockDB, + InitFn: m.Init, + DropFn: m.Drop, + VersionFn: m.Version, + DoStepFn: m.DoStep, + } } +func (mm *MockMigrator) Log() []string { return mm.log } +func (mm *MockMigrator) LogAdd(s ...string) { mm.log = append(mm.log, s...) } + func (mm *MockMigrator) LockDB(ctx context.Context) error { mm.log = append(mm.log, "lockdb") if mm.LockDBFn == nil { @@ -58,18 +79,10 @@ func (mm *MockMigrator) Version(ctx context.Context) (version int, err error) { return mm.VersionFn(ctx) } -func (mm *MockMigrator) DoStep(ctx context.Context, step Step) error { - mm.log = append(mm.log, "dostep", fmt.Sprintf("{v:%d q:'%s' notx:%v}", step.Version, step.Query, step.DisableTx)) +func (mm *MockMigrator) DoStep(ctx context.Context, step dbump.Step) error { + mm.log = append(mm.log, "dostep", fmt.Sprintf(mockDoStepFmt, step.Version, step.Query, step.DisableTx)) if mm.DoStepFn == nil { return nil } return mm.DoStepFn(ctx, step) } - -type MockLoader struct { - LoaderFn func() ([]*Migration, error) -} - -func (ml *MockLoader) Load() ([]*Migration, error) { - return ml.LoaderFn() -} diff --git a/tests/tests.go b/tests/tests.go new file mode 100644 index 0000000..f87db63 --- /dev/null +++ b/tests/tests.go @@ -0,0 +1,277 @@ +package tests + +import ( + "context" + "fmt" + "reflect" + "testing" + + "github.com/cristalhq/dbump" +) + +type MigratorSuite struct { + migrator dbump.Migrator + ApplyTmpl string + RevertTmpl string + CleanMigTmpl string + CleanTest string +} + +func NewMigratorSuite(m dbump.Migrator) *MigratorSuite { + return &MigratorSuite{ + migrator: m, + // some default harmless queries + ApplyTmpl: "SELECT %[2]d;", + RevertTmpl: "SELECT %[2]d0;", + } +} + +func (suite *MigratorSuite) ApplyAll(t *testing.T) { + migs := suite.genMigrations(t, 5, "apply_all") + + wantLog := []string{"lockdb", "init", "getversion"} + for i, m := range migs { + v := fmt.Sprintf(mockDoStepFmt, i+1, m.Apply, false) + wantLog = append(wantLog, "dostep", v) + } + wantLog = append(wantLog, "unlockdb") + + mig := suite.getMockedMigrator() + failIfErr(t, dbump.Run(context.Background(), dbump.Config{ + Migrator: mig, + Loader: dbump.NewSliceLoader(migs), + Mode: dbump.ModeApplyAll, + })) + mustEqual(t, mig.Log(), wantLog) +} + +func (suite *MigratorSuite) ApplyOne(t *testing.T) { + migs := suite.genMigrations(t, 5, "apply_one") + curr := 3 + suite.prepare(t, migs[:curr]) + + wantLog := []string{"lockdb", "init", "getversion"} + v := fmt.Sprintf(mockDoStepFmt, 4, migs[3].Apply, false) + wantLog = append(wantLog, "dostep", v) + wantLog = append(wantLog, "unlockdb") + + mig := suite.getMockedMigrator() + failIfErr(t, dbump.Run(context.Background(), dbump.Config{ + Migrator: mig, + Loader: dbump.NewSliceLoader(migs), + Mode: dbump.ModeApplyN, + Num: 1, + })) + mustEqual(t, mig.Log(), wantLog) +} + +func (suite *MigratorSuite) ApplyAllWhenFull(t *testing.T) { + migs := suite.genMigrations(t, 5, "apply_all_when_full") + suite.prepare(t, migs) + + wantLog := []string{"lockdb", "init", "getversion", "unlockdb"} + + mig := suite.getMockedMigrator() + failIfErr(t, dbump.Run(context.Background(), dbump.Config{ + Migrator: mig, + Loader: dbump.NewSliceLoader(migs), + Mode: dbump.ModeApplyAll, + })) + mustEqual(t, mig.Log(), wantLog) +} + +func (suite *MigratorSuite) RevertOne(t *testing.T) { + migs := suite.genMigrations(t, 5, "revert_one") + suite.prepare(t, migs[:3]) + + wantLog := []string{"lockdb", "init", "getversion"} + v := fmt.Sprintf(mockDoStepFmt, 2, migs[2].Revert, false) + wantLog = append(wantLog, "dostep", v) + wantLog = append(wantLog, "unlockdb") + + mig := suite.getMockedMigrator() + failIfErr(t, dbump.Run(context.Background(), dbump.Config{ + Migrator: mig, + Loader: dbump.NewSliceLoader(migs), + Mode: dbump.ModeRevertN, + Num: 1, + })) + mustEqual(t, mig.Log(), wantLog) +} + +func (suite *MigratorSuite) RevertAllWhenEmpty(t *testing.T) { + migs := suite.genMigrations(t, 5, "revert_all_when_empty") + + wantLog := []string{"lockdb", "init", "getversion", "unlockdb"} + + mig := suite.getMockedMigrator() + failIfErr(t, dbump.Run(context.Background(), dbump.Config{ + Migrator: mig, + Loader: dbump.NewSliceLoader(migs), + Mode: dbump.ModeRevertAll, + })) + mustEqual(t, mig.Log(), wantLog) +} + +func (suite *MigratorSuite) RevertAll(t *testing.T) { + migs := suite.genMigrations(t, 5, "revert_all") + suite.prepare(t, migs) + + wantLog := []string{"lockdb", "init", "getversion"} + for _, m := range reverse(migs) { + v := fmt.Sprintf(mockDoStepFmt, m.ID-1, m.Revert, false) + wantLog = append(wantLog, "dostep", v) + } + wantLog = append(wantLog, "unlockdb") + + mig := suite.getMockedMigrator() + failIfErr(t, dbump.Run(context.Background(), dbump.Config{ + Migrator: mig, + Loader: dbump.NewSliceLoader(migs), + Mode: dbump.ModeRevertAll, + })) + mustEqual(t, mig.Log(), wantLog) +} + +func (suite *MigratorSuite) Redo(t *testing.T) { + migs := suite.genMigrations(t, 5, "redo") + suite.prepare(t, migs[:3]) + + wantLog := []string{"lockdb", "init", "getversion"} + { + v := fmt.Sprintf(mockDoStepFmt, 2, migs[2].Revert, false) + wantLog = append(wantLog, "dostep", v) + v = fmt.Sprintf(mockDoStepFmt, 3, migs[2].Apply, false) + wantLog = append(wantLog, "dostep", v) + } + wantLog = append(wantLog, "unlockdb") + + mig := suite.getMockedMigrator() + failIfErr(t, dbump.Run(context.Background(), dbump.Config{ + Migrator: mig, + Loader: dbump.NewSliceLoader(migs), + Mode: dbump.ModeRedo, + })) + mustEqual(t, mig.Log(), wantLog) +} + +func (suite *MigratorSuite) Drop(t *testing.T) { + migs := suite.genMigrations(t, 5, "drop") + suite.prepare(t, migs) + + wantLog := []string{"lockdb", "init", "getversion"} + for i := 4; i >= 0; i-- { + v := fmt.Sprintf(mockDoStepFmt, i, migs[i].Revert, false) + wantLog = append(wantLog, "dostep", v) + } + wantLog = append(wantLog, "drop", "unlockdb") + + mig := suite.getMockedMigrator() + failIfErr(t, dbump.Run(context.Background(), dbump.Config{ + Migrator: mig, + Loader: dbump.NewSliceLoader(migs), + Mode: dbump.ModeDrop, + })) + mustEqual(t, mig.Log(), wantLog) +} + +// TODO: +// func TestTimeout(t *testing.T) { +// wantLog := []string{ +// "lockdb", "init", "getversion", +// "dostep", "{v:1 q:'SELECT 1;' notx:false}", +// "unlockdb", +// } + +// mm := &MockMigrator{ +// DoStepFn: func(ctx context.Context, step dbump.Step) error { +// select { +// case <-time.After(30 * time.Second): +// return nil +// case <-ctx.Done(): +// return ctx.Err() +// } +// }, +// } +// cfg := dbump.Config{ +// Migrator: mm, +// // Loader: dbump.NewSliceLoader(testdataMigrations), +// Mode: dbump.ModeApplyAll, +// Timeout: 20 * time.Millisecond, +// } + +// failIfOk(t, dbump.Run(context.Background(), cfg)) +// mustEqual(t, mm.log, wantLog) +// } + +// apply given migrations, used only at the beginning of the test. +func (suite *MigratorSuite) prepare(tb testing.TB, migs []*dbump.Migration) { + failIfErr(tb, dbump.Run(context.Background(), dbump.Config{ + Migrator: suite.getMockedMigrator(), + Loader: dbump.NewSliceLoader(migs), + Mode: dbump.ModeApplyAll, + })) +} + +func (suite *MigratorSuite) getMockedMigrator() *MockMigrator { + // hack for dbump package tests, we do not need to wrap MockMigrator + if mm, ok := suite.migrator.(*MockMigrator); ok { + mm.log = []string{} // flush log for next assert + return mm + } + return NewMockMigrator(suite.migrator) +} + +func (suite *MigratorSuite) genMigrations(tb testing.TB, num int, testname string) []*dbump.Migration { + res := make([]*dbump.Migration, 0, num) + for i := 1; i <= num; i++ { + res = append(res, &dbump.Migration{ + ID: i, + Name: fmt.Sprintf("test-mig-%d", i), + Apply: fmt.Sprintf(suite.ApplyTmpl, testname, i), + Revert: fmt.Sprintf(suite.RevertTmpl, testname, i), + }) + } + + tb.Cleanup(func() { + for i := 1; i <= num; i++ { + query := fmt.Sprintf(suite.CleanMigTmpl, testname, i) + failIfErr(tb, suite.migrator.DoStep(context.Background(), dbump.Step{ + Query: query, + })) + } + failIfErr(tb, suite.migrator.DoStep(context.Background(), dbump.Step{ + Query: suite.CleanTest, + })) + }) + return res +} + +func reverse(migs []*dbump.Migration) []*dbump.Migration { + res := make([]*dbump.Migration, len(migs)) + for i := range migs { + res[i] = migs[len(migs)-i-1] + } + return res +} + +func failIfOk(t testing.TB, err error) { + t.Helper() + if err == nil { + t.Fail() + } +} + +func failIfErr(t testing.TB, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} + +func mustEqual(t testing.TB, got, want interface{}) { + t.Helper() + if !reflect.DeepEqual(got, want) { + t.Fatalf("\nhave %+v\nwant %+v", got, want) + } +}