Repository: stretchr/testify
Pull request #1149 (2022-01-17 13:13:20)

assert: allow comparing time.Time
## Summary
Add support for comparing `time>Time` and custom types derived from it.  

## Changes
When comparing types, see if we're comparing `time.Time`, or a custom type based on `time.Time`. If that's the case, compare their Unix timestamps, with the smallest possible resolution. 

## Motivation
This now allows sticking a `time.Time` into `assert.Greater` and `assert.Less`, which is nice. Time stamps have a natural ordering, so I'd expect this to work. 

torkelrogstad: Had to fiddle a bit with build tags to get this working across versions. Nw tests pass locally for me with both a new (1.17.6) and old (1.14.15). Looks like CI is fine as well. 
torkelrogstad: Rebased, and added doc comments. Assumed you meant 1.16, as that's what's holding up the build tag stuff. 
torkelrogstad: Okay, this is a bit awkward... `go fmt` wants me to use a syntax that's not supported by earlier versions of Go, so I don't quite know how to make all Go versions happy here

Patch:
diff --git a/assert/assertion_compare.go b/assert/assertion_compare.go
index 718d582..3bb22a9 100644
--- a/assert/assertion_compare.go
+++ b/assert/assertion_compare.go
@@ -3,6 +3,7 @@ package assert
 import (
 	"fmt"
 	"reflect"
+	"time"
 )
 
 type CompareType int
@@ -30,6 +31,8 @@ var (
 	float64Type = reflect.TypeOf(float64(1))
 
 	stringType = reflect.TypeOf("")
+
+	timeType = reflect.TypeOf(time.Time{})
 )
 
 func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
@@ -299,6 +302,27 @@ func compare(obj1, obj2 interface{}, kind reflect.Kind) (CompareType, bool) {
 				return compareLess, true
 			}
 		}
+	// Check for known struct types we can check for compare results.
+	case reflect.Struct:
+		{
+			// All structs enter here. We're not interested in most types.
+			if !canConvert(obj1Value, timeType) {
+				break
+			}
+
+			// time.Time can compared!
+			timeObj1, ok := obj1.(time.Time)
+			if !ok {
+				timeObj1 = obj1Value.Convert(timeType).Interface().(time.Time)
+			}
+
+			timeObj2, ok := obj2.(time.Time)
+			if !ok {
+				timeObj2 = obj2Value.Convert(timeType).Interface().(time.Time)
+			}
+
+			return compare(timeObj1.UnixNano(), timeObj2.UnixNano(), reflect.Int64)
+		}
 	}
 
 	return compareEqual, false
diff --git a/assert/assertion_compare_can_convert.go b/assert/assertion_compare_can_convert.go
new file mode 100644
index 0000000..df22c47
--- /dev/null
+++ b/assert/assertion_compare_can_convert.go
@@ -0,0 +1,16 @@
+//go:build go1.17
+// +build go1.17
+
+// TODO: once support for Go 1.16 is dropped, this file can be
+//       merged/removed with assertion_compare_go1.17_test.go and
+//       assertion_compare_legacy.go
+
+package assert
+
+import "reflect"
+
+// Wrapper around reflect.Value.CanConvert, for compatability
+// reasons.
+func canConvert(value reflect.Value, to reflect.Type) bool {
+	return value.CanConvert(to)
+}
diff --git a/assert/assertion_compare_legacy.go b/assert/assertion_compare_legacy.go
new file mode 100644
index 0000000..1701af2
--- /dev/null
+++ b/assert/assertion_compare_legacy.go
@@ -0,0 +1,16 @@
+//go:build !go1.17
+// +build !go1.17
+
+// TODO: once support for Go 1.16 is dropped, this file can be
+//       merged/removed with assertion_compare_go1.17_test.go and
+//       assertion_compare_can_convert.go
+
+package assert
+
+import "reflect"
+
+// Older versions of Go does not have the reflect.Value.CanConvert
+// method.
+func canConvert(value reflect.Value, to reflect.Type) bool {
+	return false
+}


Test patch:
diff --git a/assert/assertion_compare_go1.17_test.go b/assert/assertion_compare_go1.17_test.go
new file mode 100644
index 0000000..d3ea935
--- /dev/null
+++ b/assert/assertion_compare_go1.17_test.go
@@ -0,0 +1,54 @@
+//go:build go1.17
+// +build go1.17
+
+// TODO: once support for Go 1.16 is dropped, this file can be
+//       merged/removed with assertion_compare_can_convert.go and
+//       assertion_compare_legacy.go
+
+package assert
+
+import (
+	"reflect"
+	"testing"
+	"time"
+)
+
+func TestCompare17(t *testing.T) {
+	type customTime time.Time
+	for _, currCase := range []struct {
+		less    interface{}
+		greater interface{}
+		cType   string
+	}{
+		{less: time.Now(), greater: time.Now().Add(time.Hour), cType: "time.Time"},
+		{less: customTime(time.Now()), greater: customTime(time.Now().Add(time.Hour)), cType: "time.Time"},
+	} {
+		resLess, isComparable := compare(currCase.less, currCase.greater, reflect.ValueOf(currCase.less).Kind())
+		if !isComparable {
+			t.Error("object should be comparable for type " + currCase.cType)
+		}
+
+		if resLess != compareLess {
+			t.Errorf("object less (%v) should be less than greater (%v) for type "+currCase.cType,
+				currCase.less, currCase.greater)
+		}
+
+		resGreater, isComparable := compare(currCase.greater, currCase.less, reflect.ValueOf(currCase.less).Kind())
+		if !isComparable {
+			t.Error("object are comparable for type " + currCase.cType)
+		}
+
+		if resGreater != compareGreater {
+			t.Errorf("object greater should be greater than less for type " + currCase.cType)
+		}
+
+		resEqual, isComparable := compare(currCase.less, currCase.less, reflect.ValueOf(currCase.less).Kind())
+		if !isComparable {
+			t.Error("object are comparable for type " + currCase.cType)
+		}
+
+		if resEqual != 0 {
+			t.Errorf("objects should be equal for type " + currCase.cType)
+		}
+	}
+}
diff --git a/assert/assertion_compare_test.go b/assert/assertion_compare_test.go
index 667dbf7..a38d880 100644
--- a/assert/assertion_compare_test.go
+++ b/assert/assertion_compare_test.go
@@ -59,7 +59,8 @@ func TestCompare(t *testing.T) {
 		}
 
 		if resLess != compareLess {
-			t.Errorf("object less should be less than greater for type " + currCase.cType)
+			t.Errorf("object less (%v) should be less than greater (%v) for type "+currCase.cType,
+				currCase.less, currCase.greater)
 		}
 
 		resGreater, isComparable := compare(currCase.greater, currCase.less, reflect.ValueOf(currCase.less).Kind())