Skip to content
This repository has been archived by the owner on Aug 12, 2022. It is now read-only.

Commit

Permalink
feat: Test migration file structure (#217)
Browse files Browse the repository at this point in the history
* feat: Test migration file structure

* s-witch hunting

* handle unordered results

* stricter checks

* clean up, parallel() in the right place

Co-authored-by: Kemal Hadimli <[email protected]>
  • Loading branch information
disq and disq authored Mar 25, 2022
1 parent a14ff90 commit fd7955e
Show file tree
Hide file tree
Showing 2 changed files with 169 additions and 0 deletions.
85 changes: 85 additions & 0 deletions migration/testbuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ package migration

import (
"context"
"fmt"
"os"
"strings"
"testing"

"github.com/cloudquery/cq-provider-sdk/database"
Expand Down Expand Up @@ -117,6 +119,10 @@ func doMigrationsTest(t *testing.T, ctx context.Context, dsn string, prov *provi
migFiles, err := migrator.ReadMigrationFiles(hclog.L(), prov.Migrations)
assert.NoError(t, err)

t.Run("FileStructure", func(t *testing.T) {
checkFileStructure(t, migFiles)
})

mig, err := migrator.New(hclog.L(), dialect, migFiles, dsn, prov.Name)
assert.NoError(t, err)
if t.Failed() {
Expand Down Expand Up @@ -181,3 +187,82 @@ func requireAllPKsToHaveColumn(t *testing.T, ctx context.Context, conn *pgxpool.
assert.NoError(t, err)
assert.Empty(t, res)
}

func checkFileStructure(t *testing.T, migrationFiles map[string]map[string][]byte) {
for dialectKey := range migrationFiles {
t.Run("dialect:"+dialectKey, func(t *testing.T) {
t.Parallel()
errs := checkFileStructureForDialect(migrationFiles[dialectKey])
if len(errs) > 0 {
t.Fail()
for _, e := range errs {
t.Log(e.Error())
}
}
})
}
}

func checkFileStructureForDialect(migrationFiles map[string][]byte) []error {
// each file should have an 'up' and a 'down'
// each version should be mentioned only once
// no rogue files, only <int>_<version>.(up|down).sql
const (
hasUp = 1
hasDown = 2
hasAll = hasUp | hasDown
)
fileUpDownness := make(map[string]int)
versionVsIdUp := make(map[string][]string)
versionVsIdDown := make(map[string][]string)
for fn := range migrationFiles {
fnParts := strings.SplitN(fn, "_", 2)
if len(fnParts) != 2 {
return []error{fmt.Errorf("invalid filename format %q: less than 2 underscores", fn)}
}
switch {
case strings.HasSuffix(fnParts[1], ".up.sql"):
fileUpDownness[fnParts[0]] |= hasUp
version := strings.TrimSuffix(fnParts[1], ".up.sql")
if !strings.HasPrefix(version, "v") {
return []error{fmt.Errorf("invalid filename format %q: version should start with v", fn)}
}
versionVsIdUp[version] = append(versionVsIdUp[version], fnParts[0])
case strings.HasSuffix(fnParts[1], ".down.sql"):
fileUpDownness[fnParts[0]] |= hasDown
version := strings.TrimSuffix(fnParts[1], ".down.sql")
if !strings.HasPrefix(version, "v") {
return []error{fmt.Errorf("invalid filename format %q: version should start with v", fn)}
}
versionVsIdDown[version] = append(versionVsIdDown[version], fnParts[0])
default:
return []error{fmt.Errorf("invalid filename format %q: neither up or down migration", fn)}
}
}

var retErrs []error
for id, flags := range fileUpDownness {
if (flags & hasAll) == hasAll {
continue
}
switch {
case (flags & hasUp) == 0:
retErrs = append(retErrs, fmt.Errorf("migration id %q is missing up migration", id))
case (flags & hasDown) == 0:
retErrs = append(retErrs, fmt.Errorf("migration id %q is missing down migration", id))
default:
retErrs = append(retErrs, fmt.Errorf("migration id %q unhandled error", id))
}
}
for version, ids := range versionVsIdUp {
if len(ids) > 1 {
retErrs = append(retErrs, fmt.Errorf("migration version %q (up) is mentioned in multiple versions: %s", version, strings.Join(ids, ", ")))
}
}
for version, ids := range versionVsIdDown {
if len(ids) > 1 {
retErrs = append(retErrs, fmt.Errorf("migration version %q (down) is mentioned in multiple versions: %s", version, strings.Join(ids, ", ")))
}
}
return retErrs
}
84 changes: 84 additions & 0 deletions migration/testbuilder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package migration

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCheckFileStructure(t *testing.T) {
cases := []struct {
filenames []string
expecterrors []string
}{
{
filenames: []string{"1_v1.0.0.up.sql", "1_v1.0.0.down.sql", "2_v1.2.0.up.sql", "2_v1.2.0.down.sql"},
},
{
filenames: []string{"1.up.sql", "1.down.sql"},
expecterrors: []string{"less than 2 underscores"},
},
{
filenames: []string{"1_123.up.sql", "1_123.down.sql"},
expecterrors: []string{"version should start with v"},
},
{
filenames: []string{"1_v0.0.1.up.sql", "1_v0.0.1.txt"},
expecterrors: []string{"neither up or down migration"},
},
{
filenames: []string{"1_v1.0.0.up.sql", "1_v1.0.0.down.sql", "2_v1.2.0.up.sql", "3_v1.2.1.up.sql", "3_v1.2.1.down.sql"},
expecterrors: []string{"missing down migration"},
},
{
filenames: []string{"1_v1.0.0.up.sql", "1_v1.0.0.down.sql", "2_v1.2.0.down.sql"},
expecterrors: []string{"missing up migration"},
},
{
filenames: []string{"1_v0.0.1.up.sql", "2_v0.0.1.down.sql"},
expecterrors: []string{"missing down migration", "missing up migration"},
},
{
filenames: []string{"1_v1.0.0.up.sql", "1_v1.0.0.down.sql", "2_v1.2.0.up.sql", "2_v1.2.0.down.sql", "3_v1.2.0.up.sql", "3_v1.2.0.down.sql"},
expecterrors: []string{"mentioned in multiple versions", "mentioned in multiple versions"},
},
{
filenames: []string{"1_v1.0.0.up.sql", "1_v1.0.0.down.sql", "2_v1.2.0.up.sql", "3_v1.2.0.up.sql", "3_v1.2.0.down.sql"},
expecterrors: []string{"missing down migration", "mentioned in multiple versions"},
},
{
filenames: []string{"21_v0.10.12.down.sql", "21_v0.10.12.up.sql", "22_v0.10.14.down.sql", "22_v0.10.14.up.sql", "23_v0.10.14.down.sql", "23_v0.10.15.up.sql"},
expecterrors: []string{"is mentioned in multiple versions"},
},
}

for _, tc := range cases {
mf := make(map[string][]byte, len(tc.filenames))
for _, fn := range tc.filenames {
mf[fn] = nil
}

errs := checkFileStructureForDialect(mf)
assert.Equal(t, len(tc.expecterrors), len(errs))
if t.Failed() {
t.Log(errs)
t.FailNow()
}

matches := 0
for _, err := range errs {
for _, expected := range tc.expecterrors {
if strings.Contains(err.Error(), expected) {
matches++
break
}
}
}
assert.Equal(t, len(tc.expecterrors), matches)
if t.Failed() {
t.Log(errs)
t.FailNow()
}
}
}

0 comments on commit fd7955e

Please sign in to comment.