Merge changes from topic "assertWithMessage" into androidx-main am: 8ab545643c
Original change: https://android-review.googlesource.com/c/platform/frameworks/support/+/2557536
Change-Id: I0846335c6a68d5a72a8f2e9b024c074725529503
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/room/integration-tests/kotlintestapp/build.gradle b/room/integration-tests/kotlintestapp/build.gradle
index 8241224..1e0b08c 100644
--- a/room/integration-tests/kotlintestapp/build.gradle
+++ b/room/integration-tests/kotlintestapp/build.gradle
@@ -103,7 +103,6 @@
exclude group: "com.android.support", module: "support-annotations"
exclude module: "hamcrest-core"
})
- androidTestImplementation(libs.truth) // Kruth still lacks assertWithMessage
androidTestImplementation(project(":internal-testutils-kmp"))
androidTestImplementation(libs.kotlinTest)
androidTestImplementation(project(":room:room-guava"))
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/ProvidedTypeConverterTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/ProvidedTypeConverterTest.kt
index 6698c89..df85ab8 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/ProvidedTypeConverterTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/ProvidedTypeConverterTest.kt
@@ -16,6 +16,8 @@
package androidx.room.integration.kotlintestapp.test
import android.content.Context
+import androidx.kruth.assertThat
+import androidx.kruth.assertWithMessage
import androidx.room.Database
import androidx.room.Entity
import androidx.room.Insert
@@ -38,12 +40,10 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
-import com.google.common.truth.Truth.assertThat
import java.nio.ByteBuffer
import java.util.Date
import java.util.Objects
import java.util.UUID
-import org.junit.Assert
import org.junit.Test
import org.junit.runner.RunWith
@@ -75,9 +75,9 @@
val pet: Pet = TestUtil.createPet(3)
pet.mName = "pet"
db.petDao().insertOrReplace(pet)
- Assert.fail("Show have thrown an IllegalArgumentException")
+ assertWithMessage("Show have thrown an IllegalArgumentException").fail()
} catch (throwable: Throwable) {
- assertThat(throwable).isInstanceOf(IllegalArgumentException::class.java)
+ assertThat(throwable).isInstanceOf<IllegalArgumentException>()
}
}
@@ -91,9 +91,9 @@
val pet: Pet = TestUtil.createPet(3)
pet.mName = "pet"
db.petDao().insertOrReplace(pet)
- Assert.fail("Show have thrown an IllegalArgumentException")
+ assertWithMessage("Show have thrown an IllegalArgumentException").fail()
} catch (throwable: Throwable) {
- assertThat(throwable).isInstanceOf(IllegalArgumentException::class.java)
+ assertThat(throwable).isInstanceOf<IllegalArgumentException>()
}
}
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/Rx2PagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/Rx2PagingSourceTest.kt
index 77f2bb0..ee0a09f 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/Rx2PagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/Rx2PagingSourceTest.kt
@@ -17,6 +17,7 @@
package androidx.room.integration.kotlintestapp.test
import androidx.kruth.assertThat
+import androidx.kruth.assertWithMessage
import androidx.paging.Pager
import androidx.paging.PagingState
import androidx.paging.rxjava2.RxPagingSource
@@ -28,7 +29,6 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertWithMessage
import io.reactivex.Single
import kotlin.test.assertFalse
import kotlin.test.assertTrue
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/Rx3PagingSourceTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/Rx3PagingSourceTest.kt
index cef0319..ecd722a 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/Rx3PagingSourceTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/Rx3PagingSourceTest.kt
@@ -17,6 +17,7 @@
package androidx.room.integration.kotlintestapp.test
import androidx.kruth.assertThat
+import androidx.kruth.assertWithMessage
import androidx.paging.Pager
import androidx.paging.PagingState
import androidx.paging.rxjava3.RxPagingSource
@@ -28,7 +29,6 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertWithMessage
import io.reactivex.rxjava3.core.Single
import kotlin.test.assertFalse
import kotlin.test.assertTrue
diff --git a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
index eb8cd5a..a80338c 100644
--- a/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTest/java/androidx/room/integration/kotlintestapp/test/SuspendingQueryTest.kt
@@ -23,6 +23,7 @@
import androidx.arch.core.executor.ArchTaskExecutor
import androidx.arch.core.executor.TaskExecutor
import androidx.kruth.assertThat
+import androidx.kruth.assertWithMessage
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.integration.kotlintestapp.NewThreadDispatcher
@@ -37,7 +38,6 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth.assertWithMessage
import java.io.IOException
import java.util.concurrent.CountDownLatch
import java.util.concurrent.ExecutorService
diff --git a/room/integration-tests/kotlintestapp/src/androidTestWithKspGenJava/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt b/room/integration-tests/kotlintestapp/src/androidTestWithKspGenJava/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt
index 2c551596..04ff446 100644
--- a/room/integration-tests/kotlintestapp/src/androidTestWithKspGenJava/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt
+++ b/room/integration-tests/kotlintestapp/src/androidTestWithKspGenJava/java/androidx/room/integration/kotlintestapp/NullabilityAwareTypeConversionTest.kt
@@ -18,6 +18,7 @@
import android.database.Cursor
import androidx.kruth.assertThat
+import androidx.kruth.assertWithMessage
import androidx.room.Dao
import androidx.room.Database
import androidx.room.Entity
@@ -33,7 +34,6 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.google.common.truth.Truth.assertWithMessage
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/BooleanSubject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/BooleanSubject.kt
index b083763..7352c54 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/BooleanSubject.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/BooleanSubject.kt
@@ -16,18 +16,19 @@
package androidx.kruth
-import kotlin.test.assertTrue
-
/**
* Propositions for boolean subjects.
*/
-class BooleanSubject(actual: Boolean?) : Subject<Boolean>(actual) {
+class BooleanSubject internal constructor(
+ actual: Boolean?,
+ metadata: FailureMetadata = FailureMetadata(),
+) : Subject<Boolean>(actual = actual, metadata = metadata) {
/**
* Fails if the subject is false or `null`.
*/
fun isFalse() {
- assertTrue(
+ asserter.assertTrue(
actual == false,
"expected to be false, but was $actual"
)
@@ -37,7 +38,7 @@
* Fails if the subject is true or `null`.
*/
fun isTrue() {
- assertTrue(
+ asserter.assertTrue(
actual == true,
"expected to be true, but was $actual"
)
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/ComparableSubject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/ComparableSubject.kt
index 9d1ac4b..da5d8f3 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/ComparableSubject.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/ComparableSubject.kt
@@ -16,14 +16,15 @@
package androidx.kruth
-import kotlin.test.fail
-
/**
* Propositions for [Comparable] typed subjects.
*
* @param T the type of the object being tested by this [ComparableSubject]
*/
-open class ComparableSubject<T : Comparable<T>> constructor(actual: T?) : Subject<T>(actual) {
+open class ComparableSubject<T : Comparable<T>> internal constructor(
+ actual: T?,
+ metadata: FailureMetadata = FailureMetadata(),
+) : Subject<T>(actual = actual, metadata = metadata) {
/**
* Checks that the subject is less than [other].
@@ -31,10 +32,11 @@
* @throws NullPointerException if [actual] or [other] is `null`.
*/
fun isLessThan(other: T?) {
- if (actual == null || other == null) {
- throw NullPointerException("Expected to be less than $other, but was $actual")
- } else if (actual >= other) {
- fail("Expected to be less than $other, but was $actual")
+ requireNonNull(actual) { "Expected to be less than $other, but was $actual" }
+ requireNonNull(other) { "Expected to be less than $other, but was $actual" }
+
+ if (actual >= other) {
+ asserter.fail("Expected to be less than $other, but was $actual")
}
}
@@ -47,7 +49,7 @@
requireNonNull(actual) { "Expected to be at least $other, but was $actual" }
requireNonNull(other) { "Expected to be at least $other, but was $actual" }
if (actual < other) {
- fail("Expected to be at least $other, but was $actual")
+ asserter.fail("Expected to be at least $other, but was $actual")
}
}
}
\ No newline at end of file
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailingOrdered.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailingOrdered.kt
index 559e0e6..1c5fafb 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailingOrdered.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailingOrdered.kt
@@ -16,16 +16,15 @@
package androidx.kruth
-import kotlin.test.fail
-
/**
* Always fails with the provided error message.
*/
internal class FailingOrdered(
+ private val asserter: KruthAsserter,
private val message: () -> String,
) : Ordered {
override fun inOrder() {
- fail(message())
+ asserter.fail(message())
}
}
\ No newline at end of file
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailureMetadata.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailureMetadata.kt
new file mode 100644
index 0000000..e966b77
--- /dev/null
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/FailureMetadata.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.kruth
+
+internal data class FailureMetadata(
+ val messagesToPrepend: List<String> = emptyList(),
+) {
+
+ fun withMessage(messageToPrepend: String): FailureMetadata =
+ copy(messagesToPrepend = messagesToPrepend + messageToPrepend)
+
+ fun formatMessage(message: String? = null): String {
+ val messages = if (message == null) messagesToPrepend else messagesToPrepend + message
+
+ return when {
+ messages.isEmpty() -> message ?: ""
+ messages.size == 1 -> messages.single()
+ else -> messages.joinToString(separator = ". ", postfix = ".")
+ }
+ }
+}
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/IterableSubject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/IterableSubject.kt
index 3a3c4f6..53662ec 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/IterableSubject.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/IterableSubject.kt
@@ -16,9 +16,6 @@
package androidx.kruth
-import kotlin.test.assertEquals
-import kotlin.test.fail
-
/**
* Propositions for [Iterable] subjects.
*
@@ -31,7 +28,10 @@
* - Assertions may also require that the elements in the given [Iterable] implement
* [Any.hashCode] correctly.
*/
-open class IterableSubject<T>(actual: Iterable<T>?) : Subject<Iterable<T>>(actual) {
+open class IterableSubject<T> internal constructor(
+ actual: Iterable<T>?,
+ metadata: FailureMetadata = FailureMetadata(),
+) : Subject<Iterable<T>>(actual = actual, metadata = metadata) {
override fun isEqualTo(expected: Any?) {
// method contract requires testing iterables for equality
@@ -57,7 +57,7 @@
requireNonNull(actual) { "Expected to be empty, but was null" }
if (!actual.isEmpty()) {
- fail("Expected to be empty")
+ asserter.fail("Expected to be empty")
}
}
@@ -66,7 +66,7 @@
requireNonNull(actual) { "Expected not to be empty, but was null" }
if (actual.isEmpty()) {
- fail("Expected to be not empty")
+ asserter.fail("Expected to be not empty")
}
}
@@ -75,7 +75,7 @@
require(expectedSize >= 0) { "expectedSize must be >= 0, but was $expectedSize" }
requireNonNull(actual) { "Expected to have size $expectedSize, but was null" }
- assertEquals(expectedSize, actual.count())
+ asserter.assertEquals(expectedSize, actual.count())
}
/** Checks (with a side-effect failure) that the subject contains the supplied item. */
@@ -85,12 +85,12 @@
if (element !in actual) {
val matchingItems = actual.retainMatchingToString(listOf(element))
if (matchingItems.isNotEmpty()) {
- fail(
+ asserter.fail(
"Expected to contain $element, but did not. " +
"Though it did contain $matchingItems"
)
} else {
- fail("Expected to contain $element, but did not")
+ asserter.fail("Expected to contain $element, but did not")
}
}
}
@@ -100,7 +100,7 @@
requireNonNull(actual) { "Expected not to contain $element, but was null" }
if (element in actual) {
- fail("Expected not to contain $element")
+ asserter.fail("Expected not to contain $element")
}
}
@@ -111,7 +111,7 @@
val duplicates = actual.groupBy { it }.values.filter { it.size > 1 }
if (duplicates.isNotEmpty()) {
- fail("Expected not to contain duplicates, but contained $duplicates")
+ asserter.fail("Expected not to contain duplicates, but contained $duplicates")
}
}
@@ -134,12 +134,12 @@
val matchingItems = actual.retainMatchingToString(expected)
if (matchingItems.isNotEmpty()) {
- fail(
+ asserter.fail(
"Expected to contain any of $expected, but did not. " +
"Though it did contain $matchingItems"
)
} else {
- fail("Expected to contain any of $expected, but did not")
+ asserter.fail("Expected to contain any of $expected, but did not")
}
}
@@ -192,7 +192,7 @@
if (missing.isNotEmpty()) {
val nearMissing = actualList.retainMatchingToString(missing)
- fail(
+ asserter.fail(
"""
Expected to contain at least $expected, but did not.
Missing $missing, though it did contain $nearMissing.
@@ -204,7 +204,7 @@
return NoopOrdered
}
- return FailingOrdered {
+ return FailingOrdered(asserter) {
buildString {
append("Required elements were all found, but order was wrong.")
append("Expected order: $expected.")
@@ -295,7 +295,9 @@
* values had multiple elements. Granted, Fuzzy Truth already does this, so maybe it's OK?
* But Fuzzy Truth doesn't (yet) make the mismatched value so prominent.
*/
- fail("Expected $actualElement to be equal to $requiredElement, but was not")
+ asserter.fail(
+ "Expected $actualElement to be equal to $requiredElement, but was not"
+ )
}
// Missing elements; elements that are not missing will be removed as we iterate.
@@ -328,7 +330,7 @@
* so return an object that will fail the test if the user calls inOrder().
*/
- return FailingOrdered {
+ return FailingOrdered(asserter) {
"""
Contents match. Expected the order to also match, but was not.
Expected: $required.
@@ -337,7 +339,7 @@
}
}
- fail(
+ asserter.fail(
"""
Contents do not match.
Expected: $required.
@@ -356,7 +358,7 @@
// extras. If the required iterator has elements, they're missing elements.
if (actualIter.hasNext()) {
- fail(
+ asserter.fail(
"""
Contents do not match.
Expected: $required.
@@ -367,7 +369,7 @@
}
if (requiredIter.hasNext()) {
- fail(
+ asserter.fail(
"""
Contents do not match.
Expected: $required.
@@ -419,7 +421,7 @@
val present = excluded.intersect(actual)
if (present.isNotEmpty()) {
- fail(
+ asserter.fail(
"""
Expected not to contain any of $excluded but contained $present.
Actual: $actual.
@@ -515,7 +517,7 @@
.zipWithNext(::Pair)
.forEach { (a, b) ->
if (!predicate(a, b)) {
- fail(message(a, b))
+ asserter.fail(message(a, b))
}
}
}
@@ -542,7 +544,7 @@
val nonIterables = iterable.filterNot { it is Iterable<*> }
if (nonIterables.isNotEmpty()) {
- fail(
+ asserter.fail(
"The actual value is an Iterable, and you've written a test that compares it to " +
"some objects that are not Iterables. Did you instead mean to check " +
"whether its *contents* match any of the *contents* of the given values? " +
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Kruth.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Kruth.kt
index 89114369..d665351 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Kruth.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Kruth.kt
@@ -45,3 +45,11 @@
fun <K, V> assertThat(actual: Map<K, V>?): MapSubject<K, V> =
MapSubject(actual)
+
+/**
+ * Begins an assertion that, if it fails, will prepend the given message to the failure message.
+ */
+fun assertWithMessage(messageToPrepend: String): StandardSubjectBuilder =
+ StandardSubjectBuilder(
+ metadata = FailureMetadata(messagesToPrepend = listOf(messageToPrepend)),
+ )
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/KruthAsserter.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/KruthAsserter.kt
new file mode 100644
index 0000000..9964628
--- /dev/null
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/KruthAsserter.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.kruth
+
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.contract
+
+@OptIn(ExperimentalContracts::class)
+internal class KruthAsserter(
+ private val formatMessage: (String?) -> String?,
+) {
+
+ /**
+ * Fails the current test with the specified message and cause exception.
+ *
+ * @param message the message to report.
+ * @param cause the exception to set as the root cause of the reported failure.
+ */
+ fun fail(message: String? = null, cause: Throwable? = null): Nothing {
+ kotlin.test.fail(message = formatMessage(message), cause = cause)
+ }
+
+ /**
+ * Asserts that the specified value is `true`.
+ *
+ * @param message the message to report if the assertion fails.
+ */
+ fun assertTrue(actual: Boolean, message: String? = null) {
+ contract { returns() implies actual }
+ kotlin.test.assertTrue(actual = actual, message = formatMessage(message))
+ }
+
+ /**
+ * Asserts that the specified value is `false`.
+ *
+ * @param message the message to report if the assertion fails.
+ */
+ fun assertFalse(actual: Boolean, message: String? = null) {
+ contract { returns() implies !actual }
+ kotlin.test.assertFalse(actual = actual, message = formatMessage(message))
+ }
+
+ /**
+ * Asserts that the specified values are equal.
+ *
+ * @param message the message to report if the assertion fails.
+ */
+ fun assertEquals(expected: Any?, actual: Any?, message: String? = null) {
+ kotlin.test.assertEquals(
+ expected = expected,
+ actual = actual,
+ message = formatMessage(message),
+ )
+ }
+
+ /**
+ * Asserts that the specified values are not equal.
+ *
+ * @param message the message to report if the assertion fails.
+ */
+ fun assertNotEquals(illegal: Any?, actual: Any?, message: String? = null) {
+ kotlin.test.assertNotEquals(
+ illegal = illegal,
+ actual = actual,
+ message = formatMessage(message),
+ )
+ }
+
+ /**
+ * Asserts that the specified value is `null`.
+ *
+ * @param message the message to report if the assertion fails.
+ */
+ fun assertNull(actual: Any?, message: String? = null) {
+ contract { returns() implies (actual == null) }
+ kotlin.test.assertNull(actual = actual, message = formatMessage(message))
+ }
+
+ /**
+ * Asserts that the specified value is not `null`.
+ *
+ * @param message the message to report if the assertion fails.
+ */
+ fun <T : Any> assertNotNull(actual: T?, message: String? = null): T {
+ contract { returns() implies (actual != null) }
+ kotlin.test.assertNotNull(actual = actual, message = formatMessage(message))
+
+ return requireNonNull(actual)
+ }
+}
\ No newline at end of file
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/KruthExt.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/KruthExt.kt
index e44dc61..4e59342 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/KruthExt.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/KruthExt.kt
@@ -29,11 +29,11 @@
// See: https://github.com/google/truth/issues/621
inline fun <reified T : Throwable> assertThrows(block: () -> Unit): ThrowableSubject<T> {
val e = assertFailsWith<T>(block = block)
- return ThrowableSubject(e)
+ return assertThat(e)
}
inline fun <T : Throwable> assertThrows(exceptionClass: KClass<T>, block: () -> Unit):
ThrowableSubject<T> {
val e = assertFailsWith<T>(exceptionClass = exceptionClass, block = block)
- return ThrowableSubject(e)
+ return assertThat(e)
}
\ No newline at end of file
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
index c5fdabd..8cc9fe9 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/MapSubject.kt
@@ -16,16 +16,14 @@
package androidx.kruth
-import kotlin.test.fail
-
-class MapSubject<K, V>(actual: Map<K, V>?) : Subject<Map<K, V>>(actual) {
+class MapSubject<K, V> internal constructor(actual: Map<K, V>?) : Subject<Map<K, V>>(actual) {
/** Fails if the map is not empty. */
fun isEmpty() {
requireNonNull(actual) { "Expected to be empty, but was null" }
if (actual.isNotEmpty()) {
- fail("Expected to be empty, but was $actual")
+ asserter.fail("Expected to be empty, but was $actual")
}
}
@@ -34,7 +32,7 @@
requireNonNull(actual) { "Expected to contain $key, but was null" }
if (!actual.containsKey(key)) {
- fail("Expected to contain $key, but was ${actual.keys}")
+ asserter.fail("Expected to contain $key, but was ${actual.keys}")
}
}
}
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/StandardSubjectBuilder.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/StandardSubjectBuilder.kt
new file mode 100644
index 0000000..85fe74e
--- /dev/null
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/StandardSubjectBuilder.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.kruth
+
+/**
+ * In a fluent assertion chain, an object with which you can do any of the following:
+ *
+ * - Set an optional message with [withMessage].
+ * - For the types of [Subject] built into Kruth, directly specify the value under test
+ * with [withMessage].
+ */
+class StandardSubjectBuilder internal constructor(
+ private val metadata: FailureMetadata = FailureMetadata(),
+) {
+
+ /**
+ * Returns a new instance that will output the given message before the main failure message. If
+ * this method is called multiple times, the messages will appear in the order that they were
+ * specified.
+ */
+ fun withMessage(messageToPrepend: String): StandardSubjectBuilder =
+ StandardSubjectBuilder(metadata = metadata.withMessage(messageToPrepend = messageToPrepend))
+
+ fun <T> that(actual: T): Subject<T> =
+ Subject(actual = actual, metadata = metadata)
+
+ fun <T : Comparable<T>> that(actual: T?): ComparableSubject<T> =
+ ComparableSubject(actual = actual, metadata = metadata)
+
+ fun <T : Throwable> that(actual: T?): ThrowableSubject<T> =
+ ThrowableSubject(actual = actual, metadata = metadata)
+
+ fun that(actual: Boolean?): BooleanSubject =
+ BooleanSubject(actual = actual, metadata = metadata)
+
+ fun that(actual: String?): StringSubject =
+ StringSubject(actual = actual, metadata = metadata)
+
+ fun <T> that(actual: Iterable<T>?): IterableSubject<T> =
+ IterableSubject(actual = actual, metadata = metadata)
+
+ /**
+ * Reports a failure.
+ *
+ * To set a message, first call [withMessage] (or, more commonly, use the shortcut
+ * [assertWithMessage].
+ */
+ fun fail(): Nothing {
+ kotlin.test.fail(metadata.formatMessage())
+ }
+}
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/StringSubject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/StringSubject.kt
index 69497e0..dd3e03a 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/StringSubject.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/StringSubject.kt
@@ -16,34 +16,41 @@
package androidx.kruth
-import kotlin.test.assertContains
-import kotlin.test.assertNotNull
-import kotlin.test.fail
-
/**
* Propositions for string subjects.
*/
-class StringSubject(actual: String?) : ComparableSubject<String>(actual) {
+class StringSubject internal constructor(
+ actual: String?,
+ metadata: FailureMetadata = FailureMetadata(),
+) : ComparableSubject<String>(actual = actual, metadata = metadata) {
/**
* Fails if the string does not contain the given sequence.
*/
fun contains(charSequence: CharSequence) {
- assertNotNull(actual)
- assertContains(actual, charSequence)
+ asserter.assertNotNull(actual)
+
+ asserter.assertTrue(
+ message = "Expected to contain \"$charSequence\", but was: \"$actual\"",
+ actual = actual.contains(charSequence),
+ )
}
/** Fails if the string does not have the given length. */
fun hasLength(expectedLength: Int) {
- assertNotNull(actual)
- assertThat(actual.length).isEqualTo(expectedLength)
+ asserter.assertNotNull(actual)
+
+ asserter.assertTrue(
+ message = "Expected to have length $expectedLength, but was: \"$actual\"",
+ actual = actual.length == expectedLength,
+ )
}
/** Fails if the string is not equal to the zero-length "empty string." */
fun isEmpty() {
- assertNotNull(actual)
+ asserter.assertNotNull(actual)
if (actual.isNotEmpty()) {
- fail(
+ asserter.fail(
"""
expected to be empty
| but was $actual
@@ -54,18 +61,18 @@
/** Fails if the string is equal to the zero-length "empty string." */
fun isNotEmpty() {
- assertNotNull(actual)
+ asserter.assertNotNull(actual)
if (actual.isEmpty()) {
- fail("expected not to be empty")
+ asserter.fail("expected not to be empty")
}
}
/** Fails if the string contains the given sequence. */
fun doesNotContain(string: CharSequence) {
- assertNotNull(actual, "expected a string that does not contain $string")
+ asserter.assertNotNull(actual, "expected a string that does not contain $string")
if (actual.contains(string)) {
- fail(
+ asserter.fail(
"""
expected not to contain $string
| but was $actual
@@ -76,10 +83,10 @@
/** Fails if the string does not start with the given string. */
fun startsWith(string: String) {
- assertNotNull(actual, "expected a string that starts with $string")
+ asserter.assertNotNull(actual, "expected a string that starts with $string")
if (!actual.startsWith(string)) {
- fail(
+ asserter.fail(
"""
expected to start with $string
| but was $actual
@@ -90,10 +97,10 @@
/** Fails if the string does not end with the given string. */
fun endsWith(string: String) {
- assertNotNull(actual, "expected a string that ends with $string")
+ asserter.assertNotNull(actual, "expected a string that ends with $string")
if (!actual.endsWith(string)) {
- fail(
+ asserter.fail(
"""
expected to end with $string
| but was $actual
@@ -125,13 +132,17 @@
fun isEqualTo(expected: String?) {
when {
(actual == null) && (expected != null) ->
- fail("Expected a string equal to \"$expected\" (case is ignored), but was null")
+ asserter.fail(
+ "Expected a string equal to \"$expected\" (case is ignored), but was null"
+ )
(expected == null) && (actual != null) ->
- fail("Expected a string that is null (null reference), but was \"$actual\"")
+ asserter.fail(
+ "Expected a string that is null (null reference), but was \"$actual\""
+ )
!actual.equals(expected, ignoreCase = true) ->
- fail(
+ asserter.fail(
"Expected a string equal to \"$expected\" (case is ignored), " +
"but was \"$actual\""
)
@@ -145,10 +156,12 @@
fun isNotEqualTo(unexpected: String?) {
when {
(actual == null) && (unexpected == null) ->
- fail("Expected a string not equal to null (null reference), but it was null")
+ asserter.fail(
+ "Expected a string not equal to null (null reference), but it was null"
+ )
actual.equals(unexpected, ignoreCase = true) ->
- fail(
+ asserter.fail(
"Expected a string not equal to \"$unexpected\" (case is ignored), " +
"but it was equal. Actual string: \"$actual\"."
)
@@ -161,29 +174,31 @@
when {
actual == null ->
- fail(
+ asserter.fail(
"Expected a string that contains \"$expected\" (case is ignored), " +
"but was null"
)
!actual.contains(expected, ignoreCase = true) ->
- fail("Expected to contain \"$expected\" (case is ignored), but was \"$actual\"")
+ asserter.fail(
+ "Expected to contain \"$expected\" (case is ignored), but was \"$actual\""
+ )
}
}
/** Fails if the string contains the given sequence (while ignoring case). */
fun doesNotContain(expected: CharSequence?) {
- checkNotNull(expected)
+ requireNonNull(expected)
when {
actual == null ->
- fail(
+ asserter.fail(
"Expected a string that does not contain \"$expected\" " +
"(case is ignored), but was null"
)
actual.contains(expected, ignoreCase = true) ->
- fail(
+ asserter.fail(
"Expected a string that does not contain \"$expected\" " +
"(case is ignored), but it was. Actual string: \"$actual\"."
)
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Subject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Subject.kt
index c70574bc..12f59e3 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Subject.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/Subject.kt
@@ -16,11 +16,7 @@
package androidx.kruth
-import kotlin.test.assertFalse
-import kotlin.test.assertIs
-import kotlin.test.assertIsNot
-import kotlin.test.assertTrue
-import kotlin.test.fail
+import kotlin.reflect.typeOf
// As opposed to Truth, which limits visibility on `actual` and the generic type, we purposely make
// them visible in Kruth to allow for an easier time extending in Kotlin.
@@ -32,7 +28,12 @@
*
* To create a [Subject] instance, most users will call an [assertThat] method.
*/
-open class Subject<out T>(val actual: T?) {
+open class Subject<out T> internal constructor(
+ val actual: T?,
+ private val metadata: FailureMetadata = FailureMetadata(),
+) {
+
+ internal val asserter: KruthAsserter = KruthAsserter(formatMessage = metadata::formatMessage)
/**
* Fails if the subject is not null.
@@ -83,7 +84,7 @@
/** Fails if the subject is not the same instance as the given object. */
open fun isSameInstanceAs(expected: Any?) {
if (actual !== expected) {
- fail(
+ asserter.fail(
"Expected ${actual.toStringForAssert()} to be the same instance as " +
"${expected.toStringForAssert()}, but was not"
)
@@ -93,26 +94,39 @@
/** Fails if the subject is the same instance as the given object. */
open fun isNotSameInstanceAs(unexpected: Any?) {
if (actual === unexpected) {
- fail("Expected ${actual.toStringForAssert()} not to be specific instance, but it was")
+ asserter.fail(
+ "Expected ${actual.toStringForAssert()} not to be specific instance, but it was"
+ )
}
}
/**
* Fails if the subject is not an instance of the given class.
*/
- inline fun <reified V> isInstanceOf() = assertIs<V>(actual)
+ inline fun <reified V> isInstanceOf() {
+ if (actual !is V) {
+ doFail("Expected $actual to be an instance of ${typeOf<V>()} but it was not")
+ }
+ }
/**
* Fails if the subject is an instance of the given class.
**/
inline fun <reified V> isNotInstanceOf() {
- assertIsNot<V>(actual)
+ if (actual is V) {
+ doFail("Expected $actual to be not an instance of ${typeOf<V>()} but it was")
+ }
+ }
+
+ @PublishedApi
+ internal fun doFail(message: String) {
+ asserter.fail(message = message)
}
/** Fails unless the subject is equal to any element in the given [iterable]. */
open fun isIn(iterable: Iterable<*>?) {
if (actual !in requireNonNull(iterable)) {
- fail("Expected $actual to be in $iterable, but was not")
+ asserter.fail("Expected $actual to be in $iterable, but was not")
}
}
@@ -124,7 +138,7 @@
/** Fails if the subject is equal to any element in the given [iterable]. */
open fun isNotIn(iterable: Iterable<*>?) {
if (actual in requireNonNull(iterable)) {
- fail("Expected $actual not to be in $iterable, but it was")
+ asserter.fail("Expected $actual not to be in $iterable, but it was")
}
}
@@ -132,67 +146,68 @@
open fun isNoneOf(first: Any?, second: Any?, vararg rest: Any?) {
isNotIn(listOf(first, second, *rest))
}
-}
-private fun Any?.standardIsEqualTo(expected: Any?) {
- assertTrue(
- compareForEquality(expected),
- "expected: ${expected.toStringForAssert()} but was: ${toStringForAssert()}",
- )
-}
-
-private fun Any?.standardIsNotEqualTo(unexpected: Any?) {
- assertFalse(
- compareForEquality(unexpected),
- "expected ${toStringForAssert()} not be equal to ${unexpected.toStringForAssert()}, " +
- "but it was",
- )
-}
-
-/**
- * Returns whether [this] equals [expected].
- *
- * The equality check follows the rules described on [Subject.isEqualTo].
- */
-private fun Any?.compareForEquality(expected: Any?): Boolean {
- @Suppress("SuspiciousEqualsCombination") // Intentional for behaviour compatibility.
- // This is migrated from Truth's equality helper, which has very specific logic for handling the
- // magic "casting" they do between types. See:
- // https://github.com/google/truth/blob/master/core/src/main/java/com/google/common/truth/Subject.java#L210
- return when {
- this == null && expected == null -> true
- this == null || expected == null -> false
- this is ByteArray && expected is ByteArray -> contentEquals(expected)
- this is IntArray && expected is IntArray -> contentEquals(expected)
- this is LongArray && expected is LongArray -> contentEquals(expected)
- this is FloatArray && expected is FloatArray -> contentEquals(expected)
- this is DoubleArray && expected is DoubleArray -> contentEquals(expected)
- this is ShortArray && expected is ShortArray -> contentEquals(expected)
- this is CharArray && expected is CharArray -> contentEquals(expected)
- this is Array<*> && expected is Array<*> -> contentDeepEquals(expected)
- isIntegralBoxedPrimitive() && expected.isIntegralBoxedPrimitive() -> {
- integralValue() == expected.integralValue()
- }
- this is Double && expected is Double -> compareTo(expected) == 0
- this is Float && expected is Float -> compareTo(expected) == 0
- this is Double && expected is Int -> compareTo(expected.toDouble()) == 0
- this is Float && expected is Int -> toDouble().compareTo(expected.toDouble()) == 0
- else -> this === expected || this == expected
+ private fun Any?.standardIsEqualTo(expected: Any?) {
+ asserter.assertTrue(
+ compareForEquality(expected),
+ "expected: ${expected.toStringForAssert()} but was: ${toStringForAssert()}",
+ )
}
-}
-private fun Any?.isIntegralBoxedPrimitive(): Boolean {
- return this is Byte || this is Short || this is Char || this is Int || this is Long
-}
+ private fun Any?.standardIsNotEqualTo(unexpected: Any?) {
+ asserter.assertFalse(
+ compareForEquality(unexpected),
+ "expected ${toStringForAssert()} not be equal to ${unexpected.toStringForAssert()}, " +
+ "but it was",
+ )
+ }
-private fun Any?.integralValue(): Long = when (this) {
- is Char -> code.toLong()
- is Number -> toLong()
- else -> throw AssertionError("$this must be either a Char or a Number.")
-}
+ /**
+ * Returns whether [this] equals [expected].
+ *
+ * The equality check follows the rules described on [Subject.isEqualTo].
+ */
+ private fun Any?.compareForEquality(expected: Any?): Boolean {
+ @Suppress("SuspiciousEqualsCombination") // Intentional for behaviour compatibility.
+ // This is migrated from Truth's equality helper, which has very specific logic for handling the
+ // magic "casting" they do between types. See:
+ // https://github.com/google/truth/blob/master/core/src/main/java/com/google/common/truth/Subject.java#L210
+ return when {
+ this == null && expected == null -> true
+ this == null || expected == null -> false
+ this is ByteArray && expected is ByteArray -> contentEquals(expected)
+ this is IntArray && expected is IntArray -> contentEquals(expected)
+ this is LongArray && expected is LongArray -> contentEquals(expected)
+ this is FloatArray && expected is FloatArray -> contentEquals(expected)
+ this is DoubleArray && expected is DoubleArray -> contentEquals(expected)
+ this is ShortArray && expected is ShortArray -> contentEquals(expected)
+ this is CharArray && expected is CharArray -> contentEquals(expected)
+ this is Array<*> && expected is Array<*> -> contentDeepEquals(expected)
+ isIntegralBoxedPrimitive() && expected.isIntegralBoxedPrimitive() -> {
+ integralValue() == expected.integralValue()
+ }
-private fun Any?.toStringForAssert(): String = when {
- this == null -> toString()
- isIntegralBoxedPrimitive() -> "${this::class.qualifiedName}<$this>"
- else -> toString()
+ this is Double && expected is Double -> compareTo(expected) == 0
+ this is Float && expected is Float -> compareTo(expected) == 0
+ this is Double && expected is Int -> compareTo(expected.toDouble()) == 0
+ this is Float && expected is Int -> toDouble().compareTo(expected.toDouble()) == 0
+ else -> this === expected || this == expected
+ }
+ }
+
+ private fun Any?.isIntegralBoxedPrimitive(): Boolean {
+ return this is Byte || this is Short || this is Char || this is Int || this is Long
+ }
+
+ private fun Any?.integralValue(): Long = when (this) {
+ is Char -> code.toLong()
+ is Number -> toLong()
+ else -> asserter.fail("$this must be either a Char or a Number.")
+ }
+
+ private fun Any?.toStringForAssert(): String = when {
+ this == null -> toString()
+ isIntegralBoxedPrimitive() -> "${this::class.qualifiedName}<$this>"
+ else -> toString()
+ }
}
diff --git a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/ThrowableSubject.kt b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/ThrowableSubject.kt
index 635b09a..2305ed4 100644
--- a/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/ThrowableSubject.kt
+++ b/testutils/testutils-kmp/src/commonMain/kotlin/androidx/kruth/ThrowableSubject.kt
@@ -19,12 +19,15 @@
/**
* Propositions for [Throwable] subjects.
*/
-class ThrowableSubject<T : Throwable>(actual: T?) : Subject<T>(actual) {
+class ThrowableSubject<T : Throwable> internal constructor(
+ actual: T?,
+ private val metadata: FailureMetadata = FailureMetadata(),
+) : Subject<T>(actual = actual, metadata = metadata) {
/**
* Returns a [StringSubject] to make assertions about the throwable's message.
*/
fun hasMessageThat(): StringSubject {
- return StringSubject(actual?.message)
+ return StringSubject(actual = actual?.message, metadata = metadata)
}
}
\ No newline at end of file
diff --git a/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/AssertWithMessageTest.kt b/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/AssertWithMessageTest.kt
new file mode 100644
index 0000000..57e27d4
--- /dev/null
+++ b/testutils/testutils-kmp/src/commonTest/kotlin/androidx/kruth/AssertWithMessageTest.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.kruth
+
+import kotlin.test.Test
+import kotlin.test.assertContains
+import kotlin.test.assertNotNull
+import kotlin.test.assertTrue
+import kotlin.test.fail
+
+class AssertWithMessageTest {
+
+ private val assert = assertWithMessage("Msg1").withMessage("Msg2")
+
+ @Test
+ fun that_customObjects_errorContainsMessage() {
+ assertFailsWithMessage {
+ assert.that(Some(value = 0)).isEqualTo(Some(value = 1))
+ }
+ }
+
+ @Test
+ fun that_comparable_errorContainsMessage() {
+ assertFailsWithMessage {
+ assert.that(1).isLessThan(0)
+ }
+ }
+
+ @Test
+ fun that_throwable_errorContainsMessage() {
+ assertFailsWithMessage {
+ assert.that(Exception("Msg")).hasMessageThat().contains("NonExistentString")
+ }
+ }
+
+ @Test
+ fun that_boolean_errorContainsMessage() {
+ assertFailsWithMessage {
+ assert.that(false).isTrue()
+ }
+ }
+
+ @Test
+ fun that_string_errorContainsMessage() {
+ assertFailsWithMessage {
+ assert.that("Str").contains("NonExistentString")
+ }
+ }
+
+ @Test
+ fun that_iterable_errorContainsMessage() {
+ assertFailsWithMessage {
+ assert.that(listOf(1, 2, 3)).contains(4)
+ }
+ }
+
+ @Test
+ fun fail_errorContainsMessage() {
+ assertFailsWithMessage {
+ assert.fail()
+ }
+ }
+
+ private fun assertFailsWithMessage(block: () -> Unit) {
+ try {
+ block()
+ fail("Expected to fail but didn't")
+ } catch (e: AssertionError) {
+ val msg = assertNotNull(e.message)
+ assertTrue(msg.startsWith("Msg1"))
+ assertContains(msg, "Msg2")
+ }
+ }
+
+ private data class Some(val value: Int)
+}
\ No newline at end of file