Skip to content

Commit

Permalink
MarkusAmshove#178: added support for assertSoftly
Browse files Browse the repository at this point in the history
  • Loading branch information
drcolombo committed Nov 17, 2020
1 parent 1f6c346 commit 36f8438
Show file tree
Hide file tree
Showing 96 changed files with 577 additions and 114 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
}
1 change: 0 additions & 1 deletion common/src/main/kotlin/org/amshove/kluent/Basic.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ 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
1 change: 0 additions & 1 deletion common/src/main/kotlin/org/amshove/kluent/Collections.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,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
104 changes: 104 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,104 @@
package org.amshove.kluent

expect val errorCollector: ErrorCollector

enum class ErrorCollectionMode {
Soft, Hard
}

typealias Clue = () -> String

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()

fun pushClue(clue: Clue)

fun popClue()

/**
* Returns the current clue context.
* That is all the clues nested to this point.
*/
fun clueContext(): List<Clue>
}

open class BasicErrorCollector : ErrorCollector {

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

override fun getCollectionMode(): ErrorCollectionMode = mode

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

override fun pushClue(clue: Clue) {
clues.add(0, clue)
}

override fun popClue() {
clues.removeAt(0)
}

override fun clueContext(): List<Clue> = clues.toList()

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

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

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

fun clueContextAsString() = errorCollector.clueContext().let {
if (it.isEmpty()) "" else it.joinToString("\n", postfix = "\n") { f -> f.invoke() }
}

/**
* 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
}
}
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
}
34 changes: 26 additions & 8 deletions common/src/main/kotlin/org/amshove/kluent/internal/Assertions.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,34 @@
package org.amshove.kluent.internal

import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlin.test.fail
import org.amshove.kluent.*

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 +105,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 @@ -104,4 +123,3 @@ fun assertSame(expected: Any?, actual: Any?) {
fun assertNotSame(expected: Any?, actual: Any?) {
assertTrue("Expected <$expected>, actual <$actual> are the same instance.", actual !== expected)
}

4 changes: 4 additions & 0 deletions common/src/main/kotlin/org/amshove/kluent/internal/Utility.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,7 @@ internal fun <R, T> join(theMap: Map<R, T>): String = "Entries: [${theMap.entrie
internal fun joinKeys(map: Map<*, *>) = "Keys: [${join(map.keys)}]"
internal fun joinValues(map: Map<*, *>) = "Values: [${join(map.values)}]"
internal fun joinPairs(map: Map<*, *>) = "Pairs: [${map.map { it.toPair() }.joinToString(", ")}]"

internal fun fail(message: String): Nothing {
throw AssertionError(message)
}
10 changes: 9 additions & 1 deletion common/src/test/kotlin/org/amshove/kluent/InternalAssertions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ fun assertMessage(message: String, func: () -> Unit) {
try {
func()
} catch (e: Throwable) {
e.message.shouldBeEqualTo(message)
e.message?.replace("\\s+|\\t|\\n".toRegex(), " ")?.trim().shouldBeEqualTo(message.replace("\\s+|\\t|\\n".toRegex(), " ").trim())
}
}

fun assertMessageContain(message: String, func: () -> Unit) {
try {
func()
} catch (e: Throwable) {
e.message?.contains(message, true)
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.amshove.kluent
package org.amshove.kluent.tests

data class Person(val name: String, val surname: String)

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.amshove.kluent.basic
package org.amshove.kluent.tests.basic

import org.amshove.kluent.shouldBeDigit
import kotlin.test.Test
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
package org.amshove.kluent.basic
package org.amshove.kluent.tests.basic

import org.amshove.kluent.assertMessage
import org.amshove.kluent.shouldBeFalse
import org.amshove.kluent.assertMessage
import org.amshove.kluent.assertMessageContain
import kotlin.test.Test
import kotlin.test.assertFails

Expand All @@ -18,6 +19,8 @@ class ShouldBeFalseShould {

@Test
fun provideADescriptiveMessage() {
assertMessage("Expected value to be false, but was true") { true.shouldBeFalse() }
assertMessageContain("Expected value to be false, but was true") {
true.shouldBeFalse()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.amshove.kluent.basic
package org.amshove.kluent.tests.basic

import org.amshove.kluent.shouldBeNull
import org.amshove.kluent.shouldNotBeNull
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.amshove.kluent.basic
package org.amshove.kluent.tests.basic

import kotlin.test.Test
import org.amshove.kluent.Person
import org.amshove.kluent.shouldBe
import org.amshove.kluent.tests.Person
import kotlin.test.assertFails

class ShouldBeShould {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package org.amshove.kluent.basic
package org.amshove.kluent.tests.basic

import org.amshove.kluent.assertMessage
import org.amshove.kluent.shouldBeTrue
import org.amshove.kluent.assertMessage
import org.amshove.kluent.assertMessageContain
import org.amshove.kluent.shouldBeFalse
import kotlin.test.Test
import kotlin.test.assertFails

Expand All @@ -18,6 +20,8 @@ class ShouldBeTrueShould {

@Test
fun provideADescriptiveMessage() {
assertMessage("Expected value to be true, but was false") { false.shouldBeTrue() }
assertMessageContain("Expected value to be true, but was false") {
false.shouldBeTrue()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package org.amshove.kluent.basic
package org.amshove.kluent.tests.basic

import org.amshove.kluent.Person
import org.amshove.kluent.shouldBeEqualTo
import org.amshove.kluent.tests.Person
import kotlin.test.Test
import kotlin.test.assertFails

Expand Down
Loading

0 comments on commit 36f8438

Please sign in to comment.