diff --git a/config.go b/config.go index e1337b3..f2fa397 100644 --- a/config.go +++ b/config.go @@ -5,6 +5,7 @@ import ( "os" "github.com/jessevdk/go-flags" + "github.com/yazgazan/jaydiff/diff" "golang.org/x/crypto/ssh/terminal" ) @@ -17,9 +18,10 @@ type config struct { Files files `positional-args:"yes" required:"yes"` Ignore ignorePatterns `long:"ignore" short:"i" description:"paths to ignore (glob)"` output - IgnoreExcess bool `long:"ignore-excess" description:"ignore excess keys and arrey elements"` - IgnoreValues bool `long:"ignore-values" description:"ignore scalar's values (only type is compared)"` - OutputReport bool `long:"report" short:"r" description:"output report format"` + IgnoreExcess bool `long:"ignore-excess" description:"ignore excess keys and arrey elements"` + IgnoreValues bool `long:"ignore-values" description:"ignore scalar's values (only type is compared)"` + OutputReport bool `long:"report" short:"r" description:"output report format"` + UseSliceMyers bool `long:"slice-myers" description:"use myers algorithm for slices"` } type output struct { @@ -44,3 +46,13 @@ func readConfig() config { return c } + +func (c config) Opts() []diff.ConfigOpt { + opts := []diff.ConfigOpt{} + + if c.UseSliceMyers { + opts = append(opts, diff.UseSliceMyers()) + } + + return opts +} diff --git a/diff/config.go b/diff/config.go new file mode 100644 index 0000000..fc7982b --- /dev/null +++ b/diff/config.go @@ -0,0 +1,20 @@ +package diff + +type config struct { + sliceFn diffFn +} + +type ConfigOpt func(config) config + +func defaultConfig() config { + return config{ + sliceFn: newSlice, + } +} + +func UseSliceMyers() ConfigOpt { + return func(c config) config { + c.sliceFn = newMyersSlice + return c + } +} diff --git a/diff/diff.go b/diff/diff.go index a9e9ba1..9e9900f 100644 --- a/diff/diff.go +++ b/diff/diff.go @@ -27,17 +27,22 @@ type Differ interface { StringIndent(key, prefix string, conf Output) string } +type diffFn func(c config, lhs, rhs interface{}, visited *visited) (Differ, error) + // Diff generates a tree representing differences and similarities between two objects. // // Diff supports maps, slices and scalars (comparables types such as int, string, etc ...). // When an unsupported type is encountered, an ErrUnsupported error is returned. -// -// BUG(yazgazan): An infinite recursion is possible if the lhs and/or rhs objects are cyclic -func Diff(lhs, rhs interface{}) (Differ, error) { - return diff(lhs, rhs, &visited{}) +func Diff(lhs, rhs interface{}, opts ...ConfigOpt) (Differ, error) { + c := defaultConfig() + for _, opt := range opts { + c = opt(c) + } + + return diff(c, lhs, rhs, &visited{}) } -func diff(lhs, rhs interface{}, visited *visited) (Differ, error) { +func diff(c config, lhs, rhs interface{}, visited *visited) (Differ, error) { lhsVal := reflect.ValueOf(lhs) rhsVal := reflect.ValueOf(rhs) @@ -55,10 +60,10 @@ func diff(lhs, rhs interface{}, visited *visited) (Differ, error) { } if lhsVal.Kind() == reflect.Slice { - return newSlice(lhs, rhs, visited) + return c.sliceFn(c, lhs, rhs, visited) } if lhsVal.Kind() == reflect.Map { - return newMap(lhs, rhs, visited) + return newMap(c, lhs, rhs, visited) } return types{lhs, rhs}, &ErrUnsupported{lhsVal.Type(), rhsVal.Type()} diff --git a/diff/diff_test.go b/diff/diff_test.go index b03af75..65fd54c 100644 --- a/diff/diff_test.go +++ b/diff/diff_test.go @@ -69,12 +69,57 @@ func TestDiff(t *testing.T) { if diff.Diff() != test.Want { t.Logf("LHS: %+#v\n", test.LHS) - t.Logf("LHS: %+#v\n", test.RHS) + t.Logf("RHS: %+#v\n", test.RHS) t.Errorf("Diff(%#v, %#v) = %q, expected %q", test.LHS, test.RHS, diff.Diff(), test.Want) } } } +func TestDiffMyers(t *testing.T) { + for _, test := range []struct { + LHS interface{} + RHS interface{} + Want Type + Error bool + }{ + {LHS: []int{1, 2, 3}, RHS: []int{1, 2, 3}, Want: Identical}, + {LHS: []int{1, 2, 3, 4}, RHS: []int{1, 2, 3}, Want: ContentDiffer}, + {LHS: []int{1, 2, 3}, RHS: []int{1, 2, 3, 4}, Want: ContentDiffer}, + {LHS: []int{1, 2, 3}, RHS: []int{4, 5}, Want: ContentDiffer}, + {LHS: []int{1, 2, 3}, RHS: []float64{4, 5}, Want: TypesDiffer}, + {LHS: []int{1, 2, 3}, RHS: []float64{4, 5}, Want: TypesDiffer}, + {LHS: []func(){func() {}}, RHS: []func(){func() {}}, Want: ContentDiffer, Error: true}, + { + LHS: map[int][]int{1: {2, 3}, 2: {3, 4}}, + RHS: map[int][]int{1: {2, 3}, 2: {3, 4}}, + Want: Identical, + }, + { + LHS: map[int][]int{1: {2, 3}, 2: {3, 4}}, + RHS: map[int][]int{1: {2, 3}, 2: {3, 5}}, + Want: ContentDiffer, + }, + {LHS: []interface{}{1, 2, 3}, RHS: []interface{}{1, 2, 3}, Want: Identical}, + {LHS: []interface{}{1, 2, 3}, RHS: []interface{}{1, 2, 3.3}, Want: ContentDiffer}, + {LHS: []interface{}(nil), RHS: []interface{}{1, 2, 3.3}, Want: ContentDiffer}, + {LHS: []int(nil), RHS: []int{}, Want: Identical}, + } { + diff, err := Diff(test.LHS, test.RHS, UseSliceMyers()) + + if err == nil && test.Error { + t.Errorf("Diff(%#v, %#v) expected an error, got nil instead", test.LHS, test.RHS) + } + if err != nil && !test.Error { + t.Errorf("Diff(%#v, %#v): unexpected error: %q", test.LHS, test.RHS, err) + } + + if diff.Diff() != test.Want { + t.Logf("LHS: %+#v\n", test.LHS) + t.Logf("RHS: %+#v\n", test.RHS) + t.Errorf("Diff(%#v, %#v) = %q, expected %q", test.LHS, test.RHS, diff.Diff(), test.Want) + } + } +} func TestTypeString(t *testing.T) { for _, test := range []struct { Input Type @@ -223,7 +268,7 @@ func TestSlice(t *testing.T) { Type: ContentDiffer, }, } { - typ, err := newSlice(test.LHS, test.RHS, &visited{}) + typ, err := newSlice(defaultConfig(), test.LHS, test.RHS, &visited{}) if err != nil { t.Errorf("NewSlice(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err) @@ -238,7 +283,7 @@ func TestSlice(t *testing.T) { testStrings("TestSlice", t, test, ss, indented) } - invalid, err := newSlice(nil, nil, &visited{}) + invalid, err := newSlice(defaultConfig(), nil, nil, &visited{}) if invalidErr, ok := err.(errInvalidType); ok { if !strings.Contains(invalidErr.Error(), "nil") { t.Errorf("NewSlice(nil, nil): unexpected format for InvalidType error: got %s", err) @@ -256,7 +301,7 @@ func TestSlice(t *testing.T) { t.Errorf("invalidSlice.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "") } - invalid, err = newSlice([]int{}, nil, &visited{}) + invalid, err = newSlice(defaultConfig(), []int{}, nil, &visited{}) if invalidErr, ok := err.(errInvalidType); ok { if !strings.Contains(invalidErr.Error(), "nil") { t.Errorf("NewSlice([]int{}, nil): unexpected format for InvalidType error: got %s", err) @@ -275,6 +320,113 @@ func TestSlice(t *testing.T) { } } +func TestSliceMyers(t *testing.T) { + c := defaultConfig() + c = UseSliceMyers()(c) + + for _, test := range []stringTest{ + { + LHS: []int{1, 2}, + RHS: []int{1, 2}, + Want: [][]string{ + {"int", "1", "2"}, + }, + Type: Identical, + }, + { + LHS: []int{1}, + RHS: []int{}, + Want: [][]string{ + {}, + {"-", "int", "1"}, + {}, + }, + Type: ContentDiffer, + }, + { + LHS: []int{}, + RHS: []int{2}, + Want: [][]string{ + {}, + {"+", "int", "2"}, + {}, + }, + Type: ContentDiffer, + }, + { + LHS: []int{1, 2}, + RHS: []float64{1.1, 2.1}, + Want: [][]string{ + {"-", "int", "1", "2"}, + {"+", "float64", "1.1", "2.1"}, + }, + Type: TypesDiffer, + }, + { + LHS: []int{1, 3}, + RHS: []int{1, 2}, + Want: [][]string{ + {}, + {"int", "1"}, + {"-", "int", "3"}, + {"+", "int", "2"}, + {}, + }, + Type: ContentDiffer, + }, + } { + typ, err := c.sliceFn(c, test.LHS, test.RHS, &visited{}) + + if err != nil { + t.Errorf("NewMyersSlice(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err) + continue + } + if typ.Diff() != test.Type { + t.Errorf("Types.Diff() = %q, expected %q", typ.Diff(), test.Type) + } + + ss := typ.Strings() + indented := typ.StringIndent(testKey, testPrefix, testOutput) + testStrings("TestSlice", t, test, ss, indented) + } + + invalid, err := c.sliceFn(c, nil, nil, &visited{}) + if invalidErr, ok := err.(errInvalidType); ok { + if !strings.Contains(invalidErr.Error(), "nil") { + t.Errorf("NewMyersSlice(nil, nil): unexpected format for InvalidType error: got %s", err) + } + } else { + t.Errorf("NewMyersSlice(nil, nil): expected InvalidType error, got %s", err) + } + ss := invalid.Strings() + if len(ss) != 0 { + t.Errorf("len(invalidSlice.Strings()) = %d, expected 0", len(ss)) + } + + indented := invalid.StringIndent(testKey, testPrefix, testOutput) + if indented != "" { + t.Errorf("invalidSlice.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "") + } + + invalid, err = c.sliceFn(c, []int{}, nil, &visited{}) + if invalidErr, ok := err.(errInvalidType); ok { + if !strings.Contains(invalidErr.Error(), "nil") { + t.Errorf("NewMyersSlice([]int{}, nil): unexpected format for InvalidType error: got %s", err) + } + } else { + t.Errorf("NewMyersSlice([]int{}, nil): expected InvalidType error, got %s", err) + } + ss = invalid.Strings() + if len(ss) != 0 { + t.Errorf("len(invalidSlice.Strings()) = %d, expected 0", len(ss)) + } + + indented = invalid.StringIndent(testKey, testPrefix, testOutput) + if indented != "" { + t.Errorf("invalidSlice.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "") + } +} + func TestMap(t *testing.T) { for i, test := range []stringTest{ { @@ -338,7 +490,7 @@ func TestMap(t *testing.T) { Type: ContentDiffer, }, } { - m, err := newMap(test.LHS, test.RHS, &visited{}) + m, err := newMap(defaultConfig(), test.LHS, test.RHS, &visited{}) if err != nil { t.Errorf("NewMap(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err) @@ -353,7 +505,7 @@ func TestMap(t *testing.T) { testStrings(fmt.Sprintf("TestMap[%d]", i), t, test, ss, indented) } - invalid, err := newMap(nil, nil, &visited{}) + invalid, err := newMap(defaultConfig(), nil, nil, &visited{}) if invalidErr, ok := err.(errInvalidType); ok { if !strings.Contains(invalidErr.Error(), "nil") { t.Errorf("NewMap(nil, nil): unexpected format for InvalidType error: got %s", err) @@ -371,7 +523,7 @@ func TestMap(t *testing.T) { t.Errorf("invalidMap.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "") } - invalid, err = newMap(map[int]int{}, nil, &visited{}) + invalid, err = newMap(defaultConfig(), map[int]int{}, nil, &visited{}) if invalidErr, ok := err.(errInvalidType); ok { if !strings.Contains(invalidErr.Error(), "nil") { t.Errorf("NewMap(map[int]int{}, nil): unexpected format for InvalidType error: got %s", err) diff --git a/diff/map.go b/diff/map.go index 76e2233..0934233 100644 --- a/diff/map.go +++ b/diff/map.go @@ -21,7 +21,7 @@ type mapExcess struct { value interface{} } -func newMap(lhs, rhs interface{}, visited *visited) (Differ, error) { +func newMap(c config, lhs, rhs interface{}, visited *visited) (Differ, error) { var diffs = make(map[interface{}]Differ) lhsVal := reflect.ValueOf(lhs) @@ -41,7 +41,7 @@ func newMap(lhs, rhs interface{}, visited *visited) (Differ, error) { rhsEl := rhsVal.MapIndex(key) if lhsEl.IsValid() && rhsEl.IsValid() { - diff, err := diff(lhsEl.Interface(), rhsEl.Interface(), visited) + diff, err := diff(c, lhsEl.Interface(), rhsEl.Interface(), visited) if diff.Diff() != Identical { } diffs[key.Interface()] = diff diff --git a/diff/slice.go b/diff/slice.go index 6d29a14..b39b56f 100644 --- a/diff/slice.go +++ b/diff/slice.go @@ -4,6 +4,8 @@ import ( "fmt" "reflect" "strings" + + myersdiff "github.com/mb0/diff" ) type slice struct { @@ -20,7 +22,98 @@ type sliceExcess struct { value interface{} } -func newSlice(lhs, rhs interface{}, visited *visited) (Differ, error) { +type diffData struct { + lhs reflect.Value + rhs reflect.Value + visited *visited + lastError error + c config +} + +func (d *diffData) Equal(i, j int) bool { + diff, err := diff(d.c, d.lhs.Index(i).Interface(), d.rhs.Index(j).Interface(), d.visited) + if err != nil { + d.lastError = err + return false + } + + return diff.Diff() == Identical +} + +func myersToDiff(conf config, lhs, rhs reflect.Value, changes []myersdiff.Change) []Differ { + res := []Differ{} + + lhsIdx := 0 + rhsIdx := 0 + for _, c := range changes { + for i := 0; lhsIdx+i < c.A; i++ { + diff, _ := diff(conf, lhs.Index(lhsIdx+i).Interface(), rhs.Index(rhsIdx+i).Interface(), &visited{}) + res = append(res, diff) + } + lhsIdx = c.A + rhsIdx = c.B + for d := 0; d < c.Del; d++ { + res = append(res, sliceMissing{lhs.Index(lhsIdx + d).Interface()}) + } + lhsIdx += c.Del + for i := 0; i < c.Ins; i++ { + res = append(res, sliceExcess{rhs.Index(rhsIdx + i).Interface()}) + } + rhsIdx += c.Ins + } + + for lhsIdx < lhs.Len() && rhsIdx < rhs.Len() { + diff, _ := diff(conf, lhs.Index(lhsIdx).Interface(), rhs.Index(rhsIdx).Interface(), &visited{}) + res = append(res, diff) + lhsIdx++ + rhsIdx++ + } + return res +} + +func newMyersSlice(c config, lhs, rhs interface{}, visited *visited) (Differ, error) { + var diffs []Differ + + lhsVal := reflect.ValueOf(lhs) + rhsVal := reflect.ValueOf(rhs) + + if typesDiffer, err := sliceTypesDiffer(lhs, rhs); err != nil { + return slice{ + lhs: lhs, + rhs: rhs, + }, err + } else if !typesDiffer { + nElems := lhsVal.Len() + if rhsVal.Len() > nElems { + nElems = rhsVal.Len() + } + + dData := diffData{ + lhs: lhsVal, + rhs: rhsVal, + visited: visited, + c: c, + } + myers := myersdiff.Diff(lhsVal.Len(), rhsVal.Len(), &dData) + + diffs = myersToDiff(c, lhsVal, rhsVal, myers) + if dData.lastError != nil { + return slice{ + lhs: lhs, + rhs: rhs, + diffs: diffs, + }, dData.lastError + } + } + + return slice{ + lhs: lhs, + rhs: rhs, + diffs: diffs, + }, nil +} + +func newSlice(c config, lhs, rhs interface{}, visited *visited) (Differ, error) { var diffs []Differ lhsVal := reflect.ValueOf(lhs) @@ -39,7 +132,7 @@ func newSlice(lhs, rhs interface{}, visited *visited) (Differ, error) { for i := 0; i < nElems; i++ { if i < lhsVal.Len() && i < rhsVal.Len() { - diff, err := diff(lhsVal.Index(i).Interface(), rhsVal.Index(i).Interface(), visited) + diff, err := diff(c, lhsVal.Index(i).Interface(), rhsVal.Index(i).Interface(), visited) if diff.Diff() != Identical { } diffs = append(diffs, diff) diff --git a/diff/walk_test.go b/diff/walk_test.go index dd52e7a..af6751b 100644 --- a/diff/walk_test.go +++ b/diff/walk_test.go @@ -32,7 +32,7 @@ func TestWalk(t *testing.T) { continue } - _, err = Walk(d, func(_, diff Differ, _ string) (Differ, error) { + _, err = Walk(d, func(_, diff Differ, p string) (Differ, error) { nCalls++ return nil, nil }) @@ -57,7 +57,7 @@ func TestWalkError(t *testing.T) { RHS interface{} }{ {42, 43}, - {[]int{42}, []int{44}}, + {[]int{42}, []int{43}}, {map[string]int{"ha": 42}, map[string]int{"ha": 45}}, } { d, err := Diff(test.LHS, test.RHS) @@ -70,6 +70,9 @@ func TestWalkError(t *testing.T) { if _, ok := diff.(scalar); ok { return nil, expectedErr } + if _, ok := diff.(sliceMissing); ok { + return nil, expectedErr + } return nil, nil }) diff --git a/main.go b/main.go index 005a6b6..4bd7b9b 100644 --- a/main.go +++ b/main.go @@ -24,7 +24,7 @@ func main() { lhs := parseFile(conf.Files.LHS) rhs := parseFile(conf.Files.RHS) - d, err := diff.Diff(lhs, rhs) + d, err := diff.Diff(lhs, rhs, conf.Opts()...) if err != nil { fmt.Fprintf(os.Stderr, "Error: diff failed: %s", err) os.Exit(statusDiffError)