From 77158ae300f6cf0f1785a0f2dca4b2a51cab009d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Mengu=C3=A9?= Date: Wed, 28 May 2025 16:08:21 +0200 Subject: [PATCH] assert.Empty: comprehensive doc of "Empty"-ness rules Document the assert.Empty rules more comprehensively. This exposes our quirks to the user to avoid wrong expectations. Add many many many more test cases that document edges cases and will allow to catch breaking changes and avoid regressions. --- assert/assertion_format.go | 16 ++++-- assert/assertion_forward.go | 32 +++++++++--- assert/assertions.go | 16 ++++-- assert/assertions_test.go | 97 ++++++++++++++++++++++++++++++++++++- require/require.go | 32 +++++++++--- require/require_forward.go | 32 +++++++++--- 6 files changed, 192 insertions(+), 33 deletions(-) diff --git a/assert/assertion_format.go b/assert/assertion_format.go index 714404125..c592f6ad5 100644 --- a/assert/assertion_format.go +++ b/assert/assertion_format.go @@ -50,10 +50,19 @@ func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string return ElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...) } -// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either -// a slice or a channel with len == 0. +// Emptyf asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // // assert.Emptyf(t, obj, "error message %s", "formatted") +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) bool { if h, ok := t.(tHelper); ok { h.Helper() @@ -595,8 +604,7 @@ func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg str return NotElementsMatch(t, listA, listB, append([]interface{}{msg}, args...)...) } -// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either -// a slice or a channel with len == 0. +// NotEmptyf asserts that the specified object is NOT [Empty]. // // if assert.NotEmptyf(t, obj, "error message %s", "formatted") { // assert.Equal(t, "two", obj[1]) diff --git a/assert/assertion_forward.go b/assert/assertion_forward.go index 38e253ebb..58db92845 100644 --- a/assert/assertion_forward.go +++ b/assert/assertion_forward.go @@ -92,10 +92,19 @@ func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg st return ElementsMatchf(a.t, listA, listB, msg, args...) } -// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either -// a slice or a channel with len == 0. +// Empty asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // // a.Empty(obj) +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -103,10 +112,19 @@ func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) bool { return Empty(a.t, object, msgAndArgs...) } -// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either -// a slice or a channel with len == 0. +// Emptyf asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // // a.Emptyf(obj, "error message %s", "formatted") +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) bool { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1182,8 +1200,7 @@ func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg return NotElementsMatchf(a.t, listA, listB, msg, args...) } -// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either -// a slice or a channel with len == 0. +// NotEmpty asserts that the specified object is NOT [Empty]. // // if a.NotEmpty(obj) { // assert.Equal(t, "two", obj[1]) @@ -1195,8 +1212,7 @@ func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) boo return NotEmpty(a.t, object, msgAndArgs...) } -// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either -// a slice or a channel with len == 0. +// NotEmptyf asserts that the specified object is NOT [Empty]. // // if a.NotEmptyf(obj, "error message %s", "formatted") { // assert.Equal(t, "two", obj[1]) diff --git a/assert/assertions.go b/assert/assertions.go index 9913ac2f0..46582060d 100644 --- a/assert/assertions.go +++ b/assert/assertions.go @@ -773,10 +773,19 @@ func isEmpty(object interface{}) bool { } } -// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either -// a slice or a channel with len == 0. +// Empty asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // // assert.Empty(t, obj) +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { pass := isEmpty(object) if !pass { @@ -789,8 +798,7 @@ func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) bool { return pass } -// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either -// a slice or a channel with len == 0. +// NotEmpty asserts that the specified object is NOT [Empty]. // // if assert.NotEmpty(t, obj) { // assert.Equal(t, "two", obj[1]) diff --git a/assert/assertions_test.go b/assert/assertions_test.go index 955188d5a..d30eef60d 100644 --- a/assert/assertions_test.go +++ b/assert/assertions_test.go @@ -1728,24 +1728,119 @@ func Test_isEmpty(t *testing.T) { True(t, isEmpty("")) True(t, isEmpty(nil)) + True(t, isEmpty(error(nil))) + True(t, isEmpty((*int)(nil))) + True(t, isEmpty((*string)(nil))) + True(t, isEmpty(new(string))) True(t, isEmpty([]string{})) + True(t, isEmpty([]string(nil))) + True(t, isEmpty([]byte(nil))) + True(t, isEmpty([]byte{})) + True(t, isEmpty([]byte(""))) + True(t, isEmpty([]bool(nil))) + True(t, isEmpty([]bool{})) + True(t, isEmpty([]interface{}(nil))) + True(t, isEmpty([]interface{}{})) + True(t, isEmpty(struct{}{})) + True(t, isEmpty(&struct{}{})) + True(t, isEmpty(struct{ A int }{A: 0})) + True(t, isEmpty(struct{ a int }{a: 0})) + True(t, isEmpty(struct { + a int + B int + }{a: 0, B: 0})) True(t, isEmpty(0)) + True(t, isEmpty(int(0))) + True(t, isEmpty(int8(0))) + True(t, isEmpty(int16(0))) + True(t, isEmpty(uint16(0))) True(t, isEmpty(int32(0))) + True(t, isEmpty(uint32(0))) True(t, isEmpty(int64(0))) + True(t, isEmpty(uint64(0))) + True(t, isEmpty('\u0000')) // rune => int32 + True(t, isEmpty(float32(0))) + True(t, isEmpty(float64(0))) + True(t, isEmpty(0i)) // complex + True(t, isEmpty(0.0i)) // complex True(t, isEmpty(false)) + True(t, isEmpty(new(bool))) True(t, isEmpty(map[string]string{})) + True(t, isEmpty(map[string]string(nil))) True(t, isEmpty(new(time.Time))) True(t, isEmpty(time.Time{})) True(t, isEmpty(make(chan struct{}))) - True(t, isEmpty([1]int{})) + True(t, isEmpty(chan struct{}(nil))) + True(t, isEmpty(chan<- struct{}(nil))) + True(t, isEmpty(make(chan struct{}))) + True(t, isEmpty(make(chan<- struct{}))) + True(t, isEmpty(make(chan struct{}, 1))) + True(t, isEmpty(make(chan<- struct{}, 1))) + True(t, isEmpty([1]int{0})) + True(t, isEmpty([2]int{0, 0})) + True(t, isEmpty([8]int{})) + True(t, isEmpty([...]int{7: 0})) + True(t, isEmpty([...]bool{false, false})) + True(t, isEmpty(errors.New(""))) // BEWARE + True(t, isEmpty([]error{})) + True(t, isEmpty([]error(nil))) + True(t, isEmpty(&[1]int{0})) + True(t, isEmpty(&[2]int{0, 0})) False(t, isEmpty("something")) False(t, isEmpty(errors.New("something"))) False(t, isEmpty([]string{"something"})) False(t, isEmpty(1)) + False(t, isEmpty(int(1))) + False(t, isEmpty(uint(1))) + False(t, isEmpty(byte(1))) + False(t, isEmpty(int8(1))) + False(t, isEmpty(uint8(1))) + False(t, isEmpty(int16(1))) + False(t, isEmpty(uint16(1))) + False(t, isEmpty(int32(1))) + False(t, isEmpty(uint32(1))) + False(t, isEmpty(int64(1))) + False(t, isEmpty(uint64(1))) + False(t, isEmpty('A')) // rune => int32 False(t, isEmpty(true)) + False(t, isEmpty(1.0)) + False(t, isEmpty(1i)) // complex + False(t, isEmpty([]byte{0})) // elements values are ignored for slices + False(t, isEmpty([]byte{0, 0})) // elements values are ignored for slices + False(t, isEmpty([]string{""})) // elements values are ignored for slices + False(t, isEmpty([]string{"a"})) // elements values are ignored for slices + 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([]error{nil})) // BEWARE + False(t, isEmpty([]error{errors.New("")})) // BEWARE False(t, isEmpty(map[string]string{"Hello": "World"})) + False(t, isEmpty(map[string]string{"": ""})) + False(t, isEmpty(map[string]string{"foo": ""})) + False(t, isEmpty(map[string]string{"": "foo"})) False(t, isEmpty(chWithValue)) + False(t, isEmpty([1]bool{true})) + False(t, isEmpty([2]bool{false, true})) + False(t, isEmpty([...]bool{10: true})) + False(t, isEmpty([]int{0})) + False(t, isEmpty([]int{42})) False(t, isEmpty([1]int{42})) + False(t, isEmpty([2]int{0, 42})) + False(t, isEmpty(&[1]int{42})) + False(t, isEmpty(&[2]int{0, 42})) + False(t, isEmpty([1]*int{new(int)})) // array elements must be the zero value, not any Empty value + False(t, isEmpty(struct{ A int }{A: 42})) + False(t, isEmpty(struct{ a int }{a: 42})) + False(t, isEmpty(struct{ a *int }{a: new(int)})) // fields must be the zero value, not any Empty value + False(t, isEmpty(struct{ a []int }{a: []int{}})) // fields must be the zero value, not any Empty value + False(t, isEmpty(struct { + a int + B int + }{a: 0, B: 42})) + False(t, isEmpty(struct { + a int + B int + }{a: 42, B: 0})) } func TestEmpty(t *testing.T) { diff --git a/require/require.go b/require/require.go index 6cd133417..2d02f9bce 100644 --- a/require/require.go +++ b/require/require.go @@ -117,10 +117,19 @@ func ElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg string t.FailNow() } -// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either -// a slice or a channel with len == 0. +// Empty asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // // require.Empty(t, obj) +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -131,10 +140,19 @@ func Empty(t TestingT, object interface{}, msgAndArgs ...interface{}) { t.FailNow() } -// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either -// a slice or a channel with len == 0. +// Emptyf asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // // require.Emptyf(t, obj, "error message %s", "formatted") +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value func Emptyf(t TestingT, object interface{}, msg string, args ...interface{}) { if h, ok := t.(tHelper); ok { h.Helper() @@ -1495,8 +1513,7 @@ func NotElementsMatchf(t TestingT, listA interface{}, listB interface{}, msg str t.FailNow() } -// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either -// a slice or a channel with len == 0. +// NotEmpty asserts that the specified object is NOT [Empty]. // // if require.NotEmpty(t, obj) { // require.Equal(t, "two", obj[1]) @@ -1511,8 +1528,7 @@ func NotEmpty(t TestingT, object interface{}, msgAndArgs ...interface{}) { t.FailNow() } -// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either -// a slice or a channel with len == 0. +// NotEmptyf asserts that the specified object is NOT [Empty]. // // if require.NotEmptyf(t, obj, "error message %s", "formatted") { // require.Equal(t, "two", obj[1]) diff --git a/require/require_forward.go b/require/require_forward.go index f6192dfea..e6f7e9446 100644 --- a/require/require_forward.go +++ b/require/require_forward.go @@ -93,10 +93,19 @@ func (a *Assertions) ElementsMatchf(listA interface{}, listB interface{}, msg st ElementsMatchf(a.t, listA, listB, msg, args...) } -// Empty asserts that the specified object is empty. I.e. nil, "", false, 0 or either -// a slice or a channel with len == 0. +// Empty asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // // a.Empty(obj) +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -104,10 +113,19 @@ func (a *Assertions) Empty(object interface{}, msgAndArgs ...interface{}) { Empty(a.t, object, msgAndArgs...) } -// Emptyf asserts that the specified object is empty. I.e. nil, "", false, 0 or either -// a slice or a channel with len == 0. +// Emptyf asserts that the given value is "empty". +// +// [Zero values] are "empty". +// +// Arrays are "empty" if every element is the zero value of the type (stricter than "empty"). +// +// Slices, maps and channels with zero length are "empty". +// +// Pointer values are "empty" if the pointer is nil or if the pointed value is "empty". // // a.Emptyf(obj, "error message %s", "formatted") +// +// [Zero values]: https://go.dev/ref/spec#The_zero_value func (a *Assertions) Emptyf(object interface{}, msg string, args ...interface{}) { if h, ok := a.t.(tHelper); ok { h.Helper() @@ -1183,8 +1201,7 @@ func (a *Assertions) NotElementsMatchf(listA interface{}, listB interface{}, msg NotElementsMatchf(a.t, listA, listB, msg, args...) } -// NotEmpty asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either -// a slice or a channel with len == 0. +// NotEmpty asserts that the specified object is NOT [Empty]. // // if a.NotEmpty(obj) { // assert.Equal(t, "two", obj[1]) @@ -1196,8 +1213,7 @@ func (a *Assertions) NotEmpty(object interface{}, msgAndArgs ...interface{}) { NotEmpty(a.t, object, msgAndArgs...) } -// NotEmptyf asserts that the specified object is NOT empty. I.e. not nil, "", false, 0 or either -// a slice or a channel with len == 0. +// NotEmptyf asserts that the specified object is NOT [Empty]. // // if a.NotEmptyf(obj, "error message %s", "formatted") { // assert.Equal(t, "two", obj[1])