Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implment issue 28 with FLAG_IGNORE_SLICE_ORDER #56

Merged
merged 5 commits into from Dec 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
96 changes: 79 additions & 17 deletions deep.go
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
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)
}
}