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

feat: Test migration file structure #217

Merged
merged 5 commits into from
Mar 25, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
}
}
}