Repository: stretchr/testify (Go)

Issue:
assert: EqualValues does not check for overflows
When asserting two numeric values with EqualValues, it passes if one is the overflowed value of the other.

example: https://go.dev/play/p/KZsFh7FnBVc

```go
func TestEqualValues(t *testing.T) {
	var a int8 = 14
	var b int = 270
	assert.Equal(t, b, a)       // fails because of type mismatch
	assert.EqualValues(t, b, a) // passes
}
```


Discussion:
https://github.com/stretchr/testify/blob/882382d845cd9780bd93c1acc8e1fa2ffe266ca1/assert/assertions.go#L161

Go Allows for Ints to be converted to Int8s, hence the `ConvertibleTo` method returns true. A check would probably needed to be added that the Value of Expected > maxSize of the actual type before doing the DeepEqual call.
> https://github.com/stretchr/testify/blob/882382d845cd9780bd93c1acc8e1fa2ffe266ca1/assert/assertions.go#L161
> 
> Go Allows for Ints to be converted to Int8s, hence the `ConvertibleTo` method returns true. A check would probably needed to be added that the Value of Expected > maxSize of the actual type before doing the DeepEqual call.

How about checking it both ways instead of looking at max size?

assume we have `var a A` and `var b B`. if `A` is convertible to `B` then `B` is also convertible to `A`. So maybe we can just check if: 
`B(a) == b && A(b) == a`
> assume we have `var a A` and `var b B`. if `A` is convertible to `B` then `B` is also convertible to `A`. So maybe we can just check if: `B(a) == b && A(b) == a`

var b can't be assumed convertible in all cases. For example: when `a` is an `int` and `b` is a `string`, the assertion would panic if we tried to introduce `actualValue.Convert(ExpectedType)`

https://go.dev/play/p/BKML-vPu4l_N 
https://go.dev/play/p/eOi-PI8CLhk

I didn't know that conversion existed. Thank you!
I still believe that's the way to go though. If a and b are not convertible to each other, they are not "EqualValues". That's testify/assert's the current approach, because no order is implied on the two parameters, which leads to inconsistencies like this:

https://go.dev/play/p/XaCM39GdJum

So I think we'd be fine with adding a reverse convertibility check to prevent the panic you mentioned.
> So I think we'd be fine with adding a reverse convertibility check

I don't think this will work. `int8.ConvertibleTo(int)` and `int.ConvertibleTo(int8)` both return `true`. I've raised a PR with a simple fix. We just need to ensure that we are converting smaller type to a larger type (`int8` to `int` in this case). In the `EqualValues` function, the order of the arguments should not matter anyway.



Patch:
diff --git a/assert/assertions.go b/assert/assertions.go
index 909daa5e2..b3b309b8d 100644
--- a/assert/assertions.go
+++ b/assert/assertions.go
@@ -165,17 +165,40 @@ func ObjectsAreEqualValues(expected, actual interface{}) bool {
 		return true
 	}
 
-	actualType := reflect.TypeOf(actual)
-	if actualType == nil {
+	expectedValue := reflect.ValueOf(expected)
+	actualValue := reflect.ValueOf(actual)
+	if !expectedValue.IsValid() || !actualValue.IsValid() {
 		return false
 	}
-	expectedValue := reflect.ValueOf(expected)
-	if expectedValue.IsValid() && expectedValue.Type().ConvertibleTo(actualType) {
+
+	expectedType := expectedValue.Type()
+	actualType := actualValue.Type()
+	if !expectedType.ConvertibleTo(actualType) {
+		return false
+	}
+
+	if !isNumericType(expectedType) || !isNumericType(actualType) {
 		// Attempt comparison after type conversion
-		return reflect.DeepEqual(expectedValue.Convert(actualType).Interface(), actual)
+		return reflect.DeepEqual(
+			expectedValue.Convert(actualType).Interface(), actual,
+		)
 	}
 
-	return false
+	// If BOTH values are numeric, there are chances of false positives due
+	// to overflow or underflow. So, we need to make sure to always convert
+	// the smaller type to a larger type before comparing.
+	if expectedType.Size() >= actualType.Size() {
+		return actualValue.Convert(expectedType).Interface() == expected
+	}
+
+	return expectedValue.Convert(actualType).Interface() == actual
+}
+
+// isNumericType returns true if the type is one of:
+// int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64,
+// float32, float64, complex64, complex128
+func isNumericType(t reflect.Type) bool {
+	return t.Kind() >= reflect.Int && t.Kind() <= reflect.Complex128
 }
 
 /* CallerInfo is necessary because the assert functions use the testing object


Test patch:
diff --git a/assert/assertions_test.go b/assert/assertions_test.go
index d65dd0218..a06d6e9e0 100644
--- a/assert/assertions_test.go
+++ b/assert/assertions_test.go
@@ -135,24 +135,42 @@ func TestObjectsAreEqual(t *testing.T) {
 
 		})
 	}
+}
 
-	// Cases where type differ but values are equal
-	if !ObjectsAreEqualValues(uint32(10), int32(10)) {
-		t.Error("ObjectsAreEqualValues should return true")
-	}
-	if ObjectsAreEqualValues(0, nil) {
-		t.Fail()
-	}
-	if ObjectsAreEqualValues(nil, 0) {
-		t.Fail()
-	}
+func TestObjectsAreEqualValues(t *testing.T) {
+	now := time.Now()
 
-	tm := time.Now()
-	tz := tm.In(time.Local)
-	if !ObjectsAreEqualValues(tm, tz) {
-		t.Error("ObjectsAreEqualValues should return true for time.Time objects with different time zones")
+	cases := []struct {
+		expected interface{}
+		actual   interface{}
+		result   bool
+	}{
+		{uint32(10), int32(10), true},
+		{0, nil, false},
+		{nil, 0, false},
+		{now, now.In(time.Local), true}, // should be time zone independent
+		{int(270), int8(14), false},     // should handle overflow/underflow
+		{int8(14), int(270), false},
+		{[]int{270, 270}, []int8{14, 14}, false},
+		{complex128(1e+100 + 1e+100i), complex64(complex(math.Inf(0), math.Inf(0))), false},
+		{complex64(complex(math.Inf(0), math.Inf(0))), complex128(1e+100 + 1e+100i), false},
+		{complex128(1e+100 + 1e+100i), 270, false},
+		{270, complex128(1e+100 + 1e+100i), false},
+		{complex128(1e+100 + 1e+100i), 3.14, false},
+		{3.14, complex128(1e+100 + 1e+100i), false},
+		{complex128(1e+10 + 1e+10i), complex64(1e+10 + 1e+10i), true},
+		{complex64(1e+10 + 1e+10i), complex128(1e+10 + 1e+10i), true},
 	}
 
+	for _, c := range cases {
+		t.Run(fmt.Sprintf("ObjectsAreEqualValues(%#v, %#v)", c.expected, c.actual), func(t *testing.T) {
+			res := ObjectsAreEqualValues(c.expected, c.actual)
+
+			if res != c.result {
+				t.Errorf("ObjectsAreEqualValues(%#v, %#v) should return %#v", c.expected, c.actual, c.result)
+			}
+		})
+	}
 }
 
 type Nested struct {