Repository: willowtreeapps/assertk (kotlin)

Issue:
Better error messages when things are not equal, but the result of `toString()` is the same. 
Consider:

```kotlin
import androidx.compose.ui.text.AnnotatedString
...
        assertThat(AnnotatedString("asdf")).isEqualTo("asdf")
```

This correctly fails, but the error message can be a bit confusing. 

as of `0.25`, the error message is: 
```
AssertionFailedError: expected:<["asdf"]> but was:<[asdf]>
```

A couple of thoughts. 

1. add the quotes for any `CharSequence` not just `String`. so that  `AnnotatedString` would print as `"asdf"` (with quotes)
2. Add type information to the failure message when the `toString()` result is the same, but they are not equal? 
   ```
   AssertionFailedError: expected:<["asdf"]> (type `java.lang.String`) but was:<[asdf]> (type `androidx.compose.ui.text.AnnotatedString`)
   ```

Pull request description:
if the types are different, add them to the error message, otherwise just print out that they don't compare equal

Fixes #449

Patch:
diff --git a/assertk/src/commonMain/kotlin/assertk/assertions/support/support.kt b/assertk/src/commonMain/kotlin/assertk/assertions/support/support.kt
index b2a4a99e..9543b0d5 100644
--- a/assertk/src/commonMain/kotlin/assertk/assertions/support/support.kt
+++ b/assertk/src/commonMain/kotlin/assertk/assertions/support/support.kt
@@ -31,6 +31,7 @@ internal fun display(value: Any?): String {
             prefix = "{",
             postfix = "}",
             transform = { (k, v) -> "${display(k)}=${display(v)}" })
+
         is BooleanArray -> value.joinToString(prefix = "[", postfix = "]", transform = ::display)
         is CharArray -> value.joinToString(prefix = "[", postfix = "]", transform = ::display)
         is IntArray -> value.joinToString(prefix = "[", postfix = "]", transform = ::display)
@@ -54,7 +55,31 @@ fun <T> Assert<T>.fail(expected: Any?, actual: Any?) {
     if (expected == null || actual == null || expected == actual) {
         expected(message = ":${show(expected)} but was:${show(actual)}", expected = expected, actual = actual)
     } else {
-        val extractor = DiffExtractor(display(expected), display(actual))
+        val displayExpected = display(expected)
+        val displayActual = display(actual)
+
+        if (displayExpected == displayActual) {
+            // display() representations are the same, if the types are different, report them
+            val expectedType = expected::class
+            val actualType = actual::class
+
+            if (expectedType != actualType) {
+                expected(
+                    message = ":<$displayActual> with type:${show(expectedType)} but was type:${show(actualType)} with the same string representation",
+                    expected = expected,
+                    actual = actual
+                )
+            } else {
+                // otherwise just note that they appear the same
+                expected(
+                    message = ":<${displayActual}> with type:${show(expectedType)} did not compare equal to the same type with the same string representation",
+                    expected = expected,
+                    actual = actual,
+                )
+            }
+        }
+
+        val extractor = DiffExtractor(displayExpected, displayActual)
         val prefix = extractor.compactPrefix()
         val suffix = extractor.compactSuffix()
         val expectedDiff = extractor.expectedDiff().renderSpecialWhitespace()
@@ -77,9 +102,17 @@ fun <T> Assert<T>.fail(expected: Any?, actual: Any?) {
 fun <T> Assert<T>.expected(message: String, expected: Any? = NONE, actual: Any? = NONE): Nothing {
     val maybeSpace = if (message.startsWith(":")) "" else " "
     val maybeInstance = if (context.originatingSubject != null) " (${context.displayOriginatingSubject()})" else ""
+    fail(
+        message = "expected${formatName(name)}$maybeSpace$message$maybeInstance",
+        expected = expected,
+        actual = actual,
+        cause = extractCause(),
+    )
+}
 
-    // Attempt to extract a helpful Throwable to use as a cause if the current value
-    // or originating subject are a Throwable or failure Result.
+// Attempt to extract a helpful Throwable to use as a cause if the current value
+// or originating subject are a Throwable or failure Result.
+private fun <T> Assert<T>.extractCause(): Throwable? {
     var cause: Throwable? = null
     if (this is ValueAssert<*>) {
         cause = when (value) {
@@ -95,13 +128,7 @@ fun <T> Assert<T>.expected(message: String, expected: Any? = NONE, actual: Any?
             }
         }
     }
-
-    fail(
-        message = "expected${formatName(name)}$maybeSpace$message$maybeInstance",
-        expected = expected,
-        actual = actual,
-        cause = cause,
-    )
+    return cause
 }
 
 /**


Test patch:
diff --git a/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt b/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
index a8dbdad9..29b650af 100644
--- a/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
+++ b/assertk/src/commonTest/kotlin/test/assertk/assertions/AnyTest.kt
@@ -49,6 +49,20 @@ class AnyTest {
         }
     }
 
+    @Test fun isEqualTo_will_report_different_types_with_the_same_display_representation() {
+        val error = assertFailsWith<AssertionError> {
+            assertThat(Actual("test")).isEqualTo(Expected("test"))
+        }
+        assertEquals("expected:<test> with type:<${Expected::class}> but was type:<${Actual::class}> with the same string representation", error.message)
+    }
+
+    @Test fun isEqualTo_will_report_that_same_types_with_the_same_display_representation_do_not_compare_equal() {
+        val error = assertFailsWith<AssertionError> {
+            assertThat(Actual("test")).isEqualTo(Actual("test"))
+        }
+        assertEquals("expected:<test> with type:<${Actual::class}> did not compare equal to the same type with the same string representation", error.message)
+    }
+
     @Test fun isNotEqualTo_non_equal_objects_passes() {
         val nonEqual = BasicObject("not test")
         assertThat(subject).isNotEqualTo(nonEqual)
@@ -491,5 +505,12 @@ class AnyTest {
         }
 
         class DifferentObject : TestObject()
+
+        class Expected(val value: String) {
+            override fun toString() = value
+        }
+        class Actual(val value: String) {
+            override fun toString() = value
+        }
     }
 }