Skip to content
This repository has been archived by the owner on Jan 3, 2025. It is now read-only.

#178: added support for assertSoftly #185

Merged
merged 5 commits into from
Dec 27, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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