diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index 92c97ce8dab6..bb5ff7eb5bd9 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -43,7 +43,7 @@ JUnit repository on GitHub. current worktree status are now included in the XML report, if applicable. - A section containing JUnit-specific metadata about each test/container to the HTML report is now written by open-test-reporting when added to the classpath/module path - +* Introduced kotlin contracts for kotlin assertion methods. [[release-notes-5.12.0-M1-junit-jupiter]] === JUnit Jupiter 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 709aaba4fb1d..16ca2490a1c9 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 @@ -20,6 +20,8 @@ import java.time.Duration import java.util.function.Supplier import java.util.stream.Stream import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind.AT_MOST_ONCE +import kotlin.contracts.InvocationKind.EXACTLY_ONCE import kotlin.contracts.contract /** @@ -30,6 +32,20 @@ fun fail( throwable: Throwable? = null ): Nothing = Assertions.fail(message, throwable) +/** + * @see Assertions.fail + */ +@OptIn(ExperimentalContracts::class) +@API(since = "5.12", status = EXPERIMENTAL) +@JvmName("fail_nonNullableLambda") +fun fail(message: () -> String): Nothing { + contract { + callsInPlace(message, EXACTLY_ONCE) + } + + return Assertions.fail(message) +} + /** * @see Assertions.fail */ @@ -93,6 +109,154 @@ fun assertAll( 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 + */ +@OptIn(ExperimentalContracts::class) +@API(since = "5.12", 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 + */ +@OptIn(ExperimentalContracts::class) +@API(since = "5.12", 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 + */ +@OptIn(ExperimentalContracts::class) +@API(since = "5.12", 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 + */ +@OptIn(ExperimentalContracts::class) +@API(since = "5.12", 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 + */ +@OptIn(ExperimentalContracts::class) +@API(since = "5.12", 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 + */ +@OptIn(ExperimentalContracts::class) +@API(since = "5.12", 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 @@ -143,10 +307,15 @@ inline fun assertThrows( * ``` * @see Assertions.assertThrows */ +@OptIn(ExperimentalContracts::class) inline fun assertThrows( noinline message: () -> String, executable: () -> Unit ): T { + contract { + callsInPlace(message, AT_MOST_ONCE) + } + val throwable: Throwable? = try { executable() @@ -175,8 +344,15 @@ inline fun assertThrows( * @see Assertions.assertDoesNotThrow * @param R the result type of the [executable] */ +@OptIn(ExperimentalContracts::class) @API(status = STABLE, since = "5.11") -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: @@ -188,11 +364,18 @@ inline fun assertDoesNotThrow(executable: () -> R): R = Assertions.assertDoe * @see Assertions.assertDoesNotThrow * @param R the result type of the [executable] */ +@OptIn(ExperimentalContracts::class) @API(status = STABLE, since = "5.11") inline fun assertDoesNotThrow( message: String, executable: () -> R -): R = assertDoesNotThrow({ message }, executable) +): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + } + + return assertDoesNotThrow({ message }, executable) +} /** * Example usage: @@ -204,15 +387,22 @@ inline fun assertDoesNotThrow( * @see Assertions.assertDoesNotThrow * @param R the result type of the [executable] */ +@OptIn(ExperimentalContracts::class) @API(status = STABLE, since = "5.11") inline fun assertDoesNotThrow( noinline message: () -> String, executable: () -> R -): R = - Assertions.assertDoesNotThrow( +): 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 = @@ -231,13 +421,20 @@ internal inline fun evaluateAndWrap(executable: () -> R): ThrowingSupplier assertTimeout( timeout: Duration, executable: () -> R -): R = Assertions.assertTimeout(timeout, executable) +): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + } + + return Assertions.assertTimeout(timeout, executable) +} /** * Example usage: @@ -247,14 +444,21 @@ fun assertTimeout( * } * ``` * @see Assertions.assertTimeout - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ +@OptIn(ExperimentalContracts::class) @API(status = STABLE, since = "5.11") fun assertTimeout( timeout: Duration, message: String, executable: () -> R -): R = Assertions.assertTimeout(timeout, executable, message) +): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + } + + return Assertions.assertTimeout(timeout, executable, message) +} /** * Example usage: @@ -264,14 +468,22 @@ fun assertTimeout( * } * ``` * @see Assertions.assertTimeout - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ +@OptIn(ExperimentalContracts::class) @API(status = STABLE, since = "5.11") fun assertTimeout( timeout: Duration, message: () -> String, executable: () -> R -): R = Assertions.assertTimeout(timeout, executable, message) +): R { + contract { + callsInPlace(executable, EXACTLY_ONCE) + callsInPlace(message, AT_MOST_ONCE) + } + + return Assertions.assertTimeout(timeout, executable, message) +} /** * Example usage: @@ -281,7 +493,7 @@ fun assertTimeout( * } * ``` * @see Assertions.assertTimeoutPreemptively - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ @API(status = STABLE, since = "5.11") fun assertTimeoutPreemptively( @@ -297,7 +509,7 @@ fun assertTimeoutPreemptively( * } * ``` * @see Assertions.assertTimeoutPreemptively - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ @API(status = STABLE, since = "5.11") fun assertTimeoutPreemptively( @@ -314,19 +526,31 @@ fun assertTimeoutPreemptively( * } * ``` * @see Assertions.assertTimeoutPreemptively - * @paramR the result of the [executable]. + * @param R the result of the [executable]. */ +@OptIn(ExperimentalContracts::class) @API(status = STABLE, since = "5.11") fun assertTimeoutPreemptively( timeout: Duration, message: () -> String, executable: () -> R -): R = Assertions.assertTimeoutPreemptively(timeout, executable, message) +): R { + contract { + callsInPlace(message, AT_MOST_ONCE) + } + + return Assertions.assertTimeoutPreemptively(timeout, executable, message) +} /** * Example usage: * ```kotlin - * assertInstanceOf(list, "List should support fast random access") + * val maybeString: Any = ... + * + * assertInstanceOf(maybeString) + * + * // The compiler smart casts maybeString to a String object. + * assertTrue(maybeString.isNotEmpty()) * ``` * @see Assertions.assertInstanceOf * @since 5.11 @@ -343,7 +567,16 @@ inline fun assertInstanceOf( return Assertions.assertInstanceOf(T::class.java, actualValue, 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 * @since 5.11 */ @@ -355,6 +588,8 @@ inline fun assertInstanceOf( ): T { contract { returns() implies (actualValue is T) + + callsInPlace(message, AT_MOST_ONCE) } return Assertions.assertInstanceOf(T::class.java, actualValue, message) } diff --git a/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinAssertTimeoutAssertionsTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinAssertTimeoutAssertionsTests.kt index b85229eb0ded..ea057f658bd5 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinAssertTimeoutAssertionsTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinAssertTimeoutAssertionsTests.kt @@ -155,6 +155,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 @@ -287,6 +322,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/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinAssertionsTests.kt b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinAssertionsTests.kt index 79593e7b7a5a..9e6958c10ebe 100644 --- a/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinAssertionsTests.kt +++ b/jupiter-tests/src/test/kotlin/org/junit/jupiter/api/KotlinAssertionsTests.kt @@ -247,6 +247,89 @@ class KotlinAssertionsTests { assertMessageStartsWith(result, "Should be a String") } + @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. + } + + @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() + } + companion object { fun assertExpectedExceptionTypes( multipleFailuresError: MultipleFailuresError,