diff --git a/cmd/config.go b/cmd/config.go index 7ee8019..416ef4b 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -1,7 +1,8 @@ package cmd import ( - "errors" + "fmt" + "github.com/spf13/afero" "strings" "github.com/aswinkarthik/csvdiff/pkg/digest" @@ -13,6 +14,8 @@ type Config struct { ValueColumnPositions []int IncludeColumnPositions []int Format string + BaseFilename string + DeltaFilename string } // GetPrimaryKeys is to return the --primary-key flags as digest.Positions array. @@ -42,13 +45,52 @@ func (c Config) GetIncludeColumnPositions() digest.Positions { // Validate validates the config object // and returns error if not valid. -func (c *Config) Validate() error { +func (c *Config) Validate(fs afero.Fs) error { + { + // format validation - for _, format := range allFormats { - if strings.ToLower(c.Format) == format { - return nil + formatFound := false + for _, format := range allFormats { + if strings.ToLower(c.Format) == format { + formatFound = true + } + } + if !formatFound { + return fmt.Errorf("specified format is not valid") + } + } + + { + // base-file validation + + if exists, err := afero.Exists(fs, c.BaseFilename); err != nil { + return fmt.Errorf("error reading base-file %s: %v", c.BaseFilename, err) + } else if !exists { + return fmt.Errorf("base-file %s does not exits", c.BaseFilename) + } + + if isDir, err := afero.IsDir(fs, c.BaseFilename); err != nil { + return fmt.Errorf("error reading base-file %s: %v", c.BaseFilename, err) + } else if isDir { + return fmt.Errorf("base-file %s should be a file", c.BaseFilename) + } + } + + { + // delta file validation + + if exists, err := afero.Exists(fs, c.DeltaFilename); err != nil { + return fmt.Errorf("error reading delta-file %s: %v", c.DeltaFilename, err) + } else if !exists { + return fmt.Errorf("delta-file %s does not exits", c.DeltaFilename) + } + + if isDir, err := afero.IsDir(fs, c.DeltaFilename); err != nil { + return fmt.Errorf("error reading delta-file %s: %v", c.DeltaFilename, err) + } else if isDir { + return fmt.Errorf("delta-file %s should be a file", c.DeltaFilename) } } - return errors.New("Specified format is not valid") + return nil } diff --git a/cmd/config_test.go b/cmd/config_test.go index 46d7102..a7b8fba 100644 --- a/cmd/config_test.go +++ b/cmd/config_test.go @@ -1,6 +1,8 @@ package cmd_test import ( + "github.com/spf13/afero" + "os" "testing" "github.com/aswinkarthik/csvdiff/cmd" @@ -31,15 +33,67 @@ func TestValueColumnPositions(t *testing.T) { } func TestConfigValidate(t *testing.T) { - config := &cmd.Config{} - assert.Error(t, config.Validate()) + t.Run("should validate format", func(t *testing.T) { + fs := afero.NewMemMapFs() - config = &cmd.Config{Format: "rowmark"} - assert.NoError(t, config.Validate()) + config := validConfig(t, fs) - config = &cmd.Config{Format: "rowMARK"} - assert.NoError(t, config.Validate()) + config.Format = "" + assert.Error(t, config.Validate(fs)) - config = &cmd.Config{Format: "json"} - assert.NoError(t, config.Validate()) + config.Format = "rowmark" + assert.NoError(t, config.Validate(fs)) + + config.Format = "rowMARK" + assert.NoError(t, config.Validate(fs)) + + config.Format = "json" + assert.NoError(t, config.Validate(fs)) + }) + + t.Run("should validate base file existence", func(t *testing.T) { + fs := afero.NewMemMapFs() + + config := &cmd.Config{Format: "json", BaseFilename: "/base.csv", DeltaFilename: "/delta.csv"} + err := config.Validate(fs) + assert.EqualError(t, err, "base-file /base.csv does not exits") + }) + + t.Run("should validate if base file or delta file is a file", func(t *testing.T) { + fs := afero.NewMemMapFs() + err := fs.Mkdir("/base.csv", os.ModePerm) + assert.NoError(t, err) + + config := &cmd.Config{Format: "json", BaseFilename: "/base.csv", DeltaFilename: "/delta.csv"} + err = config.Validate(fs) + assert.EqualError(t, err, "base-file /base.csv should be a file") + + _, err = fs.Create("/valid-base.csv") + err = fs.Mkdir("/delta.csv", os.ModePerm) + assert.NoError(t, err) + + config = &cmd.Config{Format: "json", BaseFilename: "/valid-base.csv", DeltaFilename: "/delta.csv"} + err = config.Validate(fs) + assert.EqualError(t, err, "delta-file /delta.csv should be a file") + }) + + t.Run("should validate if both base and delta file exist", func(t *testing.T) { + fs := afero.NewMemMapFs() + _, err := fs.Create("/base.csv") + assert.NoError(t, err) + _, err = fs.Create("/delta.csv") + assert.NoError(t, err) + + config := &cmd.Config{Format: "json", BaseFilename: "/base.csv", DeltaFilename: "/delta.csv"} + err = config.Validate(fs) + assert.NoError(t, err) + }) +} + +func validConfig(t *testing.T, fs afero.Fs) *cmd.Config { + _, err := fs.Create("/base.csv") + assert.NoError(t, err) + _, err = fs.Create("/delta.csv") + assert.NoError(t, err) + return &cmd.Config{Format: "json", BaseFilename: "/base.csv", DeltaFilename: "/delta.csv"} } diff --git a/cmd/root.go b/cmd/root.go index 770241d..105c494 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -23,6 +23,7 @@ package cmd import ( "fmt" "github.com/fatih/color" + "github.com/spf13/afero" "io" "os" "strings" @@ -64,20 +65,36 @@ Most suitable for csv files created from database tables`, if timed { defer timeTrack(time.Now(), "csvdiff") } + fs := afero.NewOsFs() - baseFile := newReadCloser(args[0]) - defer baseFile.Close() - deltaFile := newReadCloser(args[1]) - defer deltaFile.Close() + baseFilename := args[0] + deltaFilename := args[1] + + baseFile, err := newReadCloser(baseFilename) + if err != nil { + return fmt.Errorf("error opening base-file %s: %v", baseFilename, err) + } + + deltaFile, err := newReadCloser(deltaFilename) + if err != nil { + return fmt.Errorf("error opening delta-file %s: %v", deltaFilename, err) + } + + defer func() { + _ = baseFile.Close() + _ = deltaFile.Close() + }() config := Config{ IncludeColumnPositions: includeColumnPositions, Format: format, PrimaryKeyPositions: primaryKeyPositions, ValueColumnPositions: valueColumnPositions, + BaseFilename: baseFilename, + DeltaFilename: deltaFilename, } - if err := config.Validate(); err != nil { + if err := config.Validate(fs); err != nil { return err } @@ -151,16 +168,16 @@ func init() { rootCmd.Flags().BoolVarP(&timed, "time", "", false, "Measure time") } -func newReadCloser(filename string) io.ReadCloser { +func newReadCloser(filename string) (io.ReadCloser, error) { file, err := os.Open(filename) if err != nil { - panic(err) + return nil, err } - return file + return file, nil } func timeTrack(start time.Time, name string) { elapsed := time.Since(start) - fmt.Fprintln(os.Stderr, fmt.Sprintf("%s took %s", name, elapsed)) + _, _ = fmt.Fprintln(os.Stderr, fmt.Sprintf("%s took %s", name, elapsed)) } diff --git a/go.mod b/go.mod index fd1a46c..a74f26a 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/fatih/color v1.7.0 github.com/mattn/go-colorable v0.1.2 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/spf13/afero v1.1.2 github.com/spf13/cobra v0.0.5 github.com/stretchr/testify v1.3.0 golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa // indirect diff --git a/go.sum b/go.sum index e412cc7..0b0dbf7 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,7 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v0.0.5 h1:f0B+LkLX6DtmRH1isoNA9VTtNUK9K8xYd28JNNfOv/s= @@ -55,6 +56,7 @@ golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpbl golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa h1:KIDDMLT1O0Nr7TSxp8xM5tJcdn8tgyAONntO829og1M= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/digest/config.go b/pkg/digest/config.go index 8ad6b16..c2c0428 100644 --- a/pkg/digest/config.go +++ b/pkg/digest/config.go @@ -8,7 +8,6 @@ import "io" // Key: The primary key positions // Value: The Value positions that needs to be compared for diff // Include: Include these positions in output. It is Value positions by default. -// KeepSource: return the source and target string if diff is computed type Config struct { Key Positions Value Positions