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

Add cmpopts.EquateComparable #340

Merged
merged 1 commit into from Aug 31, 2023
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
4 changes: 2 additions & 2 deletions .github/workflows/test.yml
Expand Up @@ -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:
Expand All @@ -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 .)
29 changes: 29 additions & 0 deletions cmp/cmpopts/equate.go
Expand Up @@ -7,6 +7,7 @@ package cmpopts

import (
"errors"
"fmt"
"math"
"reflect"
"time"
Expand Down Expand Up @@ -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 }
31 changes: 31 additions & 0 deletions cmp/cmpopts/util_test.go
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"io"
"math"
"net/netip"
"reflect"
"strings"
"sync"
Expand Down Expand Up @@ -676,6 +677,36 @@ func TestOptions(t *testing.T) {
opts: []cmp.Option{EquateErrors()},
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}}}},
Expand Down
2 changes: 2 additions & 0 deletions cmp/options.go
Expand Up @@ -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.
Expand Down