diff --git a/internal/assertions/equal_test.go b/internal/assertions/equal_test.go index 9d16be64c..9765180f4 100644 --- a/internal/assertions/equal_test.go +++ b/internal/assertions/equal_test.go @@ -344,6 +344,109 @@ func TestEqualBytes(t *testing.T) { } } +func TestEqualValuePanics(t *testing.T) { + t.Parallel() + + for tt := range panicCases() { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mock := new(mockT) + NotPanics(t, func() { + Equal(mock, tt.value1, tt.value2) + }, "should not panic") + + if !tt.expectEqual { + True(t, mock.Failed(), "should have failed") + Contains(t, mock.errorString(), "Not equal:", "error message should mention inequality") + + return + } + + False(t, mock.Failed(), "should have been successful") + Empty(t, mock.errorString()) + }) + } +} + +type panicCase struct { + name string + value1 any + value2 any + expectEqual bool +} + +func panicCases() iter.Seq[panicCase] { + type structWithUnexportedMapWithArrayKey struct { + m any + } + + return slices.Values([]panicCase{ + { + // from issue https://github.com/stretchr/testify/pull/1816 + name: "panic behavior on struct with array key and unexported field (some keys vs none)", + value1: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{ + {1}: nil, + {2}: nil, + }, + }, + value2: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{}, + }, + expectEqual: false, + }, + { + name: "panic behavior on struct with array key and unexported field (same keys)", + value1: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{ + {1}: nil, + {2}: nil, + }, + }, + value2: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{ + {2}: nil, + {1}: nil, + }, + }, + expectEqual: true, + }, + { + name: "panic behavior on struct with array key and unexported field (non-nil values)", + value1: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{ + {1}: {}, + {2}: nil, + }, + }, + value2: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{ + {1}: {}, + {2}: nil, + }, + }, + expectEqual: true, + }, + { + name: "panic behavior on struct with array key and unexported field (different, non-nil values)", + value1: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{ + {1}: {}, + {2}: nil, + }, + }, + value2: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{ + {1}: nil, + {2}: {}, + }, + }, + expectEqual: false, + }, + }) +} + type equalCase struct { expected any actual any diff --git a/internal/spew/common.go b/internal/spew/common.go index 758210932..2151e3395 100644 --- a/internal/spew/common.go +++ b/internal/spew/common.go @@ -314,6 +314,12 @@ func valueSortLess(a, b reflect.Value) bool { for i := range l { av := a.Index(i) bv := b.Index(i) + + if !av.CanInterface() || !bv.CanInterface() { + // Unexported fields would panic on Interface() call. + continue + } + if av.Interface() == bv.Interface() { continue } diff --git a/internal/spew/common_impl_test.go b/internal/spew/common_impl_test.go new file mode 100644 index 000000000..d5e33ed7e --- /dev/null +++ b/internal/spew/common_impl_test.go @@ -0,0 +1,55 @@ +package spew + +import ( + "iter" + "reflect" + "slices" + "testing" +) + +func TestPanicUnexportedFields(t *testing.T) { + t.Parallel() + + for tt := range panicCases() { + t.Run(tt.name, func(t *testing.T) { + v1 := reflect.ValueOf(tt.value1) + v2 := reflect.ValueOf(tt.value2) + isLess := valueSortLess(v1, v2) + + if isLess { + t.Error("expected an ordered set") + } + }) + } +} + +type panicCase struct { + name string + value1 any + value2 any +} + +func panicCases() iter.Seq[panicCase] { + type structWithUnexportedMapWithArrayKey struct { + m any + } + + return slices.Values([]panicCase{ + { + // from issue https://github.com/stretchr/testify/pull/1816 + name: "panic behavior on struct with array key and unexported field", + value1: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{ + {1}: nil, + {2}: nil, + }, + }, + value2: structWithUnexportedMapWithArrayKey{ + map[[1]byte]*struct{}{ + {2}: nil, + {1}: nil, + }, + }, + }, + }) +}