Skip to content

Commit

Permalink
Added contracts for kotlin assertions
Browse files Browse the repository at this point in the history
Added assertNull and assertNotNull methods with contracts.
Added contracts for assertThrows and assertDoesNotThrow methods.

assertInstanceOf can be implemented only with kotlin 1.4, because
refined generics
[are not supported](https://youtrack.jetbrains.com/issue/KT-28298)
in contracts for kotlin 1.3 yet.

Issue: junit-team#1866
  • Loading branch information
awelless committed Apr 7, 2023
1 parent e188b1e commit 914babd
Show file tree
Hide file tree
Showing 3 changed files with 266 additions and 20 deletions.
8 changes: 8 additions & 0 deletions junit-jupiter-api/junit-jupiter-api.gradle.kts
Expand Up @@ -30,3 +30,11 @@ tasks {
}
}
}

kotlin {
sourceSets {
main {
languageSettings.optIn("kotlin.contracts.ExperimentalContracts")
}
}
}
238 changes: 218 additions & 20 deletions junit-jupiter-api/src/main/kotlin/org/junit/jupiter/api/Assertions.kt
Expand Up @@ -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
Expand Down Expand Up @@ -86,6 +89,139 @@ fun assertAll(vararg executables: () -> Unit) =
fun assertAll(heading: String?, vararg executables: () -> Unit) =
assertAll(heading, executables.toList().stream())

/**
* Example usage:
* ```kotlin
* val string: String? = ...
*
* assertNull(string)
*
* // compiler won't allow even safe calls, since the string is always null
* // string?.isNotEmpty()
* ```
* @see Assertions.assertNull
*/
@API(since = "5.10", status = STABLE)
fun assertNull(actual: Any?) {
contract {
returns() implies (actual == null)
}

Assertions.assertNull(actual)
}

/**
* Example usage:
* ```kotlin
* val string: String? = ...
*
* assertNull(string, "Should be nullable")
*
* // compiler won't allow even safe calls, since the string is always null
* // string?.isNotEmpty()
* ```
* @see Assertions.assertNull
*/
@API(since = "5.10", status = STABLE)
fun assertNull(actual: Any?, message: String) {
contract {
returns() implies (actual == null)
}

Assertions.assertNull(actual, message)
}

/**
* Example usage:
* ```kotlin
* val string: String? = ...
*
* assertNull(string) { "Should be nullable" }
*
* // compiler won't allow even safe calls, since the string is always null
* // string?.isNotEmpty()
* ```
* @see Assertions.assertNull
*/
@API(since = "5.10", status = STABLE)
fun assertNull(actual: Any?, messageSupplier: () -> String) {
contract {
returns() implies (actual == null)

callsInPlace(messageSupplier, AT_MOST_ONCE)
}

Assertions.assertNull(actual, messageSupplier)
}

/**
* Example usage:
* ```kotlin
* val string: String? = ...
*
* assertNotNull(string)
*
* // compiler smart casts nullableString to a non-nullable object
* assertTrue(string.isNotEmpty())
* ```
* @see Assertions.assertNotNull
*/
@API(since = "5.10", status = STABLE)
fun <T> assertNotNull(actual: T?): T {
contract {
returns() implies (actual != null)
}

Assertions.assertNotNull(actual)
return actual!!
}

/**
* Example usage:
* ```kotlin
* val string: String? = ...
*
* assertNotNull(string, "Should be non-nullable")
*
* // compiler smart casts nullableString to a non-nullable object
* assertTrue(string.isNotEmpty())
* ```
* @see Assertions.assertNotNull
*/
@API(since = "5.10", status = STABLE)
fun <T> assertNotNull(actual: T?, message: String): T {
contract {
returns() implies (actual != null)
}

Assertions.assertNotNull(actual, message)
return actual!!
}

/**
* Example usage:
* ```kotlin
* val string: String? = ...
*
* assertNotNull(string) { "Should be non-nullable" }
*
* // compiler smart casts nullableString to a non-nullable object
* assertTrue(string.isNotEmpty())
* ```
* @see Assertions.assertNotNull
*/
@API(since = "5.10", status = STABLE)
fun <T> assertNotNull(actual: T?, messageSupplier: () -> String): T {
contract {
returns() implies (actual != null)

callsInPlace(messageSupplier, AT_MOST_ONCE)
}

Assertions.assertNotNull(actual, messageSupplier)
return actual!!
}

/**
* Example usage:
* ```kotlin
Expand All @@ -97,6 +233,10 @@ fun assertAll(heading: String?, vararg executables: () -> Unit) =
* @see Assertions.assertThrows
*/
inline fun <reified T : Throwable> assertThrows(executable: () -> Unit): T {
contract {
callsInPlace(executable, EXACTLY_ONCE)
}

val throwable: Throwable? = try {
executable()
} catch (caught: Throwable) {
Expand All @@ -120,8 +260,13 @@ inline fun <reified T : Throwable> assertThrows(executable: () -> Unit): T {
* ```
* @see Assertions.assertThrows
*/
inline fun <reified T : Throwable> assertThrows(message: String, executable: () -> Unit): T =
assertThrows({ message }, executable)
inline fun <reified T : Throwable> assertThrows(message: String, executable: () -> Unit): T {
contract {
callsInPlace(executable, EXACTLY_ONCE)
}

return assertThrows({ message }, executable)
}

/**
* Example usage:
Expand All @@ -134,6 +279,11 @@ inline fun <reified T : Throwable> assertThrows(message: String, executable: ()
* @see Assertions.assertThrows
*/
inline fun <reified T : Throwable> assertThrows(noinline message: () -> String, executable: () -> Unit): T {
contract {
callsInPlace(executable, EXACTLY_ONCE)
callsInPlace(message, AT_MOST_ONCE)
}

val throwable: Throwable? = try {
executable()
} catch (caught: Throwable) {
Expand Down Expand Up @@ -162,8 +312,13 @@ inline fun <reified T : Throwable> assertThrows(noinline message: () -> String,
* @param R the result type of the [executable]
*/
@API(status = EXPERIMENTAL, since = "5.5")
inline fun <R> assertDoesNotThrow(executable: () -> R): R =
Assertions.assertDoesNotThrow(evaluateAndWrap(executable))
inline fun <R> assertDoesNotThrow(executable: () -> R): R {
contract {
callsInPlace(executable, EXACTLY_ONCE)
}

return Assertions.assertDoesNotThrow(evaluateAndWrap(executable))
}

/**
* Example usage:
Expand All @@ -176,8 +331,13 @@ inline fun <R> assertDoesNotThrow(executable: () -> R): R =
* @param R the result type of the [executable]
*/
@API(status = EXPERIMENTAL, since = "5.5")
inline fun <R> assertDoesNotThrow(message: String, executable: () -> R): R =
assertDoesNotThrow({ message }, executable)
inline fun <R> assertDoesNotThrow(message: String, executable: () -> R): R {
contract {
callsInPlace(executable, EXACTLY_ONCE)
}

return assertDoesNotThrow({ message }, executable)
}

/**
* Example usage:
Expand All @@ -190,11 +350,17 @@ inline fun <R> assertDoesNotThrow(message: String, executable: () -> R): R =
* @param R the result type of the [executable]
*/
@API(status = EXPERIMENTAL, since = "5.5")
inline fun <R> assertDoesNotThrow(noinline message: () -> String, executable: () -> R): R =
Assertions.assertDoesNotThrow(
inline fun <R> 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 <R> evaluateAndWrap(executable: () -> R): ThrowingSupplier<R> = try {
Expand All @@ -215,8 +381,13 @@ internal inline fun <R> evaluateAndWrap(executable: () -> R): ThrowingSupplier<R
* @paramR the result of the [executable].
*/
@API(status = EXPERIMENTAL, since = "5.5")
fun <R> assertTimeout(timeout: Duration, executable: () -> R): R =
Assertions.assertTimeout(timeout, executable)
fun <R> assertTimeout(timeout: Duration, executable: () -> R): R {
contract {
callsInPlace(executable, EXACTLY_ONCE)
}

return Assertions.assertTimeout(timeout, executable)
}

/**
* Example usage:
Expand All @@ -229,8 +400,13 @@ fun <R> assertTimeout(timeout: Duration, executable: () -> R): R =
* @paramR the result of the [executable].
*/
@API(status = EXPERIMENTAL, since = "5.5")
fun <R> assertTimeout(timeout: Duration, message: String, executable: () -> R): R =
Assertions.assertTimeout(timeout, executable, message)
fun <R> assertTimeout(timeout: Duration, message: String, executable: () -> R): R {
contract {
callsInPlace(executable, EXACTLY_ONCE)
}

return Assertions.assertTimeout(timeout, executable, message)
}

/**
* Example usage:
Expand All @@ -243,8 +419,14 @@ fun <R> assertTimeout(timeout: Duration, message: String, executable: () -> R):
* @paramR the result of the [executable].
*/
@API(status = EXPERIMENTAL, since = "5.5")
fun <R> assertTimeout(timeout: Duration, message: () -> String, executable: () -> R): R =
Assertions.assertTimeout(timeout, executable, message)
fun <R> 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:
Expand All @@ -257,8 +439,13 @@ fun <R> assertTimeout(timeout: Duration, message: () -> String, executable: () -
* @paramR the result of the [executable].
*/
@API(status = EXPERIMENTAL, since = "5.5")
fun <R> assertTimeoutPreemptively(timeout: Duration, executable: () -> R): R =
Assertions.assertTimeoutPreemptively(timeout, executable)
fun <R> assertTimeoutPreemptively(timeout: Duration, executable: () -> R): R {
contract {
callsInPlace(executable, EXACTLY_ONCE)
}

return Assertions.assertTimeoutPreemptively(timeout, executable)
}

/**
* Example usage:
Expand All @@ -271,8 +458,13 @@ fun <R> assertTimeoutPreemptively(timeout: Duration, executable: () -> R): R =
* @paramR the result of the [executable].
*/
@API(status = EXPERIMENTAL, since = "5.5")
fun <R> assertTimeoutPreemptively(timeout: Duration, message: String, executable: () -> R): R =
Assertions.assertTimeoutPreemptively(timeout, executable, message)
fun <R> assertTimeoutPreemptively(timeout: Duration, message: String, executable: () -> R): R {
contract {
callsInPlace(executable, EXACTLY_ONCE)
}

return Assertions.assertTimeoutPreemptively(timeout, executable, message)
}

/**
* Example usage:
Expand All @@ -285,5 +477,11 @@ fun <R> assertTimeoutPreemptively(timeout: Duration, message: String, executable
* @paramR the result of the [executable].
*/
@API(status = EXPERIMENTAL, since = "5.5")
fun <R> assertTimeoutPreemptively(timeout: Duration, message: () -> String, executable: () -> R): R =
Assertions.assertTimeoutPreemptively(timeout, executable, message)
fun <R> assertTimeoutPreemptively(timeout: Duration, message: () -> String, executable: () -> R): R {
contract {
callsInPlace(executable, EXACTLY_ONCE)
callsInPlace(message, AT_MOST_ONCE)
}

return Assertions.assertTimeoutPreemptively(timeout, executable, message)
}
Expand Up @@ -211,6 +211,46 @@ class KotlinAssertionsTests {
assertMessageStartsWith(error, assertionMessage)
}

@Test
fun `assertNotNull with compiler smart cast`() {
val nullableString: String? = "string"

assertNotNull(nullableString)
assertFalse(nullableString.isEmpty()) // smart cast to 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 string is always null
// nullableString?.isEmpty()
}

@Test
fun `assertThrows with value initialization in lambda`() {
val value: String

assertThrows<AssertionError> {
value = "string"
Assertions.fail("message")
}

assertEquals("string", value)
}

@Test
fun `assertDoesNotThrow with value initialization in lambda`() {
val value: Int

assertDoesNotThrow {
value = 10
}

assertEquals(10, value)
}

companion object {
fun assertExpectedExceptionTypes(
multipleFailuresError: MultipleFailuresError,
Expand Down

0 comments on commit 914babd

Please sign in to comment.