kotest: withClue() fails with EmptyStackException if a coroutine switches threads

Version: kotest-assertions-core:4.6.2

As described in the coroutine docs on thread-local data, a coroutine may switch threads at suspension points.

withClue() relies on thread-local data via ThreadLocalErrorCollector, without taking coroutine thread-switching into account. It fails if its block contains a coroutine resuming on a different thread.

Example

import io.kotest.assertions.withClue
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import org.junit.jupiter.api.Test

class TestCase {
    @Test
    fun `coroutine changing threads`() = runBlocking(Dispatchers.Unconfined) {
        withClue("Hello") {
            Thread.currentThread().run { println("withClue() block begins on $name, id $id") }
            delay(10)  // First suspension makes the Unconfined dispatcher resume on a different thread
            Thread.currentThread().run { println("withClue() block ends   on $name, id $id") }
        }
    }
}
Build script
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    kotlin("jvm") version "1.5.30"
    application
}

group = "me.oliver"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-assertions-core:4.6.2")
}

tasks.test {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile>() {
    kotlinOptions.jvmTarget = "11"
}

application {
    mainClass.set("MainKt")
}
Result
> Task :test FAILED
withClue() block begins on Test worker @coroutine#1, id 12
withClue() block ends   on kotlinx.coroutines.DefaultExecutor @coroutine#1, id 15

java.util.EmptyStackException
	at java.base/java.util.Stack.peek(Stack.java:102)
	at java.base/java.util.Stack.pop(Stack.java:84)
	at io.kotest.assertions.ThreadLocalErrorCollector.popClue(ErrorCollector.kt:32)
	at TestCase$coroutine changing threads$1.invokeSuspend(TestCase.kt:23)

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Comments: 32 (32 by maintainers)

Commits related to this issue

Most upvoted comments

Sorry, I had some things come up. I can provide code review though.

Yes we’ve mullled over this jim and also doing the same for soft assertions mode