Skip to content

Commit

Permalink
Add IgnoreSliceElements and IgnoreMapEntries helpers (#126)
Browse files Browse the repository at this point in the history
These helper options ignore slice elements or map entries based
on a user-provided predicate function. These are especially useful
for ignoring missing elements or entries.
  • Loading branch information
dsnet committed Mar 12, 2019
1 parent 3177a94 commit 0376dcf
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 13 deletions.
58 changes: 58 additions & 0 deletions cmp/cmpopts/ignore.go
Expand Up @@ -11,6 +11,7 @@ import (
"unicode/utf8"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/internal/function"
)

// IgnoreFields returns an Option that ignores exported fields of the
Expand Down Expand Up @@ -147,3 +148,60 @@ func isExported(id string) bool {
r, _ := utf8.DecodeRuneInString(id)
return unicode.IsUpper(r)
}

// IgnoreSliceElements returns an Option that ignores elements of []V.
// The discard function must be of the form "func(T) bool" which is used to
// ignore slice elements of type V, where V is assignable to T.
// Elements are ignored if the function reports true.
func IgnoreSliceElements(discardFunc interface{}) cmp.Option {
vf := reflect.ValueOf(discardFunc)
if !function.IsType(vf.Type(), function.ValuePredicate) || vf.IsNil() {
panic(fmt.Sprintf("invalid discard function: %T", discardFunc))
}
return cmp.FilterPath(func(p cmp.Path) bool {
si, ok := p.Index(-1).(cmp.SliceIndex)
if !ok {
return false
}
if !si.Type().AssignableTo(vf.Type().In(0)) {
return false
}
vx, vy := si.Values()
if vx.IsValid() && vf.Call([]reflect.Value{vx})[0].Bool() {
return true
}
if vy.IsValid() && vf.Call([]reflect.Value{vy})[0].Bool() {
return true
}
return false
}, cmp.Ignore())
}

// IgnoreMapEntries returns an Option that ignores entries of map[K]V.
// The discard function must be of the form "func(T, R) bool" which is used to
// ignore map entries of type K and V, where K and V are assignable to T and R.
// Entries are ignored if the function reports true.
func IgnoreMapEntries(discardFunc interface{}) cmp.Option {
vf := reflect.ValueOf(discardFunc)
if !function.IsType(vf.Type(), function.KeyValuePredicate) || vf.IsNil() {
panic(fmt.Sprintf("invalid discard function: %T", discardFunc))
}
return cmp.FilterPath(func(p cmp.Path) bool {
mi, ok := p.Index(-1).(cmp.MapIndex)
if !ok {
return false
}
if !mi.Key().Type().AssignableTo(vf.Type().In(0)) || !mi.Type().AssignableTo(vf.Type().In(1)) {
return false
}
k := mi.Key()
vx, vy := mi.Values()
if vx.IsValid() && vf.Call([]reflect.Value{k, vx})[0].Bool() {
return true
}
if vy.IsValid() && vf.Call([]reflect.Value{k, vy})[0].Bool() {
return true
}
return false
}, cmp.Ignore())
}
12 changes: 6 additions & 6 deletions cmp/cmpopts/sort.go
Expand Up @@ -26,10 +26,10 @@ import (
// !less(y, x) for two elements x and y, their relative order is maintained.
//
// SortSlices can be used in conjunction with EquateEmpty.
func SortSlices(less interface{}) cmp.Option {
vf := reflect.ValueOf(less)
func SortSlices(lessFunc interface{}) cmp.Option {
vf := reflect.ValueOf(lessFunc)
if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
panic(fmt.Sprintf("invalid less function: %T", less))
panic(fmt.Sprintf("invalid less function: %T", lessFunc))
}
ss := sliceSorter{vf.Type().In(0), vf}
return cmp.FilterValues(ss.filter, cmp.Transformer("cmpopts.SortSlices", ss.sort))
Expand Down Expand Up @@ -97,10 +97,10 @@ func (ss sliceSorter) less(v reflect.Value, i, j int) bool {
// • Total: if x != y, then either less(x, y) or less(y, x)
//
// SortMaps can be used in conjunction with EquateEmpty.
func SortMaps(less interface{}) cmp.Option {
vf := reflect.ValueOf(less)
func SortMaps(lessFunc interface{}) cmp.Option {
vf := reflect.ValueOf(lessFunc)
if !function.IsType(vf.Type(), function.Less) || vf.IsNil() {
panic(fmt.Sprintf("invalid less function: %T", less))
panic(fmt.Sprintf("invalid less function: %T", lessFunc))
}
ms := mapSorter{vf.Type().In(0), vf}
return cmp.FilterValues(ms.filter, cmp.Transformer("cmpopts.SortMaps", ms.sort))
Expand Down
76 changes: 76 additions & 0 deletions cmp/cmpopts/util_test.go
Expand Up @@ -20,7 +20,9 @@ import (

type (
MyInt int
MyInts []int
MyFloat float32
MyString string
MyTime struct{ time.Time }
MyStruct struct {
A, B []int
Expand Down Expand Up @@ -716,6 +718,80 @@ func TestOptions(t *testing.T) {
},
wantEqual: true,
reason: "equal because all Ignore options can be composed together",
}, {
label: "IgnoreSliceElements",
x: []int{1, 0, 2, 3, 0, 4, 0, 0},
y: []int{0, 0, 0, 0, 1, 2, 3, 4},
opts: []cmp.Option{
IgnoreSliceElements(func(v int) bool { return v == 0 }),
},
wantEqual: true,
reason: "equal because zero elements are ignored",
}, {
label: "IgnoreSliceElements",
x: []MyInt{1, 0, 2, 3, 0, 4, 0, 0},
y: []MyInt{0, 0, 0, 0, 1, 2, 3, 4},
opts: []cmp.Option{
IgnoreSliceElements(func(v int) bool { return v == 0 }),
},
wantEqual: false,
reason: "not equal because MyInt is not assignable to int",
}, {
label: "IgnoreSliceElements",
x: MyInts{1, 0, 2, 3, 0, 4, 0, 0},
y: MyInts{0, 0, 0, 0, 1, 2, 3, 4},
opts: []cmp.Option{
IgnoreSliceElements(func(v int) bool { return v == 0 }),
},
wantEqual: true,
reason: "equal because the element type of MyInts is assignable to int",
}, {
label: "IgnoreSliceElements+EquateEmpty",
x: []MyInt{},
y: []MyInt{0, 0, 0, 0},
opts: []cmp.Option{
IgnoreSliceElements(func(v int) bool { return v == 0 }),
EquateEmpty(),
},
wantEqual: false,
reason: "not equal because ignored elements does not imply empty slice",
}, {
label: "IgnoreMapEntries",
x: map[string]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5},
y: map[string]int{"one": 1, "three": 3, "TEN": 10},
opts: []cmp.Option{
IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }),
},
wantEqual: true,
reason: "equal because uppercase keys are ignored",
}, {
label: "IgnoreMapEntries",
x: map[MyString]int{"one": 1, "TWO": 2, "three": 3, "FIVE": 5},
y: map[MyString]int{"one": 1, "three": 3, "TEN": 10},
opts: []cmp.Option{
IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }),
},
wantEqual: false,
reason: "not equal because MyString is not assignable to string",
}, {
label: "IgnoreMapEntries",
x: map[string]MyInt{"one": 1, "TWO": 2, "three": 3, "FIVE": 5},
y: map[string]MyInt{"one": 1, "three": 3, "TEN": 10},
opts: []cmp.Option{
IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }),
},
wantEqual: false,
reason: "not equal because MyInt is not assignable to int",
}, {
label: "IgnoreMapEntries+EquateEmpty",
x: map[string]MyInt{"ONE": 1, "TWO": 2, "THREE": 3},
y: nil,
opts: []cmp.Option{
IgnoreMapEntries(func(k string, v int) bool { return strings.ToUpper(k) == k }),
EquateEmpty(),
},
wantEqual: false,
reason: "not equal because ignored entries does not imply empty map",
}, {
label: "AcyclicTransformer",
x: "a\nb\nc\nd",
Expand Down
4 changes: 2 additions & 2 deletions cmp/cmpopts/xform.go
Expand Up @@ -29,7 +29,7 @@ func (xf xformFilter) filter(p cmp.Path) bool {
//
// Had this been an unfiltered Transformer instead, this would result in an
// infinite cycle converting a string to []string to [][]string and so on.
func AcyclicTransformer(name string, f interface{}) cmp.Option {
xf := xformFilter{cmp.Transformer(name, f)}
func AcyclicTransformer(name string, xformFunc interface{}) cmp.Option {
xf := xformFilter{cmp.Transformer(name, xformFunc)}
return cmp.FilterPath(xf.filter, xf.xform)
}
22 changes: 17 additions & 5 deletions cmp/internal/function/func.go
Expand Up @@ -17,15 +17,19 @@ type funcType int
const (
_ funcType = iota

tbFunc // func(T) bool
ttbFunc // func(T, T) bool
trbFunc // func(T, R) bool
tibFunc // func(T, I) bool
trFunc // func(T) R

Equal = ttbFunc // func(T, T) bool
EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool
Transformer = trFunc // func(T) R
ValueFilter = ttbFunc // func(T, T) bool
Less = ttbFunc // func(T, T) bool
Equal = ttbFunc // func(T, T) bool
EqualAssignable = tibFunc // func(T, I) bool; encapsulates func(T, T) bool
Transformer = trFunc // func(T) R
ValueFilter = ttbFunc // func(T, T) bool
Less = ttbFunc // func(T, T) bool
ValuePredicate = tbFunc // func(T) bool
KeyValuePredicate = trbFunc // func(T, R) bool
)

var boolType = reflect.TypeOf(true)
Expand All @@ -37,10 +41,18 @@ func IsType(t reflect.Type, ft funcType) bool {
}
ni, no := t.NumIn(), t.NumOut()
switch ft {
case tbFunc: // func(T) bool
if ni == 1 && no == 1 && t.Out(0) == boolType {
return true
}
case ttbFunc: // func(T, T) bool
if ni == 2 && no == 1 && t.In(0) == t.In(1) && t.Out(0) == boolType {
return true
}
case trbFunc: // func(T, R) bool
if ni == 2 && no == 1 && t.Out(0) == boolType {
return true
}
case tibFunc: // func(T, I) bool
if ni == 2 && no == 1 && t.In(0).AssignableTo(t.In(1)) && t.Out(0) == boolType {
return true
Expand Down

0 comments on commit 0376dcf

Please sign in to comment.