From 2bb36899da3daa3294e39bf7cb58a6ef276d4063 Mon Sep 17 00:00:00 2001 From: Ilya Gorbunov Date: Sun, 26 Jan 2020 00:02:43 +0300 Subject: [PATCH] Introduce 'fail' method to throw AssertionError with cause #KT-37804 --- .../src/main/kotlin/kotlin/test/Assertions.kt | 22 +++++++++++++++++++ .../kotlin/kotlin/test/DefaultAsserter.kt | 5 +++++ .../kotlin/test/tests/BasicAssertionsTest.kt | 19 ++++++++++++---- .../kotlin/kotlin/test/DefaultJsAsserter.kt | 16 ++++++++------ .../js/src/main/kotlin/kotlin/test/JsImpl.kt | 5 +++++ .../junit/src/main/kotlin/JUnitSupport.kt | 12 ++++++++++ .../junit5/src/main/kotlin/JUnitSupport.kt | 9 +++++++- .../jvm/src/main/kotlin/AssertionsImpl.kt | 7 ++++++ .../testng/src/main/kotlin/TestNGSupport.kt | 7 ++++++ .../stdlib/js-ir/src/kotlin/exceptions.kt | 2 ++ .../stdlib/js-v1/src/kotlin/exceptions.kt | 4 +++- 11 files changed, 95 insertions(+), 13 deletions(-) diff --git a/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt index c4e26433d2b..850d994ff12 100644 --- a/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt +++ b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/Assertions.kt @@ -92,6 +92,17 @@ fun fail(message: String? = null): Nothing { asserter.fail(message) } +/** + * Marks a test as having failed if this point in the execution path is reached, with an optional [message] + * and [cause] exception. + * + * The [cause] exception is set as the root cause of the test failure. + */ +@SinceKotlin("1.4") +fun fail(message: String? = null, cause: Throwable? = null): Nothing { + asserter.fail(message, cause) +} + /** Asserts that given function [block] returns the given [expected] value. */ fun <@OnlyInputTypes T> expect(expected: T, block: () -> T) { assertEquals(expected, block()) @@ -173,6 +184,8 @@ inline fun assertFailsWith(exceptionClass: KClass, block: () inline fun assertFailsWith(exceptionClass: KClass, message: String?, block: () -> Unit): T = checkResultIsFailure(exceptionClass, message, runCatching(block)) +/** Platform-specific construction of AssertionError with cause */ +internal expect fun AssertionErrorWithCause(message: String?, cause: Throwable?): AssertionError /** * Abstracts the logic for performing assertions. Specific implementations of [Asserter] can use JUnit @@ -186,6 +199,15 @@ interface Asserter { */ fun fail(message: String?): Nothing + /** + * 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. + */ + @SinceKotlin("1.4") + fun fail(message: String?, cause: Throwable?): Nothing + /** * Asserts that the specified value is `true`. * diff --git a/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/DefaultAsserter.kt b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/DefaultAsserter.kt index 00f1af1a6ad..cff17c8689f 100644 --- a/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/DefaultAsserter.kt +++ b/libraries/kotlin.test/common/src/main/kotlin/kotlin/test/DefaultAsserter.kt @@ -15,6 +15,11 @@ object DefaultAsserter : Asserter { else throw AssertionError(message) } + + @SinceKotlin("1.4") + override fun fail(message: String?, cause: Throwable?): Nothing { + throw AssertionErrorWithCause(message, cause) + } } @Deprecated("DefaultAsserter is an object now, constructor call is not required anymore", diff --git a/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt b/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt index 5d385fcc524..7e33a253f42 100644 --- a/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt +++ b/libraries/kotlin.test/common/src/test/kotlin/kotlin/test/tests/BasicAssertionsTest.kt @@ -148,7 +148,7 @@ class BasicAssertionsTest { @Test() fun testAssertNotNullFails() { - checkFailedAssertion { assertNotNull(null) } + checkFailedAssertion { assertNotNull(null) } } @Test @@ -178,7 +178,18 @@ class BasicAssertionsTest { @Test() fun testFail() { - checkFailedAssertion { fail("should fail") } + val message = "should fail" + val actual = checkFailedAssertion { fail(message) } + assertEquals(message, actual.message) + } + + @Test + fun testFailWithCause() { + val message = "should fail due to" + val cause = IllegalStateException() + val actual = checkFailedAssertion { fail(message, cause) } + assertEquals(message, actual.message) + assertSame(cause, actual.cause) } @Test @@ -193,8 +204,8 @@ class BasicAssertionsTest { } -private fun checkFailedAssertion(assertion: () -> Unit) { - assertFailsWith { withDefaultAsserter(assertion) } +private fun checkFailedAssertion(assertion: () -> Unit): AssertionError { + return assertFailsWith { withDefaultAsserter(assertion) } } private fun withDefaultAsserter(block: () -> Unit) { diff --git a/libraries/kotlin.test/js/src/main/kotlin/kotlin/test/DefaultJsAsserter.kt b/libraries/kotlin.test/js/src/main/kotlin/kotlin/test/DefaultJsAsserter.kt index 4529f77059b..5ab5d9a6a35 100644 --- a/libraries/kotlin.test/js/src/main/kotlin/kotlin/test/DefaultJsAsserter.kt +++ b/libraries/kotlin.test/js/src/main/kotlin/kotlin/test/DefaultJsAsserter.kt @@ -57,7 +57,7 @@ internal object DefaultJsAsserter : Asserter { override fun assertTrue(lazyMessage: () -> String?, actual: Boolean) { if (!actual) { - failWithMessage(lazyMessage) + failWithMessage(lazyMessage, null) } else { invokeHook(true, lazyMessage) } @@ -68,16 +68,18 @@ internal object DefaultJsAsserter : Asserter { } override fun fail(message: String?): Nothing { - failWithMessage { message } + fail(message, null) } - private fun failWithMessage(lazyMessage: () -> String?): Nothing { + @SinceKotlin("1.4") + override fun fail(message: String?, cause: Throwable?): Nothing { + failWithMessage({ message }, cause) + } + + private inline fun failWithMessage(lazyMessage: () -> String?, cause: Throwable?): Nothing { val message = lazyMessage() invokeHook(false) { message } - if (message == null) - throw AssertionError() - else - throw AssertionError(message) + throw AssertionErrorWithCause(message, cause) } private fun invokeHook(result: Boolean, lazyMessage: () -> String?) { diff --git a/libraries/kotlin.test/js/src/main/kotlin/kotlin/test/JsImpl.kt b/libraries/kotlin.test/js/src/main/kotlin/kotlin/test/JsImpl.kt index f9cb5cd9a5e..5223cf7400f 100644 --- a/libraries/kotlin.test/js/src/main/kotlin/kotlin/test/JsImpl.kt +++ b/libraries/kotlin.test/js/src/main/kotlin/kotlin/test/JsImpl.kt @@ -17,6 +17,11 @@ actual fun todo(block: () -> Unit) { println("TODO at " + block) } +/** Platform-specific construction of AssertionError with cause */ +@Suppress("NOTHING_TO_INLINE") +internal actual inline fun AssertionErrorWithCause(message: String?, cause: Throwable?): AssertionError = + AssertionError(message, cause) + @PublishedApi internal actual fun checkResultIsFailure(exceptionClass: KClass, message: String?, blockResult: Result): T { diff --git a/libraries/kotlin.test/junit/src/main/kotlin/JUnitSupport.kt b/libraries/kotlin.test/junit/src/main/kotlin/JUnitSupport.kt index 41b8a101a64..9ccb29d64f8 100644 --- a/libraries/kotlin.test/junit/src/main/kotlin/JUnitSupport.kt +++ b/libraries/kotlin.test/junit/src/main/kotlin/JUnitSupport.kt @@ -57,4 +57,16 @@ object JUnitAsserter : Asserter { // should not get here throw AssertionError(message) } + + @SinceKotlin("1.4") + override fun fail(message: String?, cause: Throwable?): Nothing { + try { + Assert.fail(message) + } catch (e: AssertionError) { + e.initCause(cause) + throw e + } + // should not get here + throw AssertionError(message).initCause(cause) + } } diff --git a/libraries/kotlin.test/junit5/src/main/kotlin/JUnitSupport.kt b/libraries/kotlin.test/junit5/src/main/kotlin/JUnitSupport.kt index 5a1b3f0aa5f..c205df20b72 100644 --- a/libraries/kotlin.test/junit5/src/main/kotlin/JUnitSupport.kt +++ b/libraries/kotlin.test/junit5/src/main/kotlin/JUnitSupport.kt @@ -53,8 +53,15 @@ object JUnit5Asserter : Asserter { } override fun fail(message: String?): Nothing { - Assertions.fail(message) + Assertions.fail(message) // should not get here throw AssertionError(message) } + + @SinceKotlin("1.4") + override fun fail(message: String?, cause: Throwable?): Nothing { + Assertions.fail(message, cause) + // should not get here + throw AssertionError(message, cause) + } } diff --git a/libraries/kotlin.test/jvm/src/main/kotlin/AssertionsImpl.kt b/libraries/kotlin.test/jvm/src/main/kotlin/AssertionsImpl.kt index f9957b4f5fe..93e690fff0e 100644 --- a/libraries/kotlin.test/jvm/src/main/kotlin/AssertionsImpl.kt +++ b/libraries/kotlin.test/jvm/src/main/kotlin/AssertionsImpl.kt @@ -72,3 +72,10 @@ actual inline fun todo(@Suppress("UNUSED_PARAMETER") block: () -> Unit) { @InlineOnly inline fun currentStackTrace() = @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") (java.lang.Exception() as java.lang.Throwable).stackTrace + +/** Platform-specific construction of AssertionError with cause */ +internal actual fun AssertionErrorWithCause(message: String?, cause: Throwable?): AssertionError { + val assertionError = if (message == null) AssertionError() else AssertionError(message) + assertionError.initCause(cause) + return assertionError +} diff --git a/libraries/kotlin.test/testng/src/main/kotlin/TestNGSupport.kt b/libraries/kotlin.test/testng/src/main/kotlin/TestNGSupport.kt index 1bfe8a2ca5d..706fe138797 100644 --- a/libraries/kotlin.test/testng/src/main/kotlin/TestNGSupport.kt +++ b/libraries/kotlin.test/testng/src/main/kotlin/TestNGSupport.kt @@ -57,4 +57,11 @@ object TestNGAsserter : Asserter { // should not get here throw AssertionError(message) } + + @SinceKotlin("1.4") + override fun fail(message: String?, cause: Throwable?): Nothing { + Assert.fail(message, cause) + // should not get here + throw AssertionError(message).initCause(cause) + } } diff --git a/libraries/stdlib/js-ir/src/kotlin/exceptions.kt b/libraries/stdlib/js-ir/src/kotlin/exceptions.kt index 9a2a28a0cfa..aad37e8fa9a 100644 --- a/libraries/stdlib/js-ir/src/kotlin/exceptions.kt +++ b/libraries/stdlib/js-ir/src/kotlin/exceptions.kt @@ -80,6 +80,8 @@ public actual open class AssertionError : Error { public actual constructor() : super() public constructor(message: String?) : super(message) public actual constructor(message: Any?) : super(message?.toString(), message as? Throwable) + @SinceKotlin("1.4") + public constructor(message: String?, cause: Throwable?) : super(message, cause) } public actual open class NoSuchElementException : RuntimeException { diff --git a/libraries/stdlib/js-v1/src/kotlin/exceptions.kt b/libraries/stdlib/js-v1/src/kotlin/exceptions.kt index 207af95614c..1cd6cf48276 100644 --- a/libraries/stdlib/js-v1/src/kotlin/exceptions.kt +++ b/libraries/stdlib/js-v1/src/kotlin/exceptions.kt @@ -80,7 +80,9 @@ public actual open class ClassCastException actual constructor(message: String?) actual constructor() : this(null) } -public actual open class AssertionError private constructor(message: String?, cause: Throwable?) : Error(message, cause) { +public actual open class AssertionError +@SinceKotlin("1.4") +constructor(message: String?, cause: Throwable?) : Error(message, cause) { actual constructor() : this(null) constructor(message: String?) : this(message, null) actual constructor(message: Any?) : this(message.toString(), message as? Throwable)