Skip to content

Commit

Permalink
Merge pull request #56 from go-test/issue-28-and-flags
Browse files Browse the repository at this point in the history
Implment issue 28 with FLAG_IGNORE_SLICE_ORDER
  • Loading branch information
daniel-nichter authored Dec 9, 2022
2 parents b036568 + d502c9f commit 2f12927
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 17 deletions.
96 changes: 79 additions & 17 deletions deep.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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), "<no value>")
} else {
c.saveDiff("<no value>", 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), "<no value>")
} else {
c.saveDiff("<no value>", b.Index(i))
}
c.pop()
if len(c.diff) >= MaxDiff {
break
}
}
}

Expand Down Expand Up @@ -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)
Expand Down
86 changes: 86 additions & 0 deletions deep_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"reflect"
"sort"
"testing"
"time"
"unsafe"
Expand Down Expand Up @@ -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)
}
}

0 comments on commit 2f12927

Please sign in to comment.