From 6bc3e047a0510bb7d77794555afab12f4f90d3b0 Mon Sep 17 00:00:00 2001 From: Michael Lee Date: Wed, 6 Aug 2025 23:15:48 -0700 Subject: [PATCH] Extend assert.Same to support pointer-like objects Today, assert.Same and assert.NotSame support comparing only literal pointers. This PR generalizes both functions so they also support comparing maps and channels by type and pointer equality. Why these two types? Because the [Go language reference][0] states: > A map or channel value is a reference to the implementation-specific > data structure of the map or channel. That is, it states maps and channels are de-facto pointers. So, my belief is that it would minimize surprise if we allow Same/NotSame to compare any two values that consist of just a reference to some underlying value, regardless if that underlying value is a user-defined object vs some implementation-specific data structure. By this logic, I decided against modifying Same/NotSame to support comparing things like slices, which are not represented via a single reference. [0]: https://go.dev/ref/spec#Representation_of_values --- assert/assertion_format.go | 6 ++- assert/assertion_forward.go | 12 +++-- assert/assertions.go | 42 ++++++++++------ assert/assertions_test.go | 98 ++++++++++++++++++++++++++++++++++++- require/require.go | 12 +++-- require/require_forward.go | 12 +++-- 6 files changed, 152 insertions(+), 30 deletions(-) diff --git a/assert/assertion_format.go b/assert/assertion_format.go index c592f6ad5..3fd68b5c3 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -702,7 +702,8 @@ func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args .. // // assert.NotSamef(t, ptr1, ptr2, "error message %s", "formatted") // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func NotSamef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { @@ -794,7 +795,8 @@ func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...in // // assert.Samef(t, ptr1, ptr2, "error message %s", "formatted") // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func Samef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index 58db92845..39f159ee5 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -1392,7 +1392,8 @@ func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, arg // // a.NotSame(ptr1, ptr2) // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func (a *Assertions) NotSame(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { @@ -1405,7 +1406,8 @@ func (a *Assertions) NotSame(expected interface{}, actual interface{}, msgAndArg // // a.NotSamef(ptr1, ptr2, "error message %s", "formatted") // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func (a *Assertions) NotSamef(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { @@ -1576,7 +1578,8 @@ func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args . // // a.Same(ptr1, ptr2) // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { @@ -1589,7 +1592,8 @@ func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs . // // a.Samef(ptr1, ptr2, "error message %s", "formatted") // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func (a *Assertions) Samef(expected interface{}, actual interface{}, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { diff --git a/assert/assertions.go b/assert/assertions.go index de8de0cb6..03189791e 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -529,14 +529,15 @@ func validateEqualArgs(expected, actual interface{}) error { // // assert.Same(t, ptr1, ptr2) // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - same, ok := samePointers(expected, actual) + same, ok := sameReferences(expected, actual) if !ok { return Fail(t, "Both arguments must be pointers", msgAndArgs...) } @@ -556,14 +557,15 @@ func Same(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) b // // assert.NotSame(t, ptr1, ptr2) // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func NotSame(t TestingT, expected, actual interface{}, msgAndArgs ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() } - same, ok := samePointers(expected, actual) + same, ok := sameReferences(expected, actual) if !ok { // fails when the arguments are not pointers return !(Fail(t, "Both arguments must be pointers", msgAndArgs...)) @@ -577,23 +579,35 @@ func NotSame(t TestingT, expected, actual interface{}, msgAndArgs ...interface{} return true } -// samePointers checks if two generic interface objects are pointers of the same -// type pointing to the same object. It returns two values: same indicating if -// they are the same type and point to the same object, and ok indicating that -// both inputs are pointers. -func samePointers(first, second interface{}) (same bool, ok bool) { - firstPtr, secondPtr := reflect.ValueOf(first), reflect.ValueOf(second) - if firstPtr.Kind() != reflect.Ptr || secondPtr.Kind() != reflect.Ptr { - return false, false // not both are pointers +// sameReferences checks if two generic interface objects are either pointers, +// maps, or channels that have the same type and point to the same underlying +// value. It returns two values: same indicating if they are the same type and +// point to the same object, and ok indicating that both inputs are pointer-like. +// +// ok will be false for objects such as slices or functions, since these values +// are not represented as a single direct reference: +// https://go.dev/ref/spec#Representation_of_values +func sameReferences(first, second interface{}) (same bool, ok bool) { + firstValue, secondValue := reflect.ValueOf(first), reflect.ValueOf(second) + firstKind, secondKind := firstValue.Kind(), secondValue.Kind() + if firstKind != secondKind { + return false, false + } + var firstPtr, secondPtr uintptr + switch firstKind { + case reflect.Chan, reflect.Map, reflect.Ptr: + firstPtr, secondPtr = firstValue.Pointer(), secondValue.Pointer() + default: + return false, false } firstType, secondType := reflect.TypeOf(first), reflect.TypeOf(second) if firstType != secondType { - return false, true // both are pointers, but of different types + return false, true // both are pointer-like, but of different types } // compare pointer addresses - return first == second, true + return firstPtr == secondPtr, true } // formatUnequalValues takes two values of arbitrary types and returns string diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 76ae12f96..8224ec243 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -650,6 +650,31 @@ func TestSame(t *testing.T) { if !Same(mockT, p, p) { t.Error("Same should return true") } + m1 := map[int]int{} + m2 := map[int]int{} + if Same(mockT, m1, m2) { + t.Error("Same should return false") + } + if !Same(mockT, m1, m1) { + t.Error("Same should return true") + } + c1 := make(chan int) + c2 := make(chan int) + if Same(mockT, c1, c2) { + t.Error("Same should return false") + } + if !Same(mockT, c1, c1) { + t.Error("Same should return true") + } + s1 := []int{} + s2 := []int{} + if Same(mockT, s1, s2) { + t.Error("Same should return false") + } + if Same(mockT, s1, s1) { + // Slices are not pointer-like + t.Error("Same should return false") + } } func TestNotSame(t *testing.T) { @@ -670,12 +695,39 @@ func TestNotSame(t *testing.T) { if NotSame(mockT, p, p) { t.Error("NotSame should return false") } + m1 := map[int]int{} + m2 := map[int]int{} + if !NotSame(mockT, m1, m2) { + t.Error("NotSame should return true; different maps") + } + if NotSame(mockT, m1, m1) { + t.Error("NotSame should return false; same maps") + } + c1 := make(chan int) + c2 := make(chan int) + if !NotSame(mockT, c1, c2) { + t.Error("NotSame should return true; different chans") + } + if NotSame(mockT, c1, c1) { + t.Error("NotSame should return false; same chans") + } + s1 := []int{} + s2 := []int{} + if !NotSame(mockT, s1, s1) { + t.Error("NotSame should return true; slices are not pointer-like") + } + if !NotSame(mockT, s1, s2) { + t.Error("NotSame should return true; slices are not pointer-like") + } } -func Test_samePointers(t *testing.T) { +func Test_sameReferences(t *testing.T) { t.Parallel() p := ptr(2) + m := map[int]int{} + c := make(chan int) + s := []int{} type args struct { first interface{} @@ -717,6 +769,12 @@ func Test_samePointers(t *testing.T) { same: False, ok: False, }, + { + name: "slice disallowed", + args: args{first: s, second: s}, + same: False, + ok: False, + }, { name: "non-pointer vs pointer (1 != ptr(2))", args: args{first: 1, second: p}, @@ -729,10 +787,46 @@ func Test_samePointers(t *testing.T) { same: False, ok: False, }, + { + name: "map1 == map1", + args: args{first: m, second: m}, + same: True, + ok: True, + }, + { + name: "map1 != map2", + args: args{first: m, second: map[int]int{}}, + same: False, + ok: True, + }, + { + name: "map1 != map2 (different types)", + args: args{first: m, second: map[int]string{}}, + same: False, + ok: True, + }, + { + name: "chan1 == chan1", + args: args{first: c, second: c}, + same: True, + ok: True, + }, + { + name: "chan1 != chan2", + args: args{first: c, second: make(chan int)}, + same: False, + ok: True, + }, + { + name: "chan1 != chan2 (different types)", + args: args{first: c, second: make(chan string)}, + same: False, + ok: True, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - same, ok := samePointers(tt.args.first, tt.args.second) + same, ok := sameReferences(tt.args.first, tt.args.second) tt.same(t, same) tt.ok(t, ok) }) diff --git a/require/require.go b/require/require.go index 2d02f9bce..cda3c6f50 100644 --- a/require/require.go +++ b/require/require.go @@ -1759,7 +1759,8 @@ func NotRegexpf(t TestingT, rx interface{}, str interface{}, msg string, args .. // // require.NotSame(t, ptr1, ptr2) // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func NotSame(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1775,7 +1776,8 @@ func NotSame(t TestingT, expected interface{}, actual interface{}, msgAndArgs .. // // require.NotSamef(t, ptr1, ptr2, "error message %s", "formatted") // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func NotSamef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { @@ -1991,7 +1993,8 @@ func Regexpf(t TestingT, rx interface{}, str interface{}, msg string, args ...in // // require.Same(t, ptr1, ptr2) // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func Same(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { @@ -2007,7 +2010,8 @@ func Same(t TestingT, expected interface{}, actual interface{}, msgAndArgs ...in // // require.Samef(t, ptr1, ptr2, "error message %s", "formatted") // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func Samef(t TestingT, expected interface{}, actual interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { diff --git a/require/require_forward.go b/require/require_forward.go index e6f7e9446..339fbf783 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -1393,7 +1393,8 @@ func (a *Assertions) NotRegexpf(rx interface{}, str interface{}, msg string, arg // // a.NotSame(ptr1, ptr2) // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func (a *Assertions) NotSame(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { @@ -1406,7 +1407,8 @@ func (a *Assertions) NotSame(expected interface{}, actual interface{}, msgAndArg // // a.NotSamef(ptr1, ptr2, "error message %s", "formatted") // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func (a *Assertions) NotSamef(expected interface{}, actual interface{}, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { @@ -1577,7 +1579,8 @@ func (a *Assertions) Regexpf(rx interface{}, str interface{}, msg string, args . // // a.Same(ptr1, ptr2) // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { @@ -1590,7 +1593,8 @@ func (a *Assertions) Same(expected interface{}, actual interface{}, msgAndArgs . // // a.Samef(ptr1, ptr2, "error message %s", "formatted") // -// Both arguments must be pointer variables. Pointer variable sameness is +// Both arguments must be pointer variables or something directly coercible +// to a pointer such as a map or channel. Pointer variable sameness is // determined based on the equality of both type and value. func (a *Assertions) Samef(expected interface{}, actual interface{}, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok {