Skip to content

Commit a80ff5b

Browse files
authored
Merge pull request #6 from yazgazan/adding-myers-algorithm-for-slices
Adding Myers' algorithm for slices
2 parents 17e581f + dfcf28d commit a80ff5b

File tree

9 files changed

+319
-31
lines changed

9 files changed

+319
-31
lines changed

config.go

+20-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66

77
"github.com/jessevdk/go-flags"
8+
"github.com/yazgazan/jaydiff/diff"
89
"golang.org/x/crypto/ssh/terminal"
910
)
1011

@@ -17,9 +18,11 @@ type config struct {
1718
Files files `positional-args:"yes" required:"yes"`
1819
Ignore ignorePatterns `long:"ignore" short:"i" description:"paths to ignore (glob)"`
1920
output
20-
IgnoreExcess bool `long:"ignore-excess" description:"ignore excess keys and arrey elements"`
21-
IgnoreValues bool `long:"ignore-values" description:"ignore scalar's values (only type is compared)"`
22-
OutputReport bool `long:"report" short:"r" description:"output report format"`
21+
IgnoreExcess bool `long:"ignore-excess" description:"ignore excess keys and arrey elements"`
22+
IgnoreValues bool `long:"ignore-values" description:"ignore scalar's values (only type is compared)"`
23+
OutputReport bool `long:"report" short:"r" description:"output report format"`
24+
UseSliceMyers bool `long:"slice-myers" description:"use myers algorithm for slices"`
25+
Version func() `long:"version" short:"v" description:"print release version"`
2326
}
2427

2528
type output struct {
@@ -30,6 +33,10 @@ type output struct {
3033

3134
func readConfig() config {
3235
var c config
36+
c.Version = func() {
37+
fmt.Fprintf(os.Stderr, "%s\n", Version)
38+
os.Exit(0)
39+
}
3340

3441
_, err := flags.Parse(&c)
3542
if err != nil {
@@ -44,3 +51,13 @@ func readConfig() config {
4451

4552
return c
4653
}
54+
55+
func (c config) Opts() []diff.ConfigOpt {
56+
opts := []diff.ConfigOpt{}
57+
58+
if c.UseSliceMyers {
59+
opts = append(opts, diff.UseSliceMyers())
60+
}
61+
62+
return opts
63+
}

diff/config.go

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package diff
2+
3+
type config struct {
4+
sliceFn diffFn
5+
}
6+
7+
// ConfigOpt is used to pass configuration options to the diff algorithm
8+
type ConfigOpt func(config) config
9+
10+
func defaultConfig() config {
11+
return config{
12+
sliceFn: newSlice,
13+
}
14+
}
15+
16+
// UseSliceMyers configures the Diff function to use Myers' algorithm for slices
17+
func UseSliceMyers() ConfigOpt {
18+
return func(c config) config {
19+
c.sliceFn = newMyersSlice
20+
return c
21+
}
22+
}

diff/diff.go

+12-7
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,22 @@ type Differ interface {
2727
StringIndent(key, prefix string, conf Output) string
2828
}
2929

30+
type diffFn func(c config, lhs, rhs interface{}, visited *visited) (Differ, error)
31+
3032
// Diff generates a tree representing differences and similarities between two objects.
3133
//
3234
// Diff supports maps, slices and scalars (comparables types such as int, string, etc ...).
3335
// When an unsupported type is encountered, an ErrUnsupported error is returned.
34-
//
35-
// BUG(yazgazan): An infinite recursion is possible if the lhs and/or rhs objects are cyclic
36-
func Diff(lhs, rhs interface{}) (Differ, error) {
37-
return diff(lhs, rhs, &visited{})
36+
func Diff(lhs, rhs interface{}, opts ...ConfigOpt) (Differ, error) {
37+
c := defaultConfig()
38+
for _, opt := range opts {
39+
c = opt(c)
40+
}
41+
42+
return diff(c, lhs, rhs, &visited{})
3843
}
3944

40-
func diff(lhs, rhs interface{}, visited *visited) (Differ, error) {
45+
func diff(c config, lhs, rhs interface{}, visited *visited) (Differ, error) {
4146
lhsVal := reflect.ValueOf(lhs)
4247
rhsVal := reflect.ValueOf(rhs)
4348

@@ -55,10 +60,10 @@ func diff(lhs, rhs interface{}, visited *visited) (Differ, error) {
5560
}
5661

5762
if lhsVal.Kind() == reflect.Slice {
58-
return newSlice(lhs, rhs, visited)
63+
return c.sliceFn(c, lhs, rhs, visited)
5964
}
6065
if lhsVal.Kind() == reflect.Map {
61-
return newMap(lhs, rhs, visited)
66+
return newMap(c, lhs, rhs, visited)
6267
}
6368

6469
return types{lhs, rhs}, &ErrUnsupported{lhsVal.Type(), rhsVal.Type()}

diff/diff_test.go

+159-7
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,57 @@ func TestDiff(t *testing.T) {
6969

7070
if diff.Diff() != test.Want {
7171
t.Logf("LHS: %+#v\n", test.LHS)
72-
t.Logf("LHS: %+#v\n", test.RHS)
72+
t.Logf("RHS: %+#v\n", test.RHS)
7373
t.Errorf("Diff(%#v, %#v) = %q, expected %q", test.LHS, test.RHS, diff.Diff(), test.Want)
7474
}
7575
}
7676
}
7777

78+
func TestDiffMyers(t *testing.T) {
79+
for _, test := range []struct {
80+
LHS interface{}
81+
RHS interface{}
82+
Want Type
83+
Error bool
84+
}{
85+
{LHS: []int{1, 2, 3}, RHS: []int{1, 2, 3}, Want: Identical},
86+
{LHS: []int{1, 2, 3, 4}, RHS: []int{1, 2, 3}, Want: ContentDiffer},
87+
{LHS: []int{1, 2, 3}, RHS: []int{1, 2, 3, 4}, Want: ContentDiffer},
88+
{LHS: []int{1, 2, 3}, RHS: []int{4, 5}, Want: ContentDiffer},
89+
{LHS: []int{1, 2, 3}, RHS: []float64{4, 5}, Want: TypesDiffer},
90+
{LHS: []int{1, 2, 3}, RHS: []float64{4, 5}, Want: TypesDiffer},
91+
{LHS: []func(){func() {}}, RHS: []func(){func() {}}, Want: ContentDiffer, Error: true},
92+
{
93+
LHS: map[int][]int{1: {2, 3}, 2: {3, 4}},
94+
RHS: map[int][]int{1: {2, 3}, 2: {3, 4}},
95+
Want: Identical,
96+
},
97+
{
98+
LHS: map[int][]int{1: {2, 3}, 2: {3, 4}},
99+
RHS: map[int][]int{1: {2, 3}, 2: {3, 5}},
100+
Want: ContentDiffer,
101+
},
102+
{LHS: []interface{}{1, 2, 3}, RHS: []interface{}{1, 2, 3}, Want: Identical},
103+
{LHS: []interface{}{1, 2, 3}, RHS: []interface{}{1, 2, 3.3}, Want: ContentDiffer},
104+
{LHS: []interface{}(nil), RHS: []interface{}{1, 2, 3.3}, Want: ContentDiffer},
105+
{LHS: []int(nil), RHS: []int{}, Want: Identical},
106+
} {
107+
diff, err := Diff(test.LHS, test.RHS, UseSliceMyers())
108+
109+
if err == nil && test.Error {
110+
t.Errorf("Diff(%#v, %#v) expected an error, got nil instead", test.LHS, test.RHS)
111+
}
112+
if err != nil && !test.Error {
113+
t.Errorf("Diff(%#v, %#v): unexpected error: %q", test.LHS, test.RHS, err)
114+
}
115+
116+
if diff.Diff() != test.Want {
117+
t.Logf("LHS: %+#v\n", test.LHS)
118+
t.Logf("RHS: %+#v\n", test.RHS)
119+
t.Errorf("Diff(%#v, %#v) = %q, expected %q", test.LHS, test.RHS, diff.Diff(), test.Want)
120+
}
121+
}
122+
}
78123
func TestTypeString(t *testing.T) {
79124
for _, test := range []struct {
80125
Input Type
@@ -223,7 +268,7 @@ func TestSlice(t *testing.T) {
223268
Type: ContentDiffer,
224269
},
225270
} {
226-
typ, err := newSlice(test.LHS, test.RHS, &visited{})
271+
typ, err := newSlice(defaultConfig(), test.LHS, test.RHS, &visited{})
227272

228273
if err != nil {
229274
t.Errorf("NewSlice(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err)
@@ -238,7 +283,7 @@ func TestSlice(t *testing.T) {
238283
testStrings("TestSlice", t, test, ss, indented)
239284
}
240285

241-
invalid, err := newSlice(nil, nil, &visited{})
286+
invalid, err := newSlice(defaultConfig(), nil, nil, &visited{})
242287
if invalidErr, ok := err.(errInvalidType); ok {
243288
if !strings.Contains(invalidErr.Error(), "nil") {
244289
t.Errorf("NewSlice(nil, nil): unexpected format for InvalidType error: got %s", err)
@@ -256,7 +301,7 @@ func TestSlice(t *testing.T) {
256301
t.Errorf("invalidSlice.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "")
257302
}
258303

259-
invalid, err = newSlice([]int{}, nil, &visited{})
304+
invalid, err = newSlice(defaultConfig(), []int{}, nil, &visited{})
260305
if invalidErr, ok := err.(errInvalidType); ok {
261306
if !strings.Contains(invalidErr.Error(), "nil") {
262307
t.Errorf("NewSlice([]int{}, nil): unexpected format for InvalidType error: got %s", err)
@@ -275,6 +320,113 @@ func TestSlice(t *testing.T) {
275320
}
276321
}
277322

323+
func TestSliceMyers(t *testing.T) {
324+
c := defaultConfig()
325+
c = UseSliceMyers()(c)
326+
327+
for _, test := range []stringTest{
328+
{
329+
LHS: []int{1, 2},
330+
RHS: []int{1, 2},
331+
Want: [][]string{
332+
{"int", "1", "2"},
333+
},
334+
Type: Identical,
335+
},
336+
{
337+
LHS: []int{1},
338+
RHS: []int{},
339+
Want: [][]string{
340+
{},
341+
{"-", "int", "1"},
342+
{},
343+
},
344+
Type: ContentDiffer,
345+
},
346+
{
347+
LHS: []int{},
348+
RHS: []int{2},
349+
Want: [][]string{
350+
{},
351+
{"+", "int", "2"},
352+
{},
353+
},
354+
Type: ContentDiffer,
355+
},
356+
{
357+
LHS: []int{1, 2},
358+
RHS: []float64{1.1, 2.1},
359+
Want: [][]string{
360+
{"-", "int", "1", "2"},
361+
{"+", "float64", "1.1", "2.1"},
362+
},
363+
Type: TypesDiffer,
364+
},
365+
{
366+
LHS: []int{1, 3},
367+
RHS: []int{1, 2},
368+
Want: [][]string{
369+
{},
370+
{"int", "1"},
371+
{"-", "int", "3"},
372+
{"+", "int", "2"},
373+
{},
374+
},
375+
Type: ContentDiffer,
376+
},
377+
} {
378+
typ, err := c.sliceFn(c, test.LHS, test.RHS, &visited{})
379+
380+
if err != nil {
381+
t.Errorf("NewMyersSlice(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err)
382+
continue
383+
}
384+
if typ.Diff() != test.Type {
385+
t.Errorf("Types.Diff() = %q, expected %q", typ.Diff(), test.Type)
386+
}
387+
388+
ss := typ.Strings()
389+
indented := typ.StringIndent(testKey, testPrefix, testOutput)
390+
testStrings("TestSlice", t, test, ss, indented)
391+
}
392+
393+
invalid, err := c.sliceFn(c, nil, nil, &visited{})
394+
if invalidErr, ok := err.(errInvalidType); ok {
395+
if !strings.Contains(invalidErr.Error(), "nil") {
396+
t.Errorf("NewMyersSlice(nil, nil): unexpected format for InvalidType error: got %s", err)
397+
}
398+
} else {
399+
t.Errorf("NewMyersSlice(nil, nil): expected InvalidType error, got %s", err)
400+
}
401+
ss := invalid.Strings()
402+
if len(ss) != 0 {
403+
t.Errorf("len(invalidSlice.Strings()) = %d, expected 0", len(ss))
404+
}
405+
406+
indented := invalid.StringIndent(testKey, testPrefix, testOutput)
407+
if indented != "" {
408+
t.Errorf("invalidSlice.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "")
409+
}
410+
411+
invalid, err = c.sliceFn(c, []int{}, nil, &visited{})
412+
if invalidErr, ok := err.(errInvalidType); ok {
413+
if !strings.Contains(invalidErr.Error(), "nil") {
414+
t.Errorf("NewMyersSlice([]int{}, nil): unexpected format for InvalidType error: got %s", err)
415+
}
416+
} else {
417+
t.Errorf("NewMyersSlice([]int{}, nil): expected InvalidType error, got %s", err)
418+
}
419+
ss = invalid.Strings()
420+
if len(ss) != 0 {
421+
t.Errorf("len(invalidSlice.Strings()) = %d, expected 0", len(ss))
422+
}
423+
424+
indented = invalid.StringIndent(testKey, testPrefix, testOutput)
425+
if indented != "" {
426+
t.Errorf("invalidSlice.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "")
427+
}
428+
}
429+
278430
func TestMap(t *testing.T) {
279431
for i, test := range []stringTest{
280432
{
@@ -338,7 +490,7 @@ func TestMap(t *testing.T) {
338490
Type: ContentDiffer,
339491
},
340492
} {
341-
m, err := newMap(test.LHS, test.RHS, &visited{})
493+
m, err := newMap(defaultConfig(), test.LHS, test.RHS, &visited{})
342494

343495
if err != nil {
344496
t.Errorf("NewMap(%+v, %+v): unexpected error: %q", test.LHS, test.RHS, err)
@@ -353,7 +505,7 @@ func TestMap(t *testing.T) {
353505
testStrings(fmt.Sprintf("TestMap[%d]", i), t, test, ss, indented)
354506
}
355507

356-
invalid, err := newMap(nil, nil, &visited{})
508+
invalid, err := newMap(defaultConfig(), nil, nil, &visited{})
357509
if invalidErr, ok := err.(errInvalidType); ok {
358510
if !strings.Contains(invalidErr.Error(), "nil") {
359511
t.Errorf("NewMap(nil, nil): unexpected format for InvalidType error: got %s", err)
@@ -371,7 +523,7 @@ func TestMap(t *testing.T) {
371523
t.Errorf("invalidMap.StringIndent(%q, %q, %+v) = %q, expected %q", testKey, testPrefix, testOutput, indented, "")
372524
}
373525

374-
invalid, err = newMap(map[int]int{}, nil, &visited{})
526+
invalid, err = newMap(defaultConfig(), map[int]int{}, nil, &visited{})
375527
if invalidErr, ok := err.(errInvalidType); ok {
376528
if !strings.Contains(invalidErr.Error(), "nil") {
377529
t.Errorf("NewMap(map[int]int{}, nil): unexpected format for InvalidType error: got %s", err)

diff/map.go

+2-4
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ type mapExcess struct {
2121
value interface{}
2222
}
2323

24-
func newMap(lhs, rhs interface{}, visited *visited) (Differ, error) {
24+
func newMap(c config, lhs, rhs interface{}, visited *visited) (Differ, error) {
2525
var diffs = make(map[interface{}]Differ)
2626

2727
lhsVal := reflect.ValueOf(lhs)
@@ -41,9 +41,7 @@ func newMap(lhs, rhs interface{}, visited *visited) (Differ, error) {
4141
rhsEl := rhsVal.MapIndex(key)
4242

4343
if lhsEl.IsValid() && rhsEl.IsValid() {
44-
diff, err := diff(lhsEl.Interface(), rhsEl.Interface(), visited)
45-
if diff.Diff() != Identical {
46-
}
44+
diff, err := diff(c, lhsEl.Interface(), rhsEl.Interface(), visited)
4745
diffs[key.Interface()] = diff
4846

4947
if err != nil {

diff/output.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func (o Output) red(v interface{}) string {
2323
}
2424

2525
if !o.Colorized {
26-
return fmt.Sprintf("%s", s)
26+
return s
2727
}
2828

2929
return color.RedString("%s", s)
@@ -39,7 +39,7 @@ func (o Output) green(v interface{}) string {
3939
}
4040

4141
if !o.Colorized {
42-
return fmt.Sprintf("%s", s)
42+
return s
4343
}
4444

4545
return color.GreenString("%s", s)
@@ -54,7 +54,7 @@ func (o Output) white(v interface{}) string {
5454
s = fmt.Sprintf("%v", v)
5555
}
5656

57-
return fmt.Sprintf("%s", s)
57+
return s
5858
}
5959

6060
func (o Output) typ(v interface{}) string {

0 commit comments

Comments
 (0)