From 174268560fd3f0f3fc78a7c9b1ffd21f6231f91a Mon Sep 17 00:00:00 2001 From: Justin Hwang Date: Fri, 4 Sep 2026 13:52:40 -0400 Subject: [PATCH] assert: Empty looks through a pointer to an interface isEmpty was rewritten in #1761 to walk reflect.Value directly instead of re-boxing each dereference through interface{}. That round-trip used to collapse the interface layer, so recursion continued into the concrete pointee. Walking reflect.Value directly, Elem() on a *I yields a value whose Kind is reflect.Interface, which matched no case in the switch and fell through to false. The result, since v1.11.0, is that isEmpty reports false for every non-nil pointer to a non-nil interface, whatever the interface holds: Empty can never pass for a *SomeInterface, and NotEmpty always passes for one. That was an unintended side effect of a change framed as performance and simplification. Recurse on reflect.Interface the same way we recurse on reflect.Ptr, so that Empty(&i) agrees with Empty(i) again as it did in v1.10.0. Elem() on a nil interface returns the zero Value, but IsZero already catches that above, and the IsZero fast path is otherwise untouched. Fixes #1955 --- assert/assertion_format.go | 2 ++ assert/assertion_forward.go | 4 ++++ assert/assertions.go | 7 +++++-- assert/assertions_test.go | 42 +++++++++++++++++++++++++++++++++++++ require/require.go | 4 ++++ require/require_forward.go | 4 ++++ 6 files changed, 61 insertions(+), 2 deletions(-) diff --git a/assert/assertion_format.go b/assert/assertion_format.go index a19a89279..5ba5744aa 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -60,6 +60,8 @@ func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // +// Interface values are "empty" if the interface is nil or if the value it holds is "empty". +// // assert.Emptyf(t, obj, "error message %s", "formatted") // // [Zero values]: https://go.dev/ref/spec#The_zero_value diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index cd2a86061..85d90c4b1 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -102,6 +102,8 @@ func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg st // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // +// Interface values are "empty" if the interface is nil or if the value it holds is "empty". +// // a.Empty(obj) // // [Zero values]: https://go.dev/ref/spec#The_zero_value @@ -122,6 +124,8 @@ func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool { // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // +// Interface values are "empty" if the interface is nil or if the value it holds is "empty". +// // a.Emptyf(obj, "error message %s", "formatted") // // [Zero values]: https://go.dev/ref/spec#The_zero_value diff --git a/assert/assertions.go b/assert/assertions.go index 166f63726..e09c2af1c 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -768,8 +768,9 @@ func isEmptyValue(objValue reflect.Value) bool { // Note: array types are empty when they match their zero-initialized state. case reflect.Chan, reflect.Map, reflect.Slice: return objValue.Len() == 0 - // non-nil pointers are empty if the value they point to is empty - case reflect.Ptr: + // non-nil pointers are empty if the value they point to is empty, + // and likewise for non-nil interfaces and the value they hold + case reflect.Ptr, reflect.Interface: return isEmptyValue(objValue.Elem()) } return false @@ -785,6 +786,8 @@ func isEmptyValue(objValue reflect.Value) bool { // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // +// Interface values are "empty" if the interface is nil or if the value it holds is "empty". +// // assert.Empty(t, obj) // // [Zero values]: https://go.dev/ref/spec#The_zero_value diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 11642e096..4ffe34f5b 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -1766,6 +1766,19 @@ func Test_isEmpty(t *testing.T) { chWithValue := make(chan struct{}, 1) chWithValue <- struct{}{} + // Values held in an interface, so that &x below is a pointer to an interface. + var ( + emptyStringAny interface{} = "" + emptyIntAny interface{} = 0 + emptySliceAny interface{} = []int{} + emptyPtrAny interface{} = new(int) + emptyErr = errors.New("") // error interface holding a non-nil, empty-message error + nonEmptyStringAny interface{} = "something" + nonEmptyIntAny interface{} = 42 + nonEmptySliceAny interface{} = []int{42} + nonEmptyErr = errors.New("something") + ) + True(t, isEmpty("")) True(t, isEmpty(nil)) True(t, isEmpty(error(nil))) @@ -1826,6 +1839,12 @@ func Test_isEmpty(t *testing.T) { True(t, isEmpty([]error(nil))) True(t, isEmpty(&[1]int{0})) True(t, isEmpty(&[2]int{0, 0})) + True(t, isEmpty(new(error))) // ptr to nil interface + True(t, isEmpty(&emptyStringAny)) // ptr to interface holding "" + True(t, isEmpty(&emptyIntAny)) // ptr to interface holding 0 + True(t, isEmpty(&emptySliceAny)) // ptr to interface holding []int{} + True(t, isEmpty(&emptyPtrAny)) // ptr to interface holding a ptr to a zero value + True(t, isEmpty(&emptyErr)) // ptr to error holding errors.New("") - BEWARE False(t, isEmpty("something")) False(t, isEmpty(errors.New("something"))) False(t, isEmpty([]string{"something"})) @@ -1852,6 +1871,10 @@ func Test_isEmpty(t *testing.T) { False(t, isEmpty([]bool{false})) // elements values are ignored for slices False(t, isEmpty([]bool{true})) // elements values are ignored for slices False(t, isEmpty([]error{errors.New("xxx")})) + False(t, isEmpty(&nonEmptyStringAny)) // ptr to interface holding "something" + False(t, isEmpty(&nonEmptyIntAny)) // ptr to interface holding 42 + False(t, isEmpty(&nonEmptySliceAny)) // ptr to interface holding []int{42} + False(t, isEmpty(&nonEmptyErr)) // ptr to error holding errors.New("something") False(t, isEmpty([]error{nil})) // BEWARE False(t, isEmpty([]error{errors.New("")})) // BEWARE False(t, isEmpty(map[string]string{"Hello": "World"})) @@ -1915,6 +1938,13 @@ func TestEmpty(t *testing.T) { x int } + // Pointers to an interface: Empty(&i) must agree with Empty(i). + var emptyIface interface{} = "" + var nonEmptyIface interface{} = "something" + var nilIface interface{} + var emptyStructIface interface{} = &TStruct{} + var nonEmptyStructIface interface{} = &TStruct{x: 1} + True(t, Empty(mockT, ""), "Empty string is empty") True(t, Empty(mockT, nil), "Nil is empty") True(t, Empty(mockT, []string{}), "Empty string array is empty") @@ -1929,6 +1959,9 @@ func TestEmpty(t *testing.T) { True(t, Empty(mockT, TString("")), "empty aliased string is empty") True(t, Empty(mockT, sP), "ptr to nil value is empty") True(t, Empty(mockT, [1]int{}), "array is state") + True(t, Empty(mockT, &nilIface), "ptr to nil interface is empty") + True(t, Empty(mockT, &emptyIface), "ptr to interface holding an empty value is empty") + True(t, Empty(mockT, &emptyStructIface), "ptr to interface holding a ptr to an empty struct is empty") False(t, Empty(mockT, "something"), "Non Empty string is not empty") False(t, Empty(mockT, errors.New("something")), "Non nil object is not empty") @@ -1940,6 +1973,8 @@ func TestEmpty(t *testing.T) { False(t, Empty(mockT, TString("abc")), "non-empty aliased string is empty") False(t, Empty(mockT, xP), "ptr to non-nil value is not empty") False(t, Empty(mockT, [1]int{42}), "array is not state") + False(t, Empty(mockT, &nonEmptyIface), "ptr to interface holding a non-empty value is not empty") + False(t, Empty(mockT, &nonEmptyStructIface), "ptr to interface holding a ptr to a non-empty struct is not empty") // error messages validation tests := []struct { @@ -2085,6 +2120,10 @@ func TestNotEmpty(t *testing.T) { chWithValue := make(chan struct{}, 1) chWithValue <- struct{}{} + var emptyIface interface{} = "" + var nonEmptyIface interface{} = "something" + var nilIface interface{} + False(t, NotEmpty(mockT, ""), "Empty string is empty") False(t, NotEmpty(mockT, nil), "Nil is empty") False(t, NotEmpty(mockT, []string{}), "Empty string array is empty") @@ -2092,6 +2131,8 @@ func TestNotEmpty(t *testing.T) { False(t, NotEmpty(mockT, false), "False value is empty") False(t, NotEmpty(mockT, make(chan struct{})), "Channel without values is empty") False(t, NotEmpty(mockT, [1]int{}), "array is state") + False(t, NotEmpty(mockT, &nilIface), "ptr to nil interface is empty") + False(t, NotEmpty(mockT, &emptyIface), "ptr to interface holding an empty value is empty") True(t, NotEmpty(mockT, "something"), "Non Empty string is not empty") True(t, NotEmpty(mockT, errors.New("something")), "Non nil object is not empty") @@ -2100,6 +2141,7 @@ func TestNotEmpty(t *testing.T) { True(t, NotEmpty(mockT, true), "True value is not empty") True(t, NotEmpty(mockT, chWithValue), "Channel with values is not empty") True(t, NotEmpty(mockT, [1]int{42}), "array is not state") + True(t, NotEmpty(mockT, &nonEmptyIface), "ptr to interface holding a non-empty value is not empty") // error messages validation tests := []struct { diff --git a/require/require.go b/require/require.go index 652871f2e..353260b02 100644 --- a/require/require.go +++ b/require/require.go @@ -127,6 +127,8 @@ func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // +// Interface values are "empty" if the interface is nil or if the value it holds is "empty". +// // require.Empty(t, obj) // // [Zero values]: https://go.dev/ref/spec#The_zero_value @@ -150,6 +152,8 @@ func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // +// Interface values are "empty" if the interface is nil or if the value it holds is "empty". +// // require.Emptyf(t, obj, "error message %s", "formatted") // // [Zero values]: https://go.dev/ref/spec#The_zero_value diff --git a/require/require_forward.go b/require/require_forward.go index edac147ef..b6581db56 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -103,6 +103,8 @@ func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg st // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // +// Interface values are "empty" if the interface is nil or if the value it holds is "empty". +// // a.Empty(obj) // // [Zero values]: https://go.dev/ref/spec#The_zero_value @@ -123,6 +125,8 @@ func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) { // // Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // +// Interface values are "empty" if the interface is nil or if the value it holds is "empty". +// // a.Emptyf(obj, "error message %s", "formatted") // // [Zero values]: https://go.dev/ref/spec#The_zero_value