Skip to content

Commit

Permalink
Adding Myers' algorithm for slices
Browse files Browse the repository at this point in the history
  • Loading branch information
yazgazan committed Jun 28, 2017
1 parent 17e581f commit 84cbef0
Show file tree
Hide file tree
Showing 8 changed files with 309 additions and 24 deletions.
18 changes: 15 additions & 3 deletions config.go
Expand Up @@ -5,6 +5,7 @@ import (
"os"

"github.com/jessevdk/go-flags"
"github.com/yazgazan/jaydiff/diff"
"golang.org/x/crypto/ssh/terminal"
)

Expand All @@ -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 {
Expand All @@ -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
}
20 changes: 20 additions & 0 deletions 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
}
}
19 changes: 12 additions & 7 deletions diff/diff.go
Expand Up @@ -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)

Expand All @@ -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()}
Expand Down
166 changes: 159 additions & 7 deletions diff/diff_test.go
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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{
{
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions diff/map.go
Expand Up @@ -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)
Expand All @@ -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
Expand Down

0 comments on commit 84cbef0

Please sign in to comment.