diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc index 49a86d6c3abb..f6ce1a5caad4 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc @@ -30,7 +30,7 @@ repository on GitHub. test engines. * Error messages for type mismatches in `NamespacedHierarchicalStore` now include the actual type and value in addition to the required type. - +* Introduced kotlin contracts for kotlin assertion methods. [[release-notes-5.11.0-M1-junit-jupiter]] === JUnit Jupiter diff --git a/junit-jupiter-api/junit-jupiter-api.gradle.kts b/junit-jupiter-api/junit-jupiter-api.gradle.kts index 7dd03a78f4e9..8e43b97f89be 100644 --- a/junit-jupiter-api/junit-jupiter-api.gradle.kts +++ b/junit-jupiter-api/junit-jupiter-api.gradle.kts @@ -31,3 +31,11 @@ tasks { } } } + +kotlin { + sourceSets { + main { + languageSettings.optIn("kotlin.contracts.ExperimentalContracts") + } + } +} diff --git a/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt b/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt index d0f5af1e218b..b71e19a8673d 100644 --- a/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt +++ b/junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt @@ -19,6 +19,9 @@ import org.junit.jupiter.api.function.ThrowingSupplier import java.time.Duration import java.util.function.Supplier import java.util.stream.Stream +import kotlin.contracts.InvocationKind.AT_MOST_ONCE +import kotlin.contracts.InvocationKind.EXACTLY_ONCE +import kotlin.contracts.contract /** * @see Assertions.fail @@ -26,6 +29,19 @@ import java.util.stream.Stream fun fail(message: String?, throwable: Throwable? = null): Nothing = Assertions.fail(message, throwable) +/** + * @see Assertions.fail + */ +@API(since = "5.11", status = EXPERIMENTAL) +@JvmName("fail_nonNullableLambda") +fun fail(message: () -> String): Nothing { + contract { + callsInPlace(message, EXACTLY_ONCE) + } + + return Assertions.fail(message) +} + /** * @see Assertions.fail */ @@ -86,6 +102,201 @@ fun assertAll(vararg executables: () -> Unit) = fun assertAll(heading: String?, vararg executables: () -> Unit) = assertAll(heading, executables.toList().stream()) +/** + * Example usage: + * ```kotlin + * val nullableString: String? = ... + * + * assertNull(nullableString) + * + * // The compiler won't allow even safe calls, since nullableString is always null. + * // nullableString?.isNotEmpty() + * ``` + * @see Assertions.assertNull + */ +@API(since = "5.11", status = EXPERIMENTAL) +fun assertNull(actual: Any?) { + contract { + returns() implies (actual == null) + } + + Assertions.assertNull(actual) +} + +/** + * Example usage: + * ```kotlin + * val nullableString: String? = ... + * + * assertNull(nullableString, "Should be nullable") + * + * // The compiler won't allow even safe calls, since nullableString is always null. + * // nullableString?.isNotEmpty() + * ``` + * @see Assertions.assertNull + */ +@API(since = "5.11", status = EXPERIMENTAL) +fun assertNull(actual: Any?, message: String) { + contract { + returns() implies (actual == null) + } + + Assertions.assertNull(actual, message) +} + +/** + * Example usage: + * ```kotlin + * val nullableString: String? = ... + * + * assertNull(nullableString) { "Should be nullable" } + * + * // The compiler won't allow even safe calls, since nullableString is always null. + * // nullableString?.isNotEmpty() + * ``` + * @see Assertions.assertNull + */ +@API(since = "5.11", status = EXPERIMENTAL) +fun assertNull(actual: Any?, messageSupplier: () -> String) { + contract { + returns() implies (actual == null) + + callsInPlace(messageSupplier, AT_MOST_ONCE) + } + + Assertions.assertNull(actual, messageSupplier) +} + +/** + * Example usage: + * ```kotlin + * val nullableString: String? = ... + * + * assertNotNull(nullableString) + * + * // The compiler smart casts nullableString to a non-nullable object. + * assertTrue(nullableString.isNotEmpty()) + * ``` + * @see Assertions.assertNotNull + */ +@API(since = "5.11", status = EXPERIMENTAL) +fun assertNotNull(actual: Any?) { + contract { + returns() implies (actual != null) + } + + Assertions.assertNotNull(actual) +} + +/** + * Example usage: + * ```kotlin + * val nullableString: String? = ... + * + * assertNotNull(nullableString, "Should be non-nullable") + * + * // The compiler smart casts nullableString to a non-nullable object. + * assertTrue(nullableString.isNotEmpty()) + * ``` + * @see Assertions.assertNotNull + */ +@API(since = "5.11", status = EXPERIMENTAL) +fun assertNotNull(actual: Any?, message: String) { + contract { + returns() implies (actual != null) + } + + Assertions.assertNotNull(actual, message) +} + +/** + * Example usage: + * ```kotlin + * val nullableString: String? = ... + * + * assertNotNull(nullableString) { "Should be non-nullable" } + * + * // The compiler smart casts nullableString to a non-nullable object. + * assertTrue(nullableString.isNotEmpty()) + * ``` + * @see Assertions.assertNotNull + */ +@API(since = "5.11", status = EXPERIMENTAL) +fun assertNotNull(actual: Any?, messageSupplier: () -> String) { + contract { + returns() implies (actual != null) + + callsInPlace(messageSupplier, AT_MOST_ONCE) + } + + Assertions.assertNotNull(actual, messageSupplier) +} + +/** + * Example usage: + * ```kotlin + * val maybeString: Any = ... + * + * assertInstanceOf(maybeString) + * + * // The compiler smart casts maybeString to a String object. + * assertTrue(maybeString.isNotEmpty()) + * ``` + * @see Assertions.assertInstanceOf + */ +@API(since = "5.11", status = EXPERIMENTAL) +inline fun assertInstanceOf(actual: Any?) { + contract { + returns() implies (actual is T) + } + + Assertions.assertInstanceOf(T::class.java, actual) +} + +/** + * Example usage: + * ```kotlin + * val maybeString: Any = ... + * + * assertInstanceOf(maybeString, "Should be a String") + * + * // The compiler smart casts maybeString to a String object. + * assertTrue(maybeString.isNotEmpty()) + * ``` + * @see Assertions.assertInstanceOf + */ +@API(since = "5.11", status = EXPERIMENTAL) +inline fun assertInstanceOf(actual: Any?, message: String) { + contract { + returns() implies (actual is T) + } + + Assertions.assertInstanceOf(T::class.java, actual, message) +} + +/** + * Example usage: + * ```kotlin + * val maybeString: Any = ... + * + * assertInstanceOf(maybeString) { "Should be a String" } + * + * // The compiler smart casts maybeString to a String object. + * assertTrue(maybeString.isNotEmpty()) + * ``` + * @see Assertions.assertInstanceOf + */ +@API(since = "5.11", status = EXPERIMENTAL) +inline fun assertInstanceOf(actual: Any?, noinline messageSupplier: () -> String) { + contract { + returns() implies (actual is T) + + callsInPlace(messageSupplier, AT_MOST_ONCE) + } + + Assertions.assertInstanceOf(T::class.java, actual, messageSupplier) +} + /** * Example usage: * ```kotlin @@ -134,6 +345,10 @@ inline fun assertThrows(message: String, executable: () * @see Assertions.assertThrows */ inline fun assertThrows(noinline message: () -> String, executable: () -> Unit): T { + contract { + callsInPlace(message, AT_MOST_ONCE) + } + val throwable: Throwable? = try { executable() } catch (caught: Throwable) { @@ -162,8 +377,13 @@ inline fun assertThrows(noinline message: () -> String, * @param R the result type of the [executable] */ @API(status = EXPERIMENTAL, since = "5.5") -inline fun assertDoesNotThrow(executable: () -> R): R = - Assertions.assertDoesNotThrow(evaluateAndWrap(executable)) +inline fun assertDoesNotThrow(executable: () -> R): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + } + + return Assertions.assertDoesNotThrow(evaluateAndWrap(executable)) +} /** * Example usage: @@ -176,8 +396,13 @@ inline fun assertDoesNotThrow(executable: () -> R): R = * @param R the result type of the [executable] */ @API(status = EXPERIMENTAL, since = "5.5") -inline fun assertDoesNotThrow(message: String, executable: () -> R): R = - assertDoesNotThrow({ message }, executable) +inline fun assertDoesNotThrow(message: String, executable: () -> R): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + } + + return assertDoesNotThrow({ message }, executable) +} /** * Example usage: @@ -190,11 +415,17 @@ inline fun assertDoesNotThrow(message: String, executable: () -> R): R = * @param R the result type of the [executable] */ @API(status = EXPERIMENTAL, since = "5.5") -inline fun assertDoesNotThrow(noinline message: () -> String, executable: () -> R): R = - Assertions.assertDoesNotThrow( +inline fun assertDoesNotThrow(noinline message: () -> String, executable: () -> R): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + callsInPlace(message, AT_MOST_ONCE) + } + + return Assertions.assertDoesNotThrow( evaluateAndWrap(executable), Supplier(message) ) +} @PublishedApi internal inline fun evaluateAndWrap(executable: () -> R): ThrowingSupplier = try { @@ -212,11 +443,16 @@ internal inline fun evaluateAndWrap(executable: () -> R): ThrowingSupplier assertTimeout(timeout: Duration, executable: () -> R): R = - Assertions.assertTimeout(timeout, executable) +fun assertTimeout(timeout: Duration, executable: () -> R): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + } + + return Assertions.assertTimeout(timeout, executable) +} /** * Example usage: @@ -226,11 +462,16 @@ fun assertTimeout(timeout: Duration, executable: () -> R): R = * } * ``` * @see Assertions.assertTimeout - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ @API(status = EXPERIMENTAL, since = "5.5") -fun assertTimeout(timeout: Duration, message: String, executable: () -> R): R = - Assertions.assertTimeout(timeout, executable, message) +fun assertTimeout(timeout: Duration, message: String, executable: () -> R): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + } + + return Assertions.assertTimeout(timeout, executable, message) +} /** * Example usage: @@ -240,11 +481,17 @@ fun assertTimeout(timeout: Duration, message: String, executable: () -> R): * } * ``` * @see Assertions.assertTimeout - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ @API(status = EXPERIMENTAL, since = "5.5") -fun assertTimeout(timeout: Duration, message: () -> String, executable: () -> R): R = - Assertions.assertTimeout(timeout, executable, message) +fun assertTimeout(timeout: Duration, message: () -> String, executable: () -> R): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + callsInPlace(message, AT_MOST_ONCE) + } + + return Assertions.assertTimeout(timeout, executable, message) +} /** * Example usage: @@ -254,7 +501,7 @@ fun assertTimeout(timeout: Duration, message: () -> String, executable: () - * } * ``` * @see Assertions.assertTimeoutPreemptively - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ @API(status = EXPERIMENTAL, since = "5.5") fun assertTimeoutPreemptively(timeout: Duration, executable: () -> R): R = @@ -268,7 +515,7 @@ fun assertTimeoutPreemptively(timeout: Duration, executable: () -> R): R = * } * ``` * @see Assertions.assertTimeoutPreemptively - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ @API(status = EXPERIMENTAL, since = "5.5") fun assertTimeoutPreemptively(timeout: Duration, message: String, executable: () -> R): R = @@ -282,8 +529,13 @@ fun assertTimeoutPreemptively(timeout: Duration, message: String, executable * } * ``` * @see Assertions.assertTimeoutPreemptively - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ @API(status = EXPERIMENTAL, since = "5.5") -fun assertTimeoutPreemptively(timeout: Duration, message: () -> String, executable: () -> R): R = - Assertions.assertTimeoutPreemptively(timeout, executable, message) +fun assertTimeoutPreemptively(timeout: Duration, message: () -> String, executable: () -> R): R { + contract { + callsInPlace(message, AT_MOST_ONCE) + } + + return Assertions.assertTimeoutPreemptively(timeout, executable, message) +} diff --git a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinAssertTimeoutAssertionsTests.kt b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinAssertTimeoutAssertionsTests.kt index 9170c22c9e7d..e0ef5a9096ad 100644 --- a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinAssertTimeoutAssertionsTests.kt +++ b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinAssertTimeoutAssertionsTests.kt @@ -145,6 +145,41 @@ internal class KotlinAssertTimeoutAssertionsTests { assertMessageStartsWith(error, "Tempus Fugit ==> execution exceeded timeout of 10 ms by") } + @Test + fun `assertTimeout with value initialization in lambda`() { + val value: Int + + assertTimeout(ofMillis(500)) { value = 10 } + + assertEquals(10, value) + } + + @Test + fun `assertTimeout with message and value initialization in lambda`() { + val value: Int + + assertTimeout(ofMillis(500), "message") { value = 10 } + + assertEquals(10, value) + } + + @Test + fun `assertTimeout with message supplier and value initialization in lambda`() { + val value: Int + val valueInMessageSupplier: Int + + assertTimeout( + timeout = ofMillis(500), + message = { + valueInMessageSupplier = 20 // Val can be assigned in the message supplier lambda. + "message" + }, + executable = { value = 10 } + ) + + assertEquals(10, value) + } + // -- executable - preemptively --- @Test @@ -266,6 +301,20 @@ internal class KotlinAssertTimeoutAssertionsTests { assertMessageEquals(error, "Tempus Fugit ==> execution timed out after 10 ms") } + @Test + fun `assertTimeoutPreemptively with message supplier and value initialization in lambda`() { + val valueInMessageSupplier: Int + + assertTimeoutPreemptively( + timeout = ofMillis(500), + message = { + valueInMessageSupplier = 20 // Val can be assigned in the message supplier lambda. + "message" + }, + executable = {} + ) + } + /** * Take a nap for 100 milliseconds. */ diff --git a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinAssertionsTests.kt b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinAssertionsTests.kt index 3386acf06b5d..869ab55f322c 100644 --- a/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinAssertionsTests.kt +++ b/junit-jupiter-engine/src/test/kotlin/org/junit/jupiter/api/KotlinAssertionsTests.kt @@ -61,6 +61,17 @@ class KotlinAssertionsTests { assertThrows({ "should fail" }) { fail(null as Throwable?) } } + @Test + fun `assertThrows with message supplier`() { + val valueInMessageSupplier: Int + + assertThrows({ + valueInMessageSupplier = 20 // Val can be assigned in the message supplier lambda. + + "should fail" + }) { fail("message") } + } + @Test fun `expected context exception testing`() = runBlocking { assertThrows("Should fail async") { @@ -184,6 +195,38 @@ class KotlinAssertionsTests { ) ) + @Test + fun `assertDoesNotThrow with value initialization in lambda`() { + val value: Int + + assertDoesNotThrow { value = 10 } + + assertEquals(10, value) + } + + @Test + fun `assertDoesNotThrow with message and value initialization in lambda`() { + val value: Int + + assertDoesNotThrow("message") { value = 10 } + + assertEquals(10, value) + } + + @Test + fun `assertDoesNotThrow with message supplier and value initialization in lambda`() { + val value: Int + val valueInMessageSupplier: Int + + assertDoesNotThrow({ + valueInMessageSupplier = 20 // Val can be assigned in the message supplier lambda. + + "message" + }) { value = 10 } + + assertEquals(10, value) + } + @Test fun `assertAll with stream of functions that throw AssertionErrors`() { val multipleFailuresError = assertThrows("Should have thrown multiple errors") { @@ -211,6 +254,119 @@ class KotlinAssertionsTests { assertMessageStartsWith(error, assertionMessage) } + @Test + fun `assertNotNull with compiler smart cast`() { + val nullableString: String? = "string" + + assertNotNull(nullableString) + assertFalse(nullableString.isEmpty()) // A smart cast to a non-nullable object. + } + + @Test + fun `assertNotNull with message and compiler smart cast`() { + val nullableString: String? = "string" + + assertNotNull(nullableString, "nullableString is null") + assertFalse(nullableString.isEmpty()) // A smart cast to a non-nullable object. + } + + @Test + fun `assertNotNull with message supplier and compiler smart cast`() { + val nullableString: String? = "string" + + val valueInMessageSupplier: Int + + assertNotNull(nullableString) { + valueInMessageSupplier = 20 // Val can be assigned in the message supplier lambda. + + "nullableString is null" + } + + assertFalse(nullableString.isEmpty()) // A smart cast to a non-nullable object. + } + + @Test + fun `assertNull with compiler smart cast`() { + val nullableString: String? = null + + assertNull(nullableString) + // Even safe call is not allowed because compiler knows that nullableString is always null. + // nullableString?.isEmpty() + } + + @Test + fun `assertNull with message and compiler smart cast`() { + val nullableString: String? = null + + assertNull(nullableString, "nullableString is not null") + // Even safe call is not allowed because compiler knows that nullableString is always null. + // nullableString?.isEmpty() + } + + @Test + fun `assertNull with message supplier and compiler smart cast`() { + val nullableString: String? = null + + val valueInMessageSupplier: Int + + assertNull(nullableString) { + valueInMessageSupplier = 20 // Val can be assigned in the message supplier lambda. + + "nullableString is not null" + } + + // Even safe call is not allowed because compiler knows that nullableString is always null. + // nullableString?.isEmpty() + } + + @Test + fun `assertInstanceOf with compiler smart cast`() { + val maybeString: Any = "string" + + assertInstanceOf(maybeString) + assertFalse(maybeString.isEmpty()) // A smart cast to a String object. + } + + @Test + fun `assertInstanceOf with compiler nullable smart cast`() { + val maybeString: Any? = "string" + + assertInstanceOf(maybeString) + assertFalse(maybeString.isEmpty()) // A smart cast to a non-nullable String object. + } + + @Test + fun `assertInstanceOf with a null value`() { + val error = assertThrows { + assertInstanceOf(null) + } + + assertMessageStartsWith(error, "Unexpected null value") + } + + @Test + fun `assertInstanceOf with message and compiler smart cast`() { + val maybeString: Any = "string" + + assertInstanceOf(maybeString, "maybeString is not an instance of String") + assertFalse(maybeString.isEmpty()) // A smart cast to a String object. + } + + @Test + fun `assertInstanceOf with message supplier and compiler smart cast`() { + val maybeString: Any = "string" + + val valueInMessageSupplier: Int + + assertInstanceOf(maybeString) { + valueInMessageSupplier = 20 // Val can be assigned in the message supplier lambda. + + "maybeString is not an instance of String" + } + + assertFalse(maybeString.isEmpty()) // A smart cast to a String object. + } + companion object { fun assertExpectedExceptionTypes( multipleFailuresError: MultipleFailuresError,