Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

atlasaction: migrate apply command #64

Merged
merged 5 commits into from
Sep 23, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
36 changes: 36 additions & 0 deletions atlasaction/action.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package atlasaction
rotemtam marked this conversation as resolved.
Show resolved Hide resolved

import (
"context"
_ "embed"
"fmt"
"strconv"

"ariga.io/atlas-go-sdk/atlasexec"
"github.com/sethvargo/go-githubactions"
)

// MigrateApply runs the GitHub Action for "ariga/atlas-action/migrate/apply".
func MigrateApply(ctx context.Context, client *atlasexec.Client, act *githubactions.Action) error {
params := &atlasexec.MigrateApplyParams{
URL: act.GetInput("url"),
DirURL: act.GetInput("dir"),
TxMode: act.GetInput("tx-mode"), // Hidden param.
BaselineVersion: act.GetInput("baseline"), // Hidden param.
}
rotemtam marked this conversation as resolved.
Show resolved Hide resolved
run, err := client.MigrateApply(ctx, params)
if err != nil {
act.SetOutput("error", err.Error())
return err
}
if run.Error != "" {
act.SetOutput("error", run.Error)
return fmt.Errorf("run failed: %s", run.Error)
}
act.SetOutput("current", run.Current)
act.SetOutput("target", run.Target)
act.SetOutput("pending_count", strconv.Itoa(len(run.Pending)))
act.SetOutput("applied_count", strconv.Itoa(len(run.Applied)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are all these outputs? Don’t we have a way to run atlasexec with standard Atlas output?

act.Infof("Run complete: +%v", run)
return nil
}
159 changes: 159 additions & 0 deletions atlasaction/action_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package atlasaction

import (
"bytes"
"context"
"database/sql"
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"ariga.io/atlas-go-sdk/atlasexec"
_ "github.com/mattn/go-sqlite3"
"github.com/sethvargo/go-githubactions"
"github.com/stretchr/testify/require"
)

func TestMigrateApply(t *testing.T) {
t.Run("local dir", func(t *testing.T) {
tt := newT(t)
tt.setInput("url", "sqlite://"+tt.db)
tt.setInput("dir", "file://testdata/migrations/")

err := MigrateApply(context.Background(), tt.cli, tt.act)
require.NoError(t, err)

m, err := tt.outputs()
require.EqualValues(t, map[string]string{
"error": "",
}, m)
})
t.Run("tx-mode", func(t *testing.T) {
tt := newT(t)
tt.setInput("url", "sqlite://"+tt.db)
tt.setInput("dir", "file://testdata/migrations/")
tt.setInput("tx-mode", "fake")
err := MigrateApply(context.Background(), tt.cli, tt.act)

// An error here proves that the tx-mode was passed to atlasexec, which
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought we were aiming for tests that use an HTTP server and check that calls were made?

Also, relying on Atlas raw error strings seem fragile to me. These error strings could easily be changed in the Atlas repository and no one would think that this affects this repository

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test checks a local flow, cloud mode is added in one of the next PRs which will use a local HTTP server.

re the tests fragility (I prefer the Google-y term "brittle", I agree it's not iron-clad. However, there is a trade-off, not relying on any error message (only the exit code) doesn't prove the argument is passed. Relying on mocking on the other hand is less brittle, but has its own costs and risks which can be discussed.

In this case, the risk of the test breaking because of an upstream error message change is low and has no impact on the user, so I opt for the simple way even if it is not 100% textbook (In general, I try to be practical and not religious about "The Right Way™")

// is what we want to test.
exp := `run failed: unknown tx-mode "fake"`
require.EqualError(t, err, exp)
m, err := tt.outputs()
require.EqualValues(t, map[string]string{
"error": exp,
}, m)
})
t.Run("baseline", func(t *testing.T) {
tt := newT(t)
tt.setInput("url", "sqlite://"+tt.db)
tt.setInput("dir", "file://testdata/migrations/")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpicking a bit, but also a honest question since I had an opposite comment about that in one of my PRs - the raw values here are duplicated several times, why not extract them to a const?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have hard rules about when to use a constant and when to reuse a value. Places where it's more obvious to use a constant are when the value is used again downstream in a comparison in which case the const doesn't only save repetition (tho the constant itself is repeated), it proves correctnes (we compare to the same third value which formally must be equal and not prone to typos).

Another thing to consider is readability. Extracting to a constant sometimes makes the code less readable because you need to jump in the file to understand what that is. In this case, I think its important because the reader can understand which directory I'm referring to. It is very likely that this test func will have many cases with different migration directories to show different use cases.

tt.setInput("baseline", "111_fake")
err := MigrateApply(context.Background(), tt.cli, tt.act)

// An error here proves that the baseline was passed to atlasexec, which
// is what we want to test.
exp := `atlasexec: baseline version "111_fake" not found`
require.EqualError(t, err, exp)
m, err := tt.outputs()
require.EqualValues(t, map[string]string{
"error": exp,
}, m)
})

}

// sqlitedb returns a path to an initialized sqlite database file. The file is
// created in a temporary directory and will be deleted when the test finishes.
func sqlitedb(t *testing.T) string {
td := t.TempDir()
dbpath := filepath.Join(td, "file.db")
_, err := sql.Open("sqlite3", fmt.Sprintf("file:%s?cache=shared&_fk=1", dbpath))
require.NoError(t, err)
return dbpath
}

type test struct {
db string
env map[string]string
out bytes.Buffer
cli *atlasexec.Client
act *githubactions.Action
}

func newT(t *testing.T) *test {
outputFile, err := os.CreateTemp("", "")
require.NoError(t, err)
tt := &test{
db: sqlitedb(t),
env: map[string]string{
"GITHUB_OUTPUT": outputFile.Name(),
},
}
tt.act = githubactions.New(
githubactions.WithGetenv(func(key string) string {
return tt.env[key]
}),
githubactions.WithWriter(&tt.out),
)
cli, err := atlasexec.NewClient("", "atlas")
require.NoError(t, err)
tt.cli = cli
return tt
}

func (t *test) setInput(k, v string) {
t.env["INPUT_"+strings.ToUpper(k)] = v
}

// outputs is a helper that parses the GitHub Actions output file format. This is
// used to parse the output file written by the action.
func (t *test) outputs() (map[string]string, error) {
var (
key string
value strings.Builder
token = "_GitHubActionsFileCommandDelimeter_"
)
m := make(map[string]string)
c, err := os.ReadFile(t.env["GITHUB_OUTPUT"])
if err != nil {
return nil, err
}
lines := strings.Split(string(c), "\n")
for _, line := range lines {
if delim := "<<" + token; strings.Contains(line, delim) {
key = strings.TrimSpace(strings.Split(line, delim)[0])
continue
}
if strings.Contains(line, token) {
m[key] = strings.TrimSpace(value.String())
value.Reset()
continue
}
value.WriteString(line)
}
return m, nil
}

func TestParseGitHubOutputFile(t *testing.T) {
tt := newT(t)
tt.act.SetOutput("foo", "bar")
tt.act.SetOutput("baz", "qux")
out, err := tt.outputs()
require.NoError(t, err)
require.EqualValues(t, map[string]string{
"foo": "bar",
"baz": "qux",
}, out)
}

func TestSetInput(t *testing.T) {
tt := newT(t)
tt.setInput("hello-world", "greetings")
tt.setInput("goodbye-friends", "farewell")

require.Equal(t, "greetings", tt.act.GetInput("hello-world"))
require.Equal(t, "farewell", tt.act.GetInput("goodbye-friends"))
}
1 change: 1 addition & 0 deletions atlasaction/testdata/migrations/20230922132634_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
create table t1 ( c int );
2 changes: 2 additions & 0 deletions atlasaction/testdata/migrations/atlas.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
h1:Ooin+h0Z8qqTx7D9A6xXgHosLpMqvOtEfJSaSAsiGHU=
20230922132634_init.sql h1:Q+dJaaJDja1u1qEni6E0SfC4dMXhHgW2F1ybAtgcgeE=
51 changes: 45 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,63 @@ module ariga.io/atlas-action
go 1.21.1

require (
ariga.io/atlas-go-sdk v0.1.0
ariga.io/atlas v0.12.2-0.20230806193313-117e03f96e45
ariga.io/atlas-go-sdk v0.1.1-0.20230918080233-877323176ff4
github.com/alecthomas/kong v0.8.0
github.com/mattn/go-sqlite3 v1.14.17
github.com/sethvargo/go-githubactions v1.1.0
github.com/stretchr/testify v1.8.4
)

require (
ariga.io/atlas v0.12.1 // indirect
ariga.io/atlas/cmd/atlas v0.13.1 // indirect
github.com/agext/levenshtein v1.2.1 // indirect
github.com/antlr/antlr4/runtime/Go/antlr v0.0.0-20220816024939-bc8df83d7b9d // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/auxten/postgresql-parser v1.0.1 // indirect
github.com/benbjohnson/clock v1.1.0 // indirect
github.com/certifi/gocertifi v0.0.0-20200922220541-2c3bb06c6054 // indirect
github.com/cockroachdb/apd v1.1.1-0.20181017181144-bced77f817b4 // indirect
github.com/cockroachdb/errors v1.8.2 // indirect
github.com/cockroachdb/logtags v0.0.0-20190617123548-eb05cc24525f // indirect
github.com/cockroachdb/redact v1.0.8 // indirect
github.com/cockroachdb/sentry-go v0.6.1-cockroachdb.2 // indirect
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/getsentry/raven-go v0.2.0 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/google/go-cmp v0.5.8 // indirect
github.com/hashicorp/hcl/v2 v2.10.0 // indirect
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
github.com/hashicorp/hcl/v2 v2.13.0 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.7 // indirect
github.com/mitchellh/go-wordwrap v1.0.0 // indirect
github.com/pingcap/errors v0.11.5-0.20210425183316-da1aaba5fb63 // indirect
github.com/pingcap/log v0.0.0-20210625125904-98ed8e2eb1c7 // indirect
github.com/pingcap/tidb/parser v0.0.0-20220817134052-9709249e523a // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
github.com/rogpeppe/go-internal v1.6.1 // indirect
github.com/sethvargo/go-envconfig v0.9.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/zclconf/go-cty v1.8.0 // indirect
golang.org/x/text v0.8.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.8.0 // indirect
go.uber.org/zap v1.21.0 // indirect
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.1.0 // indirect
golang.org/x/sys v0.7.0 // indirect
golang.org/x/text v0.9.0 // indirect
google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 // indirect
google.golang.org/grpc v1.48.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading
Loading