Repository: ariga/atlas
Pull request #530 (2022-01-30 12:31:44)

cmd/action: add unit tests for diff command
This PR adds unit tests for the `schema diff` command. It changes some things that I would like to modify in other commands, cc @yonidavidson.

The `*cobra.Command` is created by a constructor where input flags are passed as arguments, such that it can be more easily tested and relies less on global variables (which I know are common in CLI apps in Go but I personally dislike). 


Patch:
diff --git a/cmd/action/apply.go b/cmd/action/apply.go
index e9f8152a..938b4c60 100644
--- a/cmd/action/apply.go
+++ b/cmd/action/apply.go
@@ -1,3 +1,7 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
 package action
 
 import (
diff --git a/cmd/action/diff.go b/cmd/action/diff.go
index df2fb77c..ef5319d6 100644
--- a/cmd/action/diff.go
+++ b/cmd/action/diff.go
@@ -11,41 +11,46 @@ import (
 	"github.com/spf13/cobra"
 )
 
-var (
-	// DiffFlags are the flags used in the Diff command.
-	DiffFlags struct {
-		FromDSN string
-		ToDSN   string
-	}
-	// DiffCmd represents the diff command.
-	DiffCmd = &cobra.Command{
+type diffCmdOpts struct {
+	fromDSN string
+	toDSN   string
+}
+
+// newDiffCmd returns a new *cobra.Command that runs cmdDiffRun with the given flags and mux.
+func newDiffCmd() *cobra.Command {
+	var opts diffCmdOpts
+	cmd := &cobra.Command{
 		Use:   "diff",
 		Short: "Calculate and print the diff between two schemas.",
 		Long: "`atlas schema diff`" + ` connects to two given databases, inspects
 them, calculates the difference in their schemas, and prints a plan of
 SQL queries to bring the "from" database to the schema of the "to" database.`,
-		Run: CmdDiffRun,
+		Run: func(cmd *cobra.Command, args []string) {
+			cmdDiffRun(cmd, &opts)
+		},
 	}
-)
+	cmd.Flags().StringVarP(&opts.fromDSN, "from", "", "", "[driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format")
+	cmd.Flags().StringVarP(&opts.toDSN, "to", "", "", "[driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format")
+	cobra.CheckErr(cmd.MarkFlagRequired("from"))
+	cobra.CheckErr(cmd.MarkFlagRequired("to"))
+	return cmd
+}
 
 func init() {
-	schemaCmd.AddCommand(DiffCmd)
-	DiffCmd.Flags().StringVarP(&DiffFlags.FromDSN, "from", "", "", "[driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format")
-	DiffCmd.Flags().StringVarP(&DiffFlags.ToDSN, "to", "", "", "[driver://username:password@protocol(address)/dbname?param=value] Select data source using the dsn format")
-	cobra.CheckErr(DiffCmd.MarkFlagRequired("from"))
-	cobra.CheckErr(DiffCmd.MarkFlagRequired("to"))
+	diffCmd := newDiffCmd()
+	schemaCmd.AddCommand(diffCmd)
 }
 
-// CmdDiffRun connects to the given databases, and prints an SQL plan to get from
+// cmdDiffRun connects to the given databases, and prints an SQL plan to get from
 // the "from" schema to the "to" schema.
-func CmdDiffRun(cmd *cobra.Command, args []string) {
-	fromDriver, err := defaultMux.OpenAtlas(DiffFlags.FromDSN)
+func cmdDiffRun(cmd *cobra.Command, flags *diffCmdOpts) {
+	fromDriver, err := defaultMux.OpenAtlas(flags.fromDSN)
 	cobra.CheckErr(err)
-	toDriver, err := defaultMux.OpenAtlas(DiffFlags.ToDSN)
+	toDriver, err := defaultMux.OpenAtlas(flags.toDSN)
 	cobra.CheckErr(err)
-	fromName, err := SchemaNameFromDSN(DiffFlags.FromDSN)
+	fromName, err := SchemaNameFromDSN(flags.fromDSN)
 	cobra.CheckErr(err)
-	toName, err := SchemaNameFromDSN(DiffFlags.ToDSN)
+	toName, err := SchemaNameFromDSN(flags.toDSN)
 	cobra.CheckErr(err)
 	ctx := context.Background()
 	fromSchema, err := fromDriver.InspectSchema(ctx, fromName, nil)
@@ -64,7 +69,7 @@ func CmdDiffRun(cmd *cobra.Command, args []string) {
 	p, err := toDriver.PlanChanges(ctx, "plan", diff)
 	cobra.CheckErr(err)
 	if len(p.Changes) == 0 {
-		schemaCmd.Println("Schemas are synced, no changes to be made.")
+		cmd.Println("Schemas are synced, no changes to be made.")
 		return
 	}
 	for _, c := range p.Changes {
diff --git a/cmd/action/env.go b/cmd/action/env.go
index 537afe41..0a1dfaf6 100644
--- a/cmd/action/env.go
+++ b/cmd/action/env.go
@@ -1,3 +1,7 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
 package action
 
 import (
diff --git a/cmd/action/inspect.go b/cmd/action/inspect.go
index 4dd48c55..76e1dbea 100644
--- a/cmd/action/inspect.go
+++ b/cmd/action/inspect.go
@@ -1,3 +1,7 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
 package action
 
 import (
diff --git a/cmd/action/mux.go b/cmd/action/mux.go
index ae33aecd..158e316f 100644
--- a/cmd/action/mux.go
+++ b/cmd/action/mux.go
@@ -1,3 +1,7 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
 package action
 
 import (
diff --git a/cmd/action/provider.go b/cmd/action/provider.go
index 619e46c1..f3290bbe 100644
--- a/cmd/action/provider.go
+++ b/cmd/action/provider.go
@@ -1,3 +1,7 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
 package action
 
 import (
diff --git a/cmd/action/root.go b/cmd/action/root.go
index 754f5721..6ce5e1e9 100644
--- a/cmd/action/root.go
+++ b/cmd/action/root.go
@@ -1,3 +1,7 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
 package action
 
 import (
diff --git a/cmd/action/schema.go b/cmd/action/schema.go
index 471a2035..b4ca2fe5 100644
--- a/cmd/action/schema.go
+++ b/cmd/action/schema.go
@@ -1,3 +1,7 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
 package action
 
 import (
diff --git a/cmd/action/version.go b/cmd/action/version.go
index edf78ec0..db7539c3 100644
--- a/cmd/action/version.go
+++ b/cmd/action/version.go
@@ -1,3 +1,7 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
 package action
 
 import (


Test patch:
diff --git a/cmd/action/diff_test.go b/cmd/action/diff_test.go
new file mode 100644
index 00000000..d94107e6
--- /dev/null
+++ b/cmd/action/diff_test.go
@@ -0,0 +1,67 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
+package action
+
+import (
+	"bytes"
+	"context"
+	"database/sql"
+	"fmt"
+	"io/ioutil"
+	"os"
+	"testing"
+
+	"ariga.io/atlas/sql/sqlite"
+	"github.com/spf13/cobra"
+	"github.com/stretchr/testify/require"
+)
+
+func TestDiffCmd_Diff(t *testing.T) {
+	from := openSQLite(t, "")
+	to := openSQLite(t, "create table t1 (id int);")
+	cmd := newDiffCmd()
+	s, err := runCmd(cmd, "schema", "diff", "--from", from, "--to", to)
+	require.NoError(t, err)
+	require.EqualValues(t, "-- Create \"t1\" table\nCREATE TABLE `t1` (`id` int NULL)\n", s)
+}
+
+func TestDiffCmd_Synced(t *testing.T) {
+	from := openSQLite(t, "")
+	to := openSQLite(t, "")
+	cmd := newDiffCmd()
+	s, err := runCmd(cmd, "schema", "diff", "--from", from, "--to", to)
+	require.NoError(t, err)
+	require.EqualValues(t, "Schemas are synced, no changes to be made.\n", s)
+}
+
+// openSQLite creates a sqlite db, seeds it with the seed query and returns the dsn to it.
+func openSQLite(t *testing.T, seed string) string {
+	f, err := ioutil.TempFile("", "sqlite.db")
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		os.Remove(f.Name())
+	})
+	dsn := fmt.Sprintf("file:%s?cache=shared&_fk=1", f.Name())
+	db, err := sql.Open("sqlite3", dsn)
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		db.Close()
+	})
+	drv, err := sqlite.Open(db)
+	require.NoError(t, err)
+	if len(seed) > 0 {
+		_, err := drv.ExecContext(context.Background(), seed)
+		require.NoError(t, err)
+	}
+	return fmt.Sprintf("sqlite://%s", dsn)
+}
+
+func runCmd(cmd *cobra.Command, args... string) (string, error) {
+	var out bytes.Buffer
+	cmd.SetOut(&out)
+	cmd.SetArgs(args)
+	err := cmd.Execute()
+	return out.String(), err
+}
diff --git a/cmd/action/mux_test.go b/cmd/action/mux_test.go
index 639fa849..278945e0 100644
--- a/cmd/action/mux_test.go
+++ b/cmd/action/mux_test.go
@@ -1,3 +1,7 @@
+// Copyright 2021-present The Atlas Authors. All rights reserved.
+// This source code is licensed under the Apache 2.0 license found
+// in the LICENSE file in the root directory of this source tree.
+
 package action_test
 
 import (