diff --git a/deep.go b/deep.go index 2399b74..4aab66a 100644 --- a/deep.go +++ b/deep.go @@ -56,10 +56,24 @@ var ( ErrNotHandled = errors.New("cannot compare the reflect.Kind") ) +const ( + // FLAG_NONE is a placeholder for default Equal behavior. You don't have to + // pass it to Equal; if you do, it does nothing. + FLAG_NONE byte = iota + + // FLAG_IGNORE_SLICE_ORDER causes Equal to ignore slice order so that + // []int{1, 2} and []int{2, 1} are equal. Only slices of primitive scalars + // like numbers and strings are supported. Slices of complex types, + // like []T where T is a struct, are undefined because Equal does not + // recurse into the slice value when this flag is enabled. + FLAG_IGNORE_SLICE_ORDER +) + type cmp struct { diff []string buff []string floatFormat string + flag map[byte]bool } var errorType = reflect.TypeOf((*error)(nil)).Elem() @@ -74,13 +88,17 @@ var errorType = reflect.TypeOf((*error)(nil)).Elem() // // When comparing a struct, if a field has the tag `deep:"-"` then it will be // ignored. -func Equal(a, b interface{}) []string { +func Equal(a, b interface{}, flags ...interface{}) []string { aVal := reflect.ValueOf(a) bVal := reflect.ValueOf(b) c := &cmp{ diff: []string{}, buff: []string{}, floatFormat: fmt.Sprintf("%%.%df", FloatPrecision), + flag: map[byte]bool{}, + } + for i := range flags { + c.flag[flags[i].(byte)] = true } if a == nil && b == nil { return nil @@ -339,29 +357,54 @@ func (c *cmp) equals(a, b reflect.Value, level int) { } } + // Equal if same underlying pointer and same length, this latter handles + // foo := []int{1, 2, 3, 4} + // a := foo[0:2] // == {1,2} + // b := foo[2:4] // == {3,4} + // a and b are same pointer but different slices (lengths) of the underlying + // array, so not equal. aLen := a.Len() bLen := b.Len() - if a.Pointer() == b.Pointer() && aLen == bLen { return } - n := aLen - if bLen > aLen { - n = bLen - } - for i := 0; i < n; i++ { - c.push(fmt.Sprintf("slice[%d]", i)) - if i < aLen && i < bLen { - c.equals(a.Index(i), b.Index(i), level+1) - } else if i < aLen { - c.saveDiff(a.Index(i), "") - } else { - c.saveDiff("", b.Index(i)) + if c.flag[FLAG_IGNORE_SLICE_ORDER] { + // Compare slices by value and value count; ignore order. + // Value equality is impliclity established by the maps: + // any value v1 will hash to the same map value if it's equal + // to another value v2. Then equality is determiend by value + // count: presuming v1==v2, then the slics are equal if there + // are equal numbers of v1 in each slice. + am := map[interface{}]int{} + for i := 0; i < a.Len(); i++ { + am[a.Index(i).Interface()] += 1 } - c.pop() - if len(c.diff) >= MaxDiff { - break + bm := map[interface{}]int{} + for i := 0; i < b.Len(); i++ { + bm[b.Index(i).Interface()] += 1 + } + c.cmpMapValueCounts(a, b, am, bm, true) // a cmp b + c.cmpMapValueCounts(b, a, bm, am, false) // b cmp a + } else { + // Compare slices by order + n := aLen + if bLen > aLen { + n = bLen + } + for i := 0; i < n; i++ { + c.push(fmt.Sprintf("slice[%d]", i)) + if i < aLen && i < bLen { + c.equals(a.Index(i), b.Index(i), level+1) + } else if i < aLen { + c.saveDiff(a.Index(i), "") + } else { + c.saveDiff("", b.Index(i)) + } + c.pop() + if len(c.diff) >= MaxDiff { + break + } } } @@ -435,6 +478,25 @@ func (c *cmp) saveDiff(aval, bval interface{}) { } } +func (c *cmp) cmpMapValueCounts(a, b reflect.Value, am, bm map[interface{}]int, a2b bool) { + for v := range am { + aCount, _ := am[v] + bCount, _ := bm[v] + + if aCount != bCount { + c.push(fmt.Sprintf("(unordered) slice[]=%v: value count", v)) + if a2b { + c.saveDiff(fmt.Sprintf("%d", aCount), fmt.Sprintf("%d", bCount)) + } else { + c.saveDiff(fmt.Sprintf("%d", bCount), fmt.Sprintf("%d", aCount)) + } + c.pop() + } + delete(am, v) + delete(bm, v) + } +} + func logError(err error) { if LogErrors { log.Println(err) diff --git a/deep_test.go b/deep_test.go index 055a230..bcb7d5a 100644 --- a/deep_test.go +++ b/deep_test.go @@ -4,6 +4,7 @@ import ( "errors" "fmt" "reflect" + "sort" "testing" "time" "unsafe" @@ -1495,3 +1496,88 @@ func TestFunc(t *testing.T) { t.Errorf("expected 0 diff, got %d: %s", len(diff), diff) } } + +func TestSliceOrderString(t *testing.T) { + // https://github.com/go-test/deep/issues/28 + + // These are equal if we ignore order + a := []string{"foo", "bar"} + b := []string{"bar", "foo"} + diff := deep.Equal(a, b, deep.FLAG_IGNORE_SLICE_ORDER) + if len(diff) != 0 { + t.Fatalf("expected 0 diff, got %d: %s", len(diff), diff) + } + + // Equal with dupes + a = []string{"foo", "foo", "bar"} + b = []string{"bar", "foo", "foo"} + diff = deep.Equal(a, b, deep.FLAG_IGNORE_SLICE_ORDER) + if len(diff) != 0 { + t.Fatalf("expected 0 diff, got %d: %s", len(diff), diff) + } + + // NOT equal with dupes + a = []string{"foo", "foo", "bar"} + b = []string{"bar", "bar", "foo"} + diff = deep.Equal(a, b, deep.FLAG_IGNORE_SLICE_ORDER) + if len(diff) != 2 { + t.Fatalf("expected 2 diff, got %d: %s", len(diff), diff) + } + m1 := "(unordered) slice[]=foo: value count: 2 != 1" + m2 := "(unordered) slice[]=bar: value count: 1 != 2" + if diff[0] != m1 && diff[0] != m2 { + t.Errorf("got %s, expected '%s' or '%s'", diff[0], m1, m2) + } + if diff[1] != m1 && diff[1] != m2 { + t.Errorf("got %s, expected '%s' or '%s'", diff[1], m1, m2) + } + + // NOT equal with one missing + a = []string{"foo", "bar"} + b = []string{"bar", "foo", "gone"} + diff = deep.Equal(a, b, deep.FLAG_IGNORE_SLICE_ORDER) + if len(diff) != 1 { + t.Fatalf("expected 2 diff, got %d: %s", len(diff), diff) + } + if diff[0] != "(unordered) slice[]=gone: value count: 0 != 1" { + t.Errorf("got %s, expected ''", diff[0]) + } + + // NOT equal at all + a = []string{"foo", "bar"} + b = []string{"x"} + diff = deep.Equal(a, b, deep.FLAG_IGNORE_SLICE_ORDER) + if len(diff) != 3 { + t.Fatalf("expected 2 diff, got %d: %s", len(diff), diff) + } + sort.Strings(diff) + if diff[0] != "(unordered) slice[]=bar: value count: 1 != 0" { + t.Errorf("got %s, expected '(unordered) slice[]=bar: value count: 1 != 0'", diff[0]) + } + if diff[1] != "(unordered) slice[]=foo: value count: 1 != 0" { + t.Errorf("got %s, expected '(unordered) slice[]=foo: value count: 1 != 0", diff[1]) + } + if diff[2] != "(unordered) slice[]=x: value count: 0 != 1" { + t.Errorf("got %s, expected '(unordered) slice[]=x: value count: 0 != 1'", diff[2]) + } +} + +func TestSliceOrderStruct(t *testing.T) { + // https://github.com/go-test/deep/issues/28 + // This is NOT supported but Go is so wonderful that it just happens to work. + // But again: not supported. So if this test starts to fail or be a problem, + // it can and should be removed becuase the docs say it's not supported. + type T struct{ i int } + a := []T{ + {i: 1}, + {i: 2}, + } + b := []T{ + {i: 2}, + {i: 1}, + } + diff := deep.Equal(a, b, deep.FLAG_IGNORE_SLICE_ORDER) + if len(diff) != 0 { + t.Fatalf("expected 0 diff, got %d: %s", len(diff), diff) + } +}