testing/protocmp: add SortRepeated and SortRepeatedFields

SortRepeated is similar to cmpopts.SortSlice where it accepts a
user-provided sort function, but only operates on repeated fields.
It pattern matches based on sort element type.

SortRepeatedFields is similar to SortRepeated, but chooses an
arbitrary sort order for the specified (by name) repeated fields.
It pattern matches based on message field name.

Change-Id: Ib6ef282e5394cf7b22522161d524f22e1b76677a
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/221432
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/testing/protocmp/util.go b/testing/protocmp/util.go
index 7455591..668bb2e 100644
--- a/testing/protocmp/util.go
+++ b/testing/protocmp/util.go
@@ -12,6 +12,7 @@
 	"strings"
 
 	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
 
 	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/reflect/protoreflect"
@@ -501,3 +502,178 @@
 		return strings.Trim(mi.Key().String(), "0123456789") == ""
 	}, cmp.Ignore())
 }
+
+// SortRepeated sorts repeated fields of the specified element type.
+// The less function must be of the form "func(T, T) bool" where T is the
+// Go element type for the repeated field kind.
+//
+// The element type T can be one of the following:
+//	• Go type for a protobuf scalar kind except for an enum
+//	  (i.e., bool, int32, int64, uint32, uint64, float32, float64, string, and []byte)
+//	• E where E is a concrete enum type that implements protoreflect.Enum
+//	• M where M is a concrete message type that implement proto.Message
+//
+// This option only applies to repeated fields within a protobuf message.
+// It does not operate on higher-order Go types that seem like a repeated field.
+// For example, a []T outside the context of a protobuf message will not be
+// handled by this option. To sort Go slices that are not repeated fields,
+// consider using "github.com/google/go-cmp/cmp/cmpopts".SortSlices instead.
+//
+// This must be used in conjunction with Transform.
+func SortRepeated(lessFunc interface{}) cmp.Option {
+	t, ok := checkTTBFunc(lessFunc)
+	if !ok {
+		panic(fmt.Sprintf("invalid less function: %T", lessFunc))
+	}
+
+	var opt cmp.Option
+	var sliceType reflect.Type
+	switch vf := reflect.ValueOf(lessFunc); {
+	case t.Implements(enumV2Type):
+		et := reflect.Zero(t).Interface().(protoreflect.Enum).Type()
+		lessFunc = func(x, y Enum) bool {
+			vx := reflect.ValueOf(et.New(x.Number()))
+			vy := reflect.ValueOf(et.New(y.Number()))
+			return vf.Call([]reflect.Value{vx, vy})[0].Bool()
+		}
+		opt = FilterDescriptor(et.Descriptor(), cmpopts.SortSlices(lessFunc))
+		sliceType = reflect.SliceOf(enumReflectType)
+	case t.Implements(messageV2Type):
+		mt := reflect.Zero(t).Interface().(protoreflect.ProtoMessage).ProtoReflect().Type()
+		lessFunc = func(x, y Message) bool {
+			mx := mt.New().Interface()
+			my := mt.New().Interface()
+			proto.Merge(mx, x)
+			proto.Merge(my, y)
+			vx := reflect.ValueOf(mx)
+			vy := reflect.ValueOf(my)
+			return vf.Call([]reflect.Value{vx, vy})[0].Bool()
+		}
+		opt = FilterDescriptor(mt.Descriptor(), cmpopts.SortSlices(lessFunc))
+		sliceType = reflect.SliceOf(messageReflectType)
+	default:
+		switch t {
+		case reflect.TypeOf(bool(false)):
+		case reflect.TypeOf(int32(0)):
+		case reflect.TypeOf(int64(0)):
+		case reflect.TypeOf(uint32(0)):
+		case reflect.TypeOf(uint64(0)):
+		case reflect.TypeOf(float32(0)):
+		case reflect.TypeOf(float64(0)):
+		case reflect.TypeOf(string("")):
+		case reflect.TypeOf([]byte(nil)):
+		default:
+			panic(fmt.Sprintf("invalid element type: %v", t))
+		}
+		opt = cmpopts.SortSlices(lessFunc)
+		sliceType = reflect.SliceOf(t)
+	}
+
+	return cmp.FilterPath(func(p cmp.Path) bool {
+		// Filter to only apply to repeated fields within a message.
+		if t := p.Index(-1).Type(); t == nil || t != sliceType {
+			return false
+		}
+		if t := p.Index(-2).Type(); t == nil || t.Kind() != reflect.Interface {
+			return false
+		}
+		if t := p.Index(-3).Type(); t == nil || t != messageReflectType {
+			return false
+		}
+		return true
+	}, opt)
+}
+
+func checkTTBFunc(lessFunc interface{}) (reflect.Type, bool) {
+	switch t := reflect.TypeOf(lessFunc); {
+	case t == nil:
+		return nil, false
+	case t.NumIn() != 2 || t.In(0) != t.In(1) || t.IsVariadic():
+		return nil, false
+	case t.NumOut() != 1 || t.Out(0) != reflect.TypeOf(false):
+		return nil, false
+	default:
+		return t.In(0), true
+	}
+}
+
+// SortRepeatedFields sorts the specified repeated fields.
+// Sorting a repeated field is useful for treating the list as a multiset
+// (i.e., a set where each value can appear multiple times).
+// It panics if the field does not exist or is not a repeated field.
+//
+// The sort ordering is as follows:
+//	• Booleans are sorted where false is sorted before true.
+//	• Integers are sorted in ascending order.
+//	• Floating-point numbers are sorted in ascending order according to
+//	  the total ordering defined by IEEE-754 (section 5.10).
+//	• Strings and bytes are sorted lexicographically in ascending order.
+//	• Enums are sorted in ascending order based on its numeric value.
+//	• Messages are sorted according to some arbitrary ordering
+//	  which is undefined and may change in future implementations.
+//
+// The ordering chosen for repeated messages is unlikely to be aesthetically
+// preferred by humans. Consider using a custom sort function:
+//
+//	FilterField(m, "foo_field", SortRepeated(func(x, y *foopb.MyMessage) bool {
+//	    ... // user-provided definition for less
+//	}))
+//
+// This must be used in conjunction with Transform.
+func SortRepeatedFields(message proto.Message, names ...protoreflect.Name) cmp.Option {
+	var opts cmp.Options
+	md := message.ProtoReflect().Descriptor()
+	for _, name := range names {
+		fd := mustFindFieldDescriptor(md, name)
+		if !fd.IsList() {
+			panic(fmt.Sprintf("message field %q is not repeated", fd.FullName()))
+		}
+
+		var lessFunc interface{}
+		switch fd.Kind() {
+		case protoreflect.BoolKind:
+			lessFunc = func(x, y bool) bool { return !x && y }
+		case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
+			lessFunc = func(x, y int32) bool { return x < y }
+		case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
+			lessFunc = func(x, y int64) bool { return x < y }
+		case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
+			lessFunc = func(x, y uint32) bool { return x < y }
+		case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
+			lessFunc = func(x, y uint64) bool { return x < y }
+		case protoreflect.FloatKind:
+			lessFunc = lessF32
+		case protoreflect.DoubleKind:
+			lessFunc = lessF64
+		case protoreflect.StringKind:
+			lessFunc = func(x, y string) bool { return x < y }
+		case protoreflect.BytesKind:
+			lessFunc = func(x, y []byte) bool { return bytes.Compare(x, y) < 0 }
+		case protoreflect.EnumKind:
+			lessFunc = func(x, y Enum) bool { return x.Number() < y.Number() }
+		case protoreflect.MessageKind, protoreflect.GroupKind:
+			lessFunc = func(x, y Message) bool { return x.String() < y.String() }
+		default:
+			panic(fmt.Sprintf("invalid kind: %v", fd.Kind()))
+		}
+		opts = append(opts, FilterDescriptor(fd, cmpopts.SortSlices(lessFunc)))
+	}
+	return opts
+}
+
+func lessF32(x, y float32) bool {
+	// Bit-wise implementation of IEEE-754, section 5.10.
+	xi := int32(math.Float32bits(x))
+	yi := int32(math.Float32bits(y))
+	xi ^= int32(uint32(xi>>31) >> 1)
+	yi ^= int32(uint32(yi>>31) >> 1)
+	return xi < yi
+}
+func lessF64(x, y float64) bool {
+	// Bit-wise implementation of IEEE-754, section 5.10.
+	xi := int64(math.Float64bits(x))
+	yi := int64(math.Float64bits(y))
+	xi ^= int64(uint64(xi>>63) >> 1)
+	yi ^= int64(uint64(yi>>63) >> 1)
+	return xi < yi
+}
diff --git a/testing/protocmp/util_test.go b/testing/protocmp/util_test.go
index 6f41ba4..728dcc7 100644
--- a/testing/protocmp/util_test.go
+++ b/testing/protocmp/util_test.go
@@ -5,6 +5,9 @@
 package protocmp
 
 import (
+	"math"
+	"math/rand"
+	"sort"
 	"testing"
 
 	"github.com/google/go-cmp/cmp"
@@ -1023,6 +1026,263 @@
 		want: true,
 	}}...)
 
+	// Test SortRepeated.
+	type higherOrderType struct {
+		M    *testpb.TestAllTypes
+		I32s []int32
+		Es   []testpb.TestAllTypes_NestedEnum
+		Ms   []*testpb.ForeignMessage
+	}
+	tests = append(tests, []test{{
+		x:    &testpb.TestAllTypes{RepeatedInt32: []int32{3, 2, 1, 2, 3, 3}},
+		y:    &testpb.TestAllTypes{RepeatedInt32: []int32{2, 3, 3, 2, 1, 3}},
+		opts: cmp.Options{Transform()},
+		want: false,
+	}, {
+		x: &testpb.TestAllTypes{RepeatedInt32: []int32{3, 2, 1, 2, 3, 3}},
+		y: &testpb.TestAllTypes{RepeatedInt32: []int32{2, 3, 3, 2, 1, 3}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y int32) bool { return x < y }),
+		},
+		want: true,
+	}, {
+		x: higherOrderType{
+			M:    &testpb.TestAllTypes{RepeatedInt32: []int32{3, 2, 1, 2, 3, 3}},
+			I32s: []int32{3, 2, 1, 2, 3, 3},
+		},
+		y: higherOrderType{
+			M:    &testpb.TestAllTypes{RepeatedInt32: []int32{2, 3, 3, 2, 1, 3}},
+			I32s: []int32{2, 3, 3, 2, 1, 3},
+		},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y int32) bool { return x < y }),
+		},
+		want: false, // sort does not apply to []int32 outside of a message
+	}, {
+		x: &testpb.TestAllTypes{RepeatedInt32: []int32{3, 2, 1, 2, 3, 3}},
+		y: &testpb.TestAllTypes{RepeatedInt32: []int32{2, 3, 3, 2, 1, 3}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y int64) bool { return x < y }),
+		},
+		want: false, // wrong sort type: int32 != int64
+	}, {
+		x:    &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAR, testpb.TestAllTypes_BAZ}},
+		y:    &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR, testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAZ}},
+		opts: cmp.Options{Transform()},
+		want: false,
+	}, {
+		x: &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAR, testpb.TestAllTypes_BAZ}},
+		y: &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR, testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAZ}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y testpb.TestAllTypes_NestedEnum) bool { return x < y }),
+		},
+		want: true,
+	}, {
+		x: higherOrderType{
+			M:  &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAR, testpb.TestAllTypes_BAZ}},
+			Es: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAR, testpb.TestAllTypes_BAZ},
+		},
+		y: higherOrderType{
+			M:  &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR, testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAZ}},
+			Es: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR, testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAZ},
+		},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y testpb.TestAllTypes_NestedEnum) bool { return x < y }),
+		},
+		want: false, // sort does not apply to []testpb.TestAllTypes_NestedEnum outside of a message
+	}, {
+		x: &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAR, testpb.TestAllTypes_BAZ}},
+		y: &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR, testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAZ}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y testpb.ForeignEnum) bool { return x < y }),
+		},
+		want: false, // wrong sort type: testpb.TestAllTypes_NestedEnum != testpb.ForeignEnum
+	}, {
+		x:    &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{{}, {C: proto.Int32(3)}, nil, {C: proto.Int32(3)}, {C: proto.Int32(5)}, {C: proto.Int32(4)}}},
+		y:    &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(3)}, {}, {C: proto.Int32(4)}, {C: proto.Int32(3)}, {C: proto.Int32(5)}}},
+		opts: cmp.Options{Transform()},
+		want: false,
+	}, {
+		x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{{}, {C: proto.Int32(3)}, nil, {C: proto.Int32(3)}, {C: proto.Int32(5)}, {C: proto.Int32(4)}}},
+		y: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(3)}, {}, {C: proto.Int32(4)}, {C: proto.Int32(3)}, {C: proto.Int32(5)}}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y *testpb.ForeignMessage) bool { return x.GetC() < y.GetC() }),
+		},
+		want: true,
+	}, {
+		x: higherOrderType{
+			M:  &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{{}, {C: proto.Int32(3)}, nil, {C: proto.Int32(3)}, {C: proto.Int32(5)}, {C: proto.Int32(4)}}},
+			Ms: []*testpb.ForeignMessage{{}, {C: proto.Int32(3)}, nil, {C: proto.Int32(3)}, {C: proto.Int32(5)}, {C: proto.Int32(4)}},
+		},
+		y: higherOrderType{
+			M:  &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(3)}, {}, {C: proto.Int32(4)}, {C: proto.Int32(3)}, {C: proto.Int32(5)}}},
+			Ms: []*testpb.ForeignMessage{nil, {C: proto.Int32(3)}, {}, {C: proto.Int32(4)}, {C: proto.Int32(3)}, {C: proto.Int32(5)}},
+		},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y *testpb.ForeignMessage) bool { return x.GetC() < y.GetC() }),
+		},
+		want: false, // sort does not apply to []*testpb.ForeignMessage outside of a message
+	}, {
+		x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{{}, {C: proto.Int32(3)}, nil, {C: proto.Int32(3)}, {C: proto.Int32(5)}, {C: proto.Int32(4)}}},
+		y: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(3)}, {}, {C: proto.Int32(4)}, {C: proto.Int32(3)}, {C: proto.Int32(5)}}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y *testpb.TestAllTypes_NestedMessage) bool { return x.GetA() < y.GetA() }),
+		},
+		want: false, // wrong sort type: *testpb.ForeignMessage != *testpb.TestAllTypes_NestedMessage
+	}, {
+		x: &testpb.TestAllTypes{
+			RepeatedInt32:    []int32{-32, +32},
+			RepeatedSint32:   []int32{-32, +32},
+			RepeatedSfixed32: []int32{-32, +32},
+			RepeatedInt64:    []int64{-64, +64},
+			RepeatedSint64:   []int64{-64, +64},
+			RepeatedSfixed64: []int64{-64, +64},
+			RepeatedUint32:   []uint32{0, 32},
+			RepeatedFixed32:  []uint32{0, 32},
+			RepeatedUint64:   []uint64{0, 64},
+			RepeatedFixed64:  []uint64{0, 64},
+		},
+		y: &testpb.TestAllTypes{
+			RepeatedInt32:    []int32{+32, -32},
+			RepeatedSint32:   []int32{+32, -32},
+			RepeatedSfixed32: []int32{+32, -32},
+			RepeatedInt64:    []int64{+64, -64},
+			RepeatedSint64:   []int64{+64, -64},
+			RepeatedSfixed64: []int64{+64, -64},
+			RepeatedUint32:   []uint32{32, 0},
+			RepeatedFixed32:  []uint32{32, 0},
+			RepeatedUint64:   []uint64{64, 0},
+			RepeatedFixed64:  []uint64{64, 0},
+		},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeated(func(x, y int32) bool { return x < y }),
+			SortRepeated(func(x, y int64) bool { return x < y }),
+			SortRepeated(func(x, y uint32) bool { return x < y }),
+			SortRepeated(func(x, y uint64) bool { return x < y }),
+		},
+		want: true,
+	}}...)
+
+	// Test SortRepeatedFields.
+	tests = append(tests, []test{{
+		x:    &testpb.TestAllTypes{RepeatedInt32: []int32{3, 2, 1, 2, 3, 3}},
+		y:    &testpb.TestAllTypes{RepeatedInt32: []int32{2, 3, 3, 2, 1, 3}},
+		opts: cmp.Options{Transform()},
+		want: false,
+	}, {
+		x: &testpb.TestAllTypes{RepeatedInt32: []int32{3, 2, 1, 2, 3, 3}},
+		y: &testpb.TestAllTypes{RepeatedInt32: []int32{2, 3, 3, 2, 1, 3}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeatedFields(new(testpb.TestAllTypes), "repeated_int32"),
+		},
+		want: true,
+	}, {
+		x: &testpb.TestAllTypes{RepeatedInt32: []int32{3, 2, 1, 2, 3, 3}},
+		y: &testpb.TestAllTypes{RepeatedInt32: []int32{2, 3, 3, 2, 1, 3}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeatedFields(new(testpb.TestAllTypes), "repeated_int64"),
+		},
+		want: false, // wrong field: repeated_int32 != repeated_int64
+	}, {
+		x:    &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAR, testpb.TestAllTypes_BAZ}},
+		y:    &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR, testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAZ}},
+		opts: cmp.Options{Transform()},
+		want: false,
+	}, {
+		x: &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAR, testpb.TestAllTypes_BAZ}},
+		y: &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR, testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAZ}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeatedFields(new(testpb.TestAllTypes), "repeated_nested_enum"),
+		},
+		want: true,
+	}, {
+		x: &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAR, testpb.TestAllTypes_BAZ}},
+		y: &testpb.TestAllTypes{RepeatedNestedEnum: []testpb.TestAllTypes_NestedEnum{testpb.TestAllTypes_BAR, testpb.TestAllTypes_FOO, testpb.TestAllTypes_BAZ}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeatedFields(new(testpb.TestAllTypes), "repeated_foreign_enum"),
+		},
+		want: false, // wrong field: repeated_nested_enum != repeated_foreign_enum
+	}, {
+		x:    &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{{}, {C: proto.Int32(3)}, nil, {C: proto.Int32(3)}, {C: proto.Int32(5)}, {C: proto.Int32(4)}}},
+		y:    &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(3)}, {}, {C: proto.Int32(4)}, {C: proto.Int32(3)}, {C: proto.Int32(5)}}},
+		opts: cmp.Options{Transform()},
+		want: false,
+	}, {
+		x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{{}, {C: proto.Int32(3)}, nil, {C: proto.Int32(3)}, {C: proto.Int32(5)}, {C: proto.Int32(4)}}},
+		y: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(3)}, {}, {C: proto.Int32(4)}, {C: proto.Int32(3)}, {C: proto.Int32(5)}}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeatedFields(new(testpb.TestAllTypes), "repeated_foreign_message"),
+		},
+		want: true,
+	}, {
+		x: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{{}, {C: proto.Int32(3)}, nil, {C: proto.Int32(3)}, {C: proto.Int32(5)}, {C: proto.Int32(4)}}},
+		y: &testpb.TestAllTypes{RepeatedForeignMessage: []*testpb.ForeignMessage{nil, {C: proto.Int32(3)}, {}, {C: proto.Int32(4)}, {C: proto.Int32(3)}, {C: proto.Int32(5)}}},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeatedFields(new(testpb.TestAllTypes), "repeated_nested_message"),
+		},
+		want: false, // wrong field: repeated_foreign_message != repeated_nested_message
+	}, {
+		x: &testpb.TestAllTypes{
+			RepeatedBool:           []bool{false, true},
+			RepeatedInt32:          []int32{-32, +32},
+			RepeatedInt64:          []int64{-64, +64},
+			RepeatedUint32:         []uint32{0, 32},
+			RepeatedUint64:         []uint64{0, 64},
+			RepeatedFloat:          []float32{-32.32, +32.32},
+			RepeatedDouble:         []float64{-64.64, +64.64},
+			RepeatedString:         []string{"hello", "world"},
+			RepeatedBytes:          [][]byte{[]byte("hello"), []byte("world")},
+			RepeatedForeignEnum:    []testpb.ForeignEnum{testpb.ForeignEnum_FOREIGN_FOO, testpb.ForeignEnum_FOREIGN_BAR},
+			RepeatedForeignMessage: []*testpb.ForeignMessage{{C: proto.Int32(-1)}, {C: proto.Int32(+1)}},
+		},
+		y: &testpb.TestAllTypes{
+			RepeatedBool:           []bool{true, false},
+			RepeatedInt32:          []int32{+32, -32},
+			RepeatedInt64:          []int64{+64, -64},
+			RepeatedUint32:         []uint32{32, 0},
+			RepeatedUint64:         []uint64{64, 0},
+			RepeatedFloat:          []float32{+32.32, -32.32},
+			RepeatedDouble:         []float64{+64.64, -64.64},
+			RepeatedString:         []string{"world", "hello"},
+			RepeatedBytes:          [][]byte{[]byte("world"), []byte("hello")},
+			RepeatedForeignEnum:    []testpb.ForeignEnum{testpb.ForeignEnum_FOREIGN_BAR, testpb.ForeignEnum_FOREIGN_FOO},
+			RepeatedForeignMessage: []*testpb.ForeignMessage{{C: proto.Int32(+1)}, {C: proto.Int32(-1)}},
+		},
+		opts: cmp.Options{
+			Transform(),
+			SortRepeatedFields(new(testpb.TestAllTypes),
+				"repeated_bool",
+				"repeated_int32",
+				"repeated_int64",
+				"repeated_uint32",
+				"repeated_uint64",
+				"repeated_float",
+				"repeated_double",
+				"repeated_string",
+				"repeated_bytes",
+				"repeated_foreign_enum",
+				"repeated_foreign_message",
+			),
+		},
+		want: true,
+	}}...)
+
 	for _, tt := range tests {
 		t.Run("", func(t *testing.T) {
 			got := cmp.Equal(tt.x, tt.y, tt.opts)
@@ -1066,3 +1326,74 @@
 	}
 	return m
 }
+
+func TestSort(t *testing.T) {
+	t.Run("F32", func(t *testing.T) {
+		want := []float32{
+			float32(math.Float32frombits(0xffc00000)), // -NaN
+			float32(math.Inf(-1)),
+			float32(-math.MaxFloat32),
+			float32(-123.456),
+			float32(-math.SmallestNonzeroFloat32),
+			float32(math.Copysign(0, -1)),
+			float32(math.Copysign(0, +1)),
+			float32(+math.SmallestNonzeroFloat32),
+			float32(+123.456),
+			float32(+math.MaxFloat32),
+			float32(math.Inf(+1)),
+			float32(math.Float32frombits(0x7fc00000)), // +NaN
+		}
+		for i := 0; i < 10; i++ {
+			t.Run("", func(t *testing.T) {
+				got := append([]float32(nil), want...)
+				rn := rand.New(rand.NewSource(int64(i)))
+				for i, j := range rn.Perm(len(got)) {
+					got[i], got[j] = got[j], got[i]
+				}
+				sort.Slice(got, func(i, j int) bool {
+					return lessF32(got[i], got[j])
+				})
+				cmpF32s := cmp.Comparer(func(x, y float32) bool {
+					return math.Float32bits(x) == math.Float32bits(y)
+				})
+				if diff := cmp.Diff(want, got, cmpF32s); diff != "" {
+					t.Errorf("Sort mismatch (-want +got):\n%s", diff)
+				}
+			})
+		}
+	})
+	t.Run("F64", func(t *testing.T) {
+		want := []float64{
+			float64(math.Float64frombits(0xfff8000000000001)), // -NaN
+			float64(math.Inf(-1)),
+			float64(-math.MaxFloat64),
+			float64(-123.456),
+			float64(-math.SmallestNonzeroFloat64),
+			float64(math.Copysign(0, -1)),
+			float64(math.Copysign(0, +1)),
+			float64(+math.SmallestNonzeroFloat64),
+			float64(+123.456),
+			float64(+math.MaxFloat64),
+			float64(math.Inf(+1)),
+			float64(math.Float64frombits(0x7ff8000000000001)), // +NaN
+		}
+		for i := 0; i < 10; i++ {
+			t.Run("", func(t *testing.T) {
+				got := append([]float64(nil), want...)
+				rn := rand.New(rand.NewSource(int64(i)))
+				for i, j := range rn.Perm(len(got)) {
+					got[i], got[j] = got[j], got[i]
+				}
+				sort.Slice(got, func(i, j int) bool {
+					return lessF64(got[i], got[j])
+				})
+				cmpF64s := cmp.Comparer(func(x, y float64) bool {
+					return math.Float64bits(x) == math.Float64bits(y)
+				})
+				if diff := cmp.Diff(want, got, cmpF64s); diff != "" {
+					t.Errorf("Sort mismatch (-want +got):\n%s", diff)
+				}
+			})
+		}
+	})
+}
diff --git a/testing/protocmp/xform.go b/testing/protocmp/xform.go
index 3b2f9f3..8f26aa7 100644
--- a/testing/protocmp/xform.go
+++ b/testing/protocmp/xform.go
@@ -2,7 +2,8 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// Package protocmp provides protobuf specific options for the cmp package.
+// Package protocmp provides protobuf specific options for the
+// "github.com/google/go-cmp/cmp" package.
 //
 // The primary feature is the Transform option, which transform proto.Message
 // types into a Message map that is suitable for cmp to introspect upon.
@@ -24,6 +25,7 @@
 )
 
 var (
+	enumV2Type    = reflect.TypeOf((*protoreflect.Enum)(nil)).Elem()
 	messageV1Type = reflect.TypeOf((*protoiface.MessageV1)(nil)).Elem()
 	messageV2Type = reflect.TypeOf((*proto.Message)(nil)).Elem()
 )