Skip to content

Commit

Permalink
Merge pull request #185 from drcolombo/master
Browse files Browse the repository at this point in the history
#178: added support for assertSoftly
  • Loading branch information
MarkusAmshove authored Dec 27, 2020
2 parents 1f6c346 + 3fafddf commit d3dfbea
Show file tree
Hide file tree
Showing 130 changed files with 1,015 additions and 196 deletions.
47 changes: 47 additions & 0 deletions common/src/main/kotlin/org/amshove/kluent/AssertionErrors.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package org.amshove.kluent

import kotlin.test.assertFails

/** An error that bundles multiple other [Throwable]s together */
class MultiAssertionError(errors: List<Throwable>) : AssertionError(createMessage(errors)) {
companion object {
private fun createMessage(errors: List<Throwable>) = buildString {
append("\nThe following ")

if (errors.size == 1) {
append("assertion")
} else {
append(errors.size).append(" assertions")
}
append(" failed:\n")

if (errors.size == 1) {
append(errors[0].message).append("\n")
stacktraces.throwableLocation(errors[0])?.let {
append("\tat ").append(it).append("\n")
}
} else {
for ((i, err) in errors.withIndex()) {
append(i + 1).append(") ").append(err.message).append("\n")
stacktraces.throwableLocation(err)?.let {
append("\tat ").append(it).append("\n")
}
}
}
}
}
}

fun assertionError(error: Throwable): Throwable {
val message = buildString {
append("\nThe following assertion failed:\n")

append(error.message).append("\n")
stacktraces.throwableLocation(error)?.let {
append("\tat ").append(it).append("\n")
}
}
val t = AssertionError(message)
stacktraces.cleanStackTrace(t)
return t
}
3 changes: 0 additions & 3 deletions common/src/main/kotlin/org/amshove/kluent/Basic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ package org.amshove.kluent
import org.amshove.kluent.internal.*
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.fail

@Deprecated("Use `shouldBeEqualTo`", ReplaceWith("this.shouldBeEqualTo(expected)"))
infix fun <T> T.shouldEqual(expected: T?): T = this.shouldBeEqualTo(expected)
Expand Down
4 changes: 2 additions & 2 deletions common/src/main/kotlin/org/amshove/kluent/CharSequence.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.amshove.kluent

import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.internal.assertFalse
import org.amshove.kluent.internal.assertNotEquals
import org.amshove.kluent.internal.assertTrue
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

infix fun <T : CharSequence> T.shouldStartWith(expected: T) = this.apply { assertTrue("Expected the CharSequence $this to start with $expected", this.startsWith(expected)) }

Expand Down
3 changes: 0 additions & 3 deletions common/src/main/kotlin/org/amshove/kluent/Collections.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package org.amshove.kluent

import org.amshove.kluent.internal.*
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
import kotlin.test.fail

infix fun <T> Array<T>.shouldContain(expected: T) = apply { if (this.contains(expected)) Unit else failExpectedActual("Array doesn't contain \"$expected\"", "the Array to contain \"$expected\"", join(this)) }

Expand Down
77 changes: 77 additions & 0 deletions common/src/main/kotlin/org/amshove/kluent/ErrorCollector.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package org.amshove.kluent

expect val errorCollector: ErrorCollector

enum class ErrorCollectionMode {
Soft, Hard
}

interface ErrorCollector {

fun getCollectionMode(): ErrorCollectionMode

fun setCollectionMode(mode: ErrorCollectionMode)

/**
* Returns the errors accumulated in the current context.
*/
fun errors(): List<Throwable>

/**
* Adds the given error to the current context.
*/
fun pushError(t: Throwable)

/**
* Clears all errors from the current context.
*/
fun clear()
}

open class BasicErrorCollector : ErrorCollector {

private val failures = mutableListOf<Throwable>()
private var mode = ErrorCollectionMode.Hard

override fun getCollectionMode(): ErrorCollectionMode = mode

override fun setCollectionMode(mode: ErrorCollectionMode) {
this.mode = mode
}

override fun pushError(t: Throwable) {
failures.add(t)
}

override fun errors(): List<Throwable> = failures.toList()

override fun clear() = failures.clear()
}

/**
* If we are in "soft assertion mode" will add this throwable to the
* list of throwables for the current execution. Otherwise will
* throw immediately.
*/
fun ErrorCollector.collectOrThrow(error: Throwable) {
when (getCollectionMode()) {
ErrorCollectionMode.Soft -> pushError(error)
ErrorCollectionMode.Hard -> throw error
}
}

/**
* The errors for the current execution are thrown as a single
* throwable.
*/
fun ErrorCollector.throwCollectedErrors() {
// set the collection mode back to the default
setCollectionMode(ErrorCollectionMode.Hard)
val failures = errors()
clear()
if (failures.isNotEmpty()) {
val t = MultiAssertionError(failures)
stacktraces.cleanStackTrace(t)
throw t
}
}
4 changes: 2 additions & 2 deletions common/src/main/kotlin/org/amshove/kluent/Numerical.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.amshove.kluent

import org.amshove.kluent.internal.assertEquals
import org.amshove.kluent.internal.assertNotEquals
import org.amshove.kluent.internal.assertTrue
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

@Deprecated("Use `shouldBeEqualTo`", ReplaceWith("this.shouldBeEqualTo(expected)"))
infix fun Boolean.shouldEqualTo(expected: Boolean) = this.shouldBeEqualTo(expected)
Expand Down
20 changes: 20 additions & 0 deletions common/src/main/kotlin/org/amshove/kluent/Softly.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package org.amshove.kluent

inline fun <T> assertSoftly(assertions: () -> T): T {
// Handle the edge case of nested calls to this function by only calling throwCollectedErrors in the
// outermost verifyAll block
if (errorCollector.getCollectionMode() == ErrorCollectionMode.Soft) {
return assertions()
}
errorCollector.setCollectionMode(ErrorCollectionMode.Soft)
return assertions().apply {
errorCollector.throwCollectedErrors()
}
}

inline fun <T> assertSoftly(t: T, assertions: T.(T) -> Unit): T {
return assertSoftly {
t.assertions(t)
t
}
}
37 changes: 37 additions & 0 deletions common/src/main/kotlin/org/amshove/kluent/StackTraces.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.amshove.kluent

expect val stacktraces: StackTraces

object BasicStackTraces : StackTraces {
override fun throwableLocation(t: Throwable): String? = null
override fun throwableLocation(t: Throwable, n: Int): List<String>? = null
override fun <T : Throwable> cleanStackTrace(throwable: T): T = throwable
override fun root(throwable: Throwable): Throwable = throwable
}

interface StackTraces {

/**
* Returns the first line of this stack trace, skipping io.kotest if possible.
* On some platforms the stack trace may not be available and will return null.
*/
fun throwableLocation(t: Throwable): String?

/**
* Returns the first n lines of this stack trace, skipping io.test if possible.
* On some platforms the stack trace may not be available and will return null.
*/
fun throwableLocation(t: Throwable, n: Int): List<String>?

/**
* Removes io.kotest stack elements from the given throwable if the platform supports stack traces,
* otherwise returns the exception as is.
*/
fun <T : Throwable> cleanStackTrace(throwable: T): T

/**
* Returns the root cause of the given throwable. If it has no root cause, or the platform does
* not support causes, this will be returned.
*/
fun root(throwable: Throwable): Throwable
}
128 changes: 121 additions & 7 deletions common/src/main/kotlin/org/amshove/kluent/internal/Assertions.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
package org.amshove.kluent.internal

import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.fail
import org.amshove.kluent.*
import kotlin.jvm.JvmName
import kotlin.reflect.KClass
import kotlin.test.asserter

internal fun assertTrue(message: String, boolean: Boolean) = assertTrue(boolean, message)
internal inline fun assertTrue(boolean: Boolean, lazyMessage: () -> String) {
if (!boolean) fail(lazyMessage())
internal fun assertTrue(actual: Boolean, message: String? = null) {
if (!actual) {
if (errorCollector.getCollectionMode() == ErrorCollectionMode.Soft) {
try {
throw AssertionError(message)
} catch (ex: AssertionError) {
errorCollector.pushError(ex)
}
} else {
try {
throw AssertionError(message)
} catch (ex: AssertionError) {
throw assertionError(ex)
}
}
}
}

internal inline fun assertTrue(actual: Boolean, lazyMessage: () -> String) {
assertTrue(actual, lazyMessage())
}

internal fun assertFalse(message: String, boolean: Boolean) = assertFalse(boolean, message)
fun assertFalse(actual: Boolean, message: String? = null) {
return assertTrue(message ?: "Expected value to be false.", !actual)
}

internal fun <T> assertArrayEquals(a1: Array<T>?, a2: Array<T>?) {
if (!arraysEqual(a1, a2)) {
Expand Down Expand Up @@ -86,8 +108,8 @@ internal fun failExpectedActual(message: String, expected: String?, actual: Stri

internal fun failCollectionWithDifferentItems(message: String, expected: String?, actual: String?): Nothing = fail("""
|$message
|${ if(!expected.isNullOrEmpty()) "Items included on the expected collection but not in the actual: $expected" else "" }
|${ if(!actual.isNullOrEmpty()) "Items included on the actual collection but not in the expected: $actual" else "" }
|${if (!expected.isNullOrEmpty()) "Items included on the expected collection but not in the actual: $expected" else ""}
|${if (!actual.isNullOrEmpty()) "Items included on the actual collection but not in the expected: $actual" else ""}
""".trimMargin())

internal fun failFirstSecond(message: String, first: String?, second: String?): Nothing = fail("""
Expand All @@ -105,3 +127,95 @@ fun assertNotSame(expected: Any?, actual: Any?) {
assertTrue("Expected <$expected>, actual <$actual> are the same instance.", actual !== expected)
}

/** Asserts that the [expected] value is equal to the [actual] value, with an optional [message]. */
fun <T> assertEquals(expected: T, actual: T, message: String? = null) {
assertEquals(message, expected, actual)
}

/**
* Asserts that the specified values are equal.
*
* @param message the message to report if the assertion fails.
*/
fun assertEquals(message: String?, expected: Any?, actual: Any?): Unit {
assertTrue(actual == expected) { messagePrefix(message) + "Expected <$expected>, actual <$actual>." }
}

/** Asserts that the [actual] value is not equal to the illegal value, with an optional [message]. */
fun <T> assertNotEquals(illegal: T, actual: T, message: String? = null) {
assertNotEquals(message, illegal, actual)
}

/**
* Asserts that the specified values are not equal.
*
* @param message the message to report if the assertion fails.
*/
fun assertNotEquals(message: String?, illegal: Any?, actual: Any?): Unit {
assertTrue(actual != illegal) { messagePrefix(message) + "Illegal value: <$actual>." }
}

/**
* Asserts that given function [block] fails by throwing an exception.
*
* @return An exception that was expected to be thrown and was successfully caught.
* The returned exception can be inspected further, for example by asserting its property values.
*/
@JvmName("assertFailsInline")
inline fun assertFails(block: () -> Unit): Throwable =
checkResultIsFailure(null, runCatching(block))

/**
* Asserts that given function [block] fails by throwing an exception.
*
* If the assertion fails, the specified [message] is used unless it is null as a prefix for the failure message.
*
* @return An exception that was expected to be thrown and was successfully caught.
* The returned exception can be inspected further, for example by asserting its property values.
*/
@JvmName("assertFailsInline")
inline fun assertFails(message: String?, block: () -> Unit): Throwable =
checkResultIsFailure(message, runCatching(block))

@PublishedApi
internal fun checkResultIsFailure(message: String?, blockResult: Result<Unit>): Throwable {
blockResult.fold(
onSuccess = {
asserter.fail(messagePrefix(message) + "Expected an exception to be thrown, but was completed successfully.")
},
onFailure = { e ->
return e
}
)
}

/** Asserts that a [block] fails with a specific exception of type [T] being thrown.
*
* If the assertion fails, the specified [message] is used unless it is null as a prefix for the failure message.
*
* @return An exception of the expected exception type [T] that successfully caught.
* The returned exception can be inspected further, for example by asserting its property values.
*/
inline fun <reified T : Throwable> assertFailsWith(message: String? = null, block: () -> Unit): T =
assertFailsWith(T::class, message, block)

/**
* Asserts that a [block] fails with a specific exception of type [exceptionClass] being thrown.
*
* @return An exception of the expected exception type [T] that successfully caught.
* The returned exception can be inspected further, for example by asserting its property values.
*/
@JvmName("assertFailsWithInline")
inline fun <T : Throwable> assertFailsWith(exceptionClass: KClass<T>, block: () -> Unit): T = assertFailsWith(exceptionClass, null, block)

/**
* Asserts that a [block] fails with a specific exception of type [exceptionClass] being thrown.
*
* If the assertion fails, the specified [message] is used unless it is null as a prefix for the failure message.
*
* @return An exception of the expected exception type [T] that successfully caught.
* The returned exception can be inspected further, for example by asserting its property values.
*/
@JvmName("assertFailsWithInline")
inline fun <T : Throwable> assertFailsWith(exceptionClass: KClass<T>, message: String?, block: () -> Unit): T =
checkResultIsFailure(exceptionClass, message, runCatching(block))
Loading

0 comments on commit d3dfbea

Please sign in to comment.