Skip to content

Commit

Permalink
Add Stacktrace Substring Matching to Throwable Matchers (#3998)
Browse files Browse the repository at this point in the history
This PR introduces the ability to match substrings and regular
expressions in the stacktrace of a `Throwable` in the Kotest assertions
library. This feature can be useful when writing tests that need to
assert on specific parts of a stacktrace.

Changes:

1. Added two new infix functions `shouldHaveStacktraceContaining` and
`shouldNotHaveStacktraceContaining` to the `Throwable` class. These
functions accept a `String` or `Regex` and use it to match against the
stacktrace of the `Throwable`.
2. Added corresponding matcher functions `haveStacktraceContaining` that
create a `Matcher<Throwable>` to perform the actual matching logic.
3. Added tests for these new functions in `ThrowableMatchersTest.kt`.

This enhancement provides more flexibility when writing tests involving
exceptions and their stacktraces.

---------

Co-authored-by: Sam <sam@sksamuel.com>
Co-authored-by: Emil Kantis <emil.kantis@protonmail.com>
  • Loading branch information
3 people committed Jun 3, 2024
1 parent 2d3cbb6 commit 9b00d42
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2392,14 +2392,20 @@ public final class io/kotest/matchers/throwable/MatchersKt {
public static final fun haveCause ()Lio/kotest/matchers/Matcher;
public static final fun haveMessage (Ljava/lang/String;)Lio/kotest/matchers/Matcher;
public static final fun haveMessage (Lkotlin/text/Regex;)Lio/kotest/matchers/Matcher;
public static final fun haveStackTraceContaining (Ljava/lang/String;)Lio/kotest/matchers/Matcher;
public static final fun haveStackTraceContaining (Lkotlin/text/Regex;)Lio/kotest/matchers/Matcher;
public static final fun resultForThrowable (Ljava/lang/Throwable;)Lio/kotest/matchers/MatcherResult;
public static final fun shouldHaveCause (Ljava/lang/Throwable;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun shouldHaveCause$default (Ljava/lang/Throwable;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun shouldHaveMessage (Ljava/lang/Throwable;Ljava/lang/String;)V
public static final fun shouldHaveMessage (Ljava/lang/Throwable;Lkotlin/text/Regex;)V
public static final fun shouldHaveStackTraceContaining (Ljava/lang/Throwable;Ljava/lang/String;)V
public static final fun shouldHaveStackTraceContaining (Ljava/lang/Throwable;Lkotlin/text/Regex;)V
public static final fun shouldNotHaveCause (Ljava/lang/Throwable;)V
public static final fun shouldNotHaveMessage (Ljava/lang/Throwable;Ljava/lang/String;)V
public static final fun shouldNotHaveMessage (Ljava/lang/Throwable;Lkotlin/text/Regex;)V
public static final fun shouldNotHaveStackTraceContaining (Ljava/lang/Throwable;Ljava/lang/String;)V
public static final fun shouldNotHaveStackTraceContaining (Ljava/lang/Throwable;Lkotlin/text/Regex;)V
}

public final class io/kotest/matchers/time/DurationKt {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,24 @@ fun haveCause() = object : Matcher<Throwable> {
override fun test(value: Throwable) = resultForThrowable(value.cause)
}

infix fun Throwable.shouldHaveStackTraceContaining(substr: String) = this should haveStackTraceContaining(substr)
infix fun Throwable.shouldNotHaveStackTraceContaining(substr: String) = this shouldNot haveStackTraceContaining(substr)
fun haveStackTraceContaining(substr: String) = object : Matcher<Throwable> {
override fun test(value: Throwable) = MatcherResult(
value.stackTraceToString().contains(substr),
{ "Throwable stacktrace should contain substring: ${substr.print().value}\nActual was:\n${value.stackTraceToString().print().value}" },
{ "Throwable stacktrace should not contain substring: ${substr.print().value}" })
}

infix fun Throwable.shouldHaveStackTraceContaining(regex: Regex) = this should haveStackTraceContaining(regex)
infix fun Throwable.shouldNotHaveStackTraceContaining(regex: Regex) = this shouldNot haveStackTraceContaining(regex)
fun haveStackTraceContaining(regex: Regex) = object : Matcher<Throwable> {
override fun test(value: Throwable) = MatcherResult(
value.stackTraceToString().contains(regex),
{ "Throwable stacktrace should contain regex: ${regex.print().value}\nActual was:\n${value.stackTraceToString().print().value}" },
{ "Throwable stacktrace should not contain regex: ${regex.print().value}" })
}

inline fun <reified T : Throwable> Throwable.shouldHaveCauseInstanceOf() = this should haveCauseInstanceOf<T>()
inline fun <reified T : Throwable> Throwable.shouldNotHaveCauseInstanceOf() = this shouldNot haveCauseInstanceOf<T>()
inline fun <reified T : Throwable> haveCauseInstanceOf() = object : Matcher<Throwable> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,15 @@ import io.kotest.matchers.throwable.shouldHaveCause
import io.kotest.matchers.throwable.shouldHaveCauseInstanceOf
import io.kotest.matchers.throwable.shouldHaveCauseOfType
import io.kotest.matchers.throwable.shouldHaveMessage
import io.kotest.matchers.throwable.shouldHaveStackTraceContaining
import io.kotest.matchers.throwable.shouldNotHaveCause
import io.kotest.matchers.throwable.shouldNotHaveCauseInstanceOf
import io.kotest.matchers.throwable.shouldNotHaveCauseOfType
import io.kotest.matchers.throwable.shouldNotHaveMessage
import io.kotest.matchers.throwable.shouldNotHaveStackTraceContaining
import java.io.FileNotFoundException
import java.io.IOException
import kotlin.text.RegexOption.DOT_MATCHES_ALL

class ThrowableMatchersTest : FreeSpec() {
init {
Expand Down Expand Up @@ -190,6 +193,26 @@ expected:<"foo"> but was:<"This is a test exception">"""
"shouldNotHaveCauseOfType" {
Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!.shouldNotHaveCauseOfType<IOException>()
}
"shouldHaveStackTraceContaining" {
Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!
.shouldHaveStackTraceContaining("CompleteTestException")
Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!
.shouldHaveStackTraceContaining("Complete\\w+Exception".toRegex())
shouldThrow<AssertionError> { CompleteTestException().shouldHaveStackTraceContaining("SomeOtherException") }
.shouldHaveMessage("^Throwable stacktrace should contain substring: \"SomeOtherException\"\nActual was:\n\".+".toRegex(DOT_MATCHES_ALL))
shouldThrow<AssertionError> { CompleteTestException().shouldHaveStackTraceContaining("SomeOt.+Exception".toRegex()) }
.shouldHaveMessage("^Throwable stacktrace should contain regex: SomeOt.+Exception\nActual was:\n\".+".toRegex(DOT_MATCHES_ALL))
}
"shouldNotHaveStackTraceContaining" {
Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!
.shouldNotHaveStackTraceContaining("ProductionException")
Result.failure<Any>(CompleteTestException()).exceptionOrNull()!!
.shouldNotHaveStackTraceContaining("Prod\\w+Exception".toRegex())
shouldThrow<AssertionError> { CompleteTestException().shouldNotHaveStackTraceContaining("file.txt not found") }
.shouldHaveMessage("Throwable stacktrace should not contain substring: \"file.txt not found\"")
shouldThrow<AssertionError> { CompleteTestException().shouldNotHaveStackTraceContaining(".+ not found".toRegex()) }
.shouldHaveMessage("Throwable stacktrace should not contain regex: .+ not found")
}
}

"shouldThrowWithMessage" {
Expand Down

0 comments on commit 9b00d42

Please sign in to comment.