aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoe Tsai <joetsai@digital-static.net>2023-08-31 10:32:40 -0700
committerGitHub <noreply@github.com>2023-08-31 10:32:40 -0700
commitc3ad8435e7bef96af35732bc0789e5a2278c6d5f (patch)
treea56d226234088bb5c9cf3105bb8cfa12adf6c21b
parente250a55e913727afa4a4ccf87d716232288d9b5f (diff)
downloadgo-cmp-upstream-master.tar.gz
Add cmpopts.EquateComparable (#340)upstream-master
This helper function makes it easier to specify that comparable types are safe to directly compare with the == operator in Go. The API does not use generics as it follows existing options like cmp.AllowUnexported, cmpopts.IgnoreUnexported, or cmpopts.IgnoreTypes. While generics provides type safety, the user experience is not as nice. Our current API allows multiple types to be specified: cmpopts.EquateComparable(netip.Addr{}, netip.Prefix{}) While generics would not allow variadic arguments: cmpopts.EquateComparable[netip.Addr]() cmpopts.EquateComparable[netip.Prefix]() Bump mininimum supported Go to 1.18 for net/netip type. Start testing on Go 1.21. Fixes #339
-rw-r--r--.github/workflows/test.yml4
-rw-r--r--cmp/cmpopts/equate.go29
-rw-r--r--cmp/cmpopts/util_test.go31
-rw-r--r--cmp/options.go2
4 files changed, 64 insertions, 2 deletions
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index a85a606..e21ebfa 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -6,7 +6,7 @@ jobs:
test:
strategy:
matrix:
- go-version: [1.13.x, 1.14.x, 1.15.x, 1.16.x, 1.17.x, 1.18.x, 1.19.x, 1.20.x]
+ go-version: [1.18.x, 1.19.x, 1.20.x, 1.21.x]
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
@@ -19,5 +19,5 @@ jobs:
- name: Test
run: go test -v -race ./...
- name: Format
- if: matrix.go-version == '1.20.x'
+ if: matrix.go-version == '1.21.x'
run: diff -u <(echo -n) <(gofmt -d .)
diff --git a/cmp/cmpopts/equate.go b/cmp/cmpopts/equate.go
index 90974e6..3d8d0cd 100644
--- a/cmp/cmpopts/equate.go
+++ b/cmp/cmpopts/equate.go
@@ -7,6 +7,7 @@ package cmpopts
import (
"errors"
+ "fmt"
"math"
"reflect"
"time"
@@ -154,3 +155,31 @@ func compareErrors(x, y interface{}) bool {
ye := y.(error)
return errors.Is(xe, ye) || errors.Is(ye, xe)
}
+
+// EquateComparable returns a [cmp.Option] that determines equality
+// of comparable types by directly comparing them using the == operator in Go.
+// The types to compare are specified by passing a value of that type.
+// This option should only be used on types that are documented as being
+// safe for direct == comparison. For example, [net/netip.Addr] is documented
+// as being semantically safe to use with ==, while [time.Time] is documented
+// to discourage the use of == on time values.
+func EquateComparable(typs ...interface{}) cmp.Option {
+ types := make(typesFilter)
+ for _, typ := range typs {
+ switch t := reflect.TypeOf(typ); {
+ case !t.Comparable():
+ panic(fmt.Sprintf("%T is not a comparable Go type", typ))
+ case types[t]:
+ panic(fmt.Sprintf("%T is already specified", typ))
+ default:
+ types[t] = true
+ }
+ }
+ return cmp.FilterPath(types.filter, cmp.Comparer(equateAny))
+}
+
+type typesFilter map[reflect.Type]bool
+
+func (tf typesFilter) filter(p cmp.Path) bool { return tf[p.Last().Type()] }
+
+func equateAny(x, y interface{}) bool { return x == y }
diff --git a/cmp/cmpopts/util_test.go b/cmp/cmpopts/util_test.go
index 7adeb9b..6a7c300 100644
--- a/cmp/cmpopts/util_test.go
+++ b/cmp/cmpopts/util_test.go
@@ -10,6 +10,7 @@ import (
"fmt"
"io"
"math"
+ "net/netip"
"reflect"
"strings"
"sync"
@@ -677,6 +678,36 @@ func TestOptions(t *testing.T) {
wantEqual: false,
reason: "AnyError is not equal to nil value",
}, {
+ label: "EquateComparable",
+ x: []struct{ P netip.Addr }{
+ {netip.AddrFrom4([4]byte{1, 2, 3, 4})},
+ {netip.AddrFrom4([4]byte{1, 2, 3, 5})},
+ {netip.AddrFrom4([4]byte{1, 2, 3, 6})},
+ },
+ y: []struct{ P netip.Addr }{
+ {netip.AddrFrom4([4]byte{1, 2, 3, 4})},
+ {netip.AddrFrom4([4]byte{1, 2, 3, 5})},
+ {netip.AddrFrom4([4]byte{1, 2, 3, 6})},
+ },
+ opts: []cmp.Option{EquateComparable(netip.Addr{})},
+ wantEqual: true,
+ reason: "equal because all IP addresses are the same",
+ }, {
+ label: "EquateComparable",
+ x: []struct{ P netip.Addr }{
+ {netip.AddrFrom4([4]byte{1, 2, 3, 4})},
+ {netip.AddrFrom4([4]byte{1, 2, 3, 5})},
+ {netip.AddrFrom4([4]byte{1, 2, 3, 6})},
+ },
+ y: []struct{ P netip.Addr }{
+ {netip.AddrFrom4([4]byte{1, 2, 3, 4})},
+ {netip.AddrFrom4([4]byte{1, 2, 3, 7})},
+ {netip.AddrFrom4([4]byte{1, 2, 3, 6})},
+ },
+ opts: []cmp.Option{EquateComparable(netip.Addr{})},
+ wantEqual: false,
+ reason: "not equal because second IP address is different",
+ }, {
label: "IgnoreFields",
x: Bar1{Foo3{&Foo2{&Foo1{Alpha: 5}}}},
y: Bar1{Foo3{&Foo2{&Foo1{Alpha: 6}}}},
diff --git a/cmp/options.go b/cmp/options.go
index 518b6ac..754496f 100644
--- a/cmp/options.go
+++ b/cmp/options.go
@@ -234,6 +234,8 @@ func (validator) apply(s *state, vx, vy reflect.Value) {
name = fmt.Sprintf("%q.%v", t.PkgPath(), t.Name()) // e.g., "path/to/package".MyType
if _, ok := reflect.New(t).Interface().(error); ok {
help = "consider using cmpopts.EquateErrors to compare error values"
+ } else if t.Comparable() {
+ help = "consider using cmpopts.EquateComparable to compare comparable Go types"
}
} else {
// Unnamed type with unexported fields. Derive PkgPath from field.