diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/assertions/AssertionCounterMultithreadingTests.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/assertions/AssertionCounterMultithreadingTests.kt new file mode 100644 index 00000000000..30d42b70744 --- /dev/null +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/assertions/AssertionCounterMultithreadingTests.kt @@ -0,0 +1,25 @@ +package com.sksamuel.kotest.assertions + +import io.kotest.assertions.assertionCounter +import io.kotest.assertions.assertionCounterContextElement +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.withContext + +class AssertionCounterMultithreadingTests : FunSpec({ + test("assertionCounter should work across coroutine thread switch") { + withContext(Dispatchers.Unconfined + assertionCounterContextElement) { + val threadIds = mutableSetOf() + assertionCounter.inc() + threadIds.add(Thread.currentThread().id) + delay(50) + assertionCounter.inc() + threadIds.add(Thread.currentThread().id) + assertionCounter.get() shouldBe 2 + threadIds shouldHaveSize 2 + } + } +}) diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/assertions/timing/EventuallyTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/assertions/timing/EventuallyTest.kt index 5f6bc721a1a..32ac24ba4c2 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/assertions/timing/EventuallyTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/assertions/timing/EventuallyTest.kt @@ -20,20 +20,21 @@ import io.kotest.matchers.nulls.shouldBeNull import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.newSingleThreadContext +import kotlinx.coroutines.withContext import java.io.FileNotFoundException import java.io.IOException import java.util.concurrent.CountDownLatch -import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +@OptIn(DelicateCoroutinesApi::class) class EventuallyTest : WordSpec() { init { @@ -139,35 +140,37 @@ class EventuallyTest : WordSpec() { count.shouldBeLessThan(3) } "do one final iteration if we never executed before interval expired" { - val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - launch(dispatcher) { - Thread.sleep(250) - } - val counter = AtomicInteger(0) - withContext(dispatcher) { - // we won't be able to run in here - eventually(1.seconds, 5.milliseconds) { - counter.incrementAndGet() + newSingleThreadContext("single").use { dispatcher -> + launch(dispatcher) { + Thread.sleep(250) + } + val counter = AtomicInteger(0) + withContext(dispatcher) { + // we won't be able to run in here + eventually(1.seconds, 5.milliseconds) { + counter.incrementAndGet() + } } + counter.get().shouldBe(1) } - counter.get().shouldBe(1) } "do one final iteration if we only executed once and the last delay > interval" { - val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - // this will start immediately, free the dispatcher to allow eventually to run once, then block the thread - launch(dispatcher) { - delay(100.milliseconds) - Thread.sleep(500) - } - val counter = AtomicInteger(0) - withContext(dispatcher) { - // this will execute once immediately, then the earlier async will steal the thread - // and then since the delay has been > interval and times == 1, we will execute once more - eventually(250.milliseconds, 25.milliseconds) { - counter.incrementAndGet() shouldBe 2 + newSingleThreadContext("single").use { dispatcher -> + // this will start immediately, free the dispatcher to allow eventually to run once, then block the thread + launch(dispatcher) { + delay(100.milliseconds) + Thread.sleep(500) + } + val counter = AtomicInteger(0) + withContext(dispatcher) { + // this will execute once immediately, then the earlier async will steal the thread + // and then since the delay has been > interval and times == 1, we will execute once more + eventually(250.milliseconds, 25.milliseconds) { + counter.incrementAndGet() shouldBe 2 + } } + counter.get().shouldBe(2) } - counter.get().shouldBe(2) } "handle shouldNotBeNull" { val duration = measureTimeMillisCompat { diff --git a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/future/FutureMatcherTest.kt b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/future/FutureMatcherTest.kt index a43c341bbc6..bc071c9ac30 100644 --- a/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/future/FutureMatcherTest.kt +++ b/kotest-assertions/kotest-assertions-core/src/jvmTest/kotlin/com/sksamuel/kotest/matchers/future/FutureMatcherTest.kt @@ -3,11 +3,22 @@ package com.sksamuel.kotest.matchers.future import io.kotest.assertions.throwables.shouldThrowMessage import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.future.* +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.delay +import kotlinx.coroutines.newSingleThreadContext +import kotlinx.coroutines.withContext import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executors class FutureMatcherTest : StringSpec({ + suspend fun runOnSeparateThread(block: () -> Unit) { + @OptIn(DelicateCoroutinesApi::class) + newSingleThreadContext("separate").use { + withContext(it) { + block() + } + } + } + "test future is completed" { val completableFuture = CompletableFuture() completableFuture.complete(2) @@ -28,7 +39,7 @@ class FutureMatcherTest : StringSpec({ } "test future is completed exceptionally" { val completableFuture = CompletableFuture() - Executors.newFixedThreadPool(1).submit { + runOnSeparateThread { completableFuture.cancel(false) } delay(200) @@ -39,11 +50,11 @@ class FutureMatcherTest : StringSpec({ completableFuture.complete(2) completableFuture.shouldNotBeCompletedExceptionally() } - "test future completes exceptionally with the given exception"{ + "test future completes exceptionally with the given exception" { val completableFuture = CompletableFuture() val exception = RuntimeException("Boom Boom") - Executors.newFixedThreadPool(1).submit { + runOnSeparateThread { completableFuture.completeExceptionally(exception) } @@ -52,7 +63,7 @@ class FutureMatcherTest : StringSpec({ "test future does not completes exceptionally with given exception " { val completableFuture = CompletableFuture() - Executors.newFixedThreadPool(1).submit { + runOnSeparateThread { completableFuture.completeExceptionally(RuntimeException("Boom Boom")) } diff --git a/kotest-assertions/kotest-assertions-shared/api/kotest-assertions-shared.api b/kotest-assertions/kotest-assertions-shared/api/kotest-assertions-shared.api index 70df9ee8c9b..0fc138dd38a 100644 --- a/kotest-assertions/kotest-assertions-shared/api/kotest-assertions-shared.api +++ b/kotest-assertions/kotest-assertions-shared/api/kotest-assertions-shared.api @@ -300,13 +300,6 @@ public final class io/kotest/assertions/RetryKt { public static final fun retryConfig (Lkotlin/jvm/functions/Function1;)Lio/kotest/assertions/RetryConfig; } -public final class io/kotest/assertions/ThreadLocalAssertionCounter : io/kotest/assertions/AssertionCounter { - public static final field INSTANCE Lio/kotest/assertions/ThreadLocalAssertionCounter; - public fun get ()I - public fun inc ()V - public fun reset ()V -} - public final class io/kotest/assertions/async/TimeoutKt { public static final fun shouldTimeout (JLjava/util/concurrent/TimeUnit;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public static final fun shouldTimeout (Ljava/time/Duration;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; @@ -389,6 +382,7 @@ public final class io/kotest/assertions/eq/ThrowableEq : io/kotest/assertions/eq public final class io/kotest/assertions/jvmcounter { public static final fun getAssertionCounter ()Lio/kotest/assertions/AssertionCounter; + public static final fun getAssertionCounterContextElement ()Lkotlin/coroutines/CoroutineContext$Element; } public final class io/kotest/assertions/jvmerrorcollector { diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/assertions/ErrorCollector.kt b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/assertions/ErrorCollector.kt index ac2a7a7db3f..ea12fd5eafd 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/assertions/ErrorCollector.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/assertions/ErrorCollector.kt @@ -22,9 +22,7 @@ val errorCollectorContextElement: CoroutineContext.Element get() = ErrorCollectorContextElement(threadLocalErrorCollector.get()) -private val threadLocalErrorCollector = object : ThreadLocal() { - override fun initialValue() = CoroutineLocalErrorCollector() -} +private val threadLocalErrorCollector = ThreadLocal.withInitial { CoroutineLocalErrorCollector() } private class CoroutineLocalErrorCollector : BasicErrorCollector() { diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/assertions/counter.kt b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/assertions/counter.kt index 7c826f7873c..aa4295fba86 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/assertions/counter.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmMain/kotlin/io/kotest/assertions/counter.kt @@ -1,15 +1,36 @@ @file:JvmName("jvmcounter") + package io.kotest.assertions -actual val assertionCounter: AssertionCounter = ThreadLocalAssertionCounter +import kotlinx.coroutines.asContextElement +import kotlin.coroutines.CoroutineContext + +actual val assertionCounter: AssertionCounter get() = threadLocalAssertionCounter.get() + +/** + * A [CoroutineContext.Element] which keeps the [assertionCounter] synchronized with thread-switching coroutines. + * + * When using [assertionCounter] without the Kotest framework, this context element should be added to a + * coroutine context, e.g. via + * - `runBlocking(assertionCounterContextElement) { ... }` + * - `runTest(Dispatchers.IO + assertionCounterContextElement) { ... }` + */ +val assertionCounterContextElement: CoroutineContext.Element + get() = threadLocalAssertionCounter.asContextElement() -object ThreadLocalAssertionCounter : AssertionCounter { +private val threadLocalAssertionCounter: ThreadLocal = + ThreadLocal.withInitial { CoroutineLocalAssertionCounter() } - private val context = object : ThreadLocal() { - override fun initialValue(): Int = 0 +private class CoroutineLocalAssertionCounter : AssertionCounter { + private var value = 0 + + override fun get(): Int = value + + override fun reset() { + value = 0 } - override fun get(): Int = context.get() - override fun reset() = context.set(0) - override fun inc() = context.set(context.get() + 1) + override fun inc() { + value++ + } } diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmTest/kotlin/io/kotest/assertions/AssertSoftlyTests.kt b/kotest-assertions/kotest-assertions-shared/src/jvmTest/kotlin/io/kotest/assertions/AssertSoftlyTests.kt index 5718f1ae050..d750eddb1b9 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmTest/kotlin/io/kotest/assertions/AssertSoftlyTests.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmTest/kotlin/io/kotest/assertions/AssertSoftlyTests.kt @@ -18,7 +18,7 @@ class AssertSoftlyTests : FunSpec({ threadIds.add(Thread.currentThread().id) "assertSoftly block begins on $name, id $id" shouldBe "collected failure" } - delay(10) + delay(50) Thread.currentThread().run { threadIds.add(Thread.currentThread().id) "assertSoftly block ends on $name, id $id" shouldBe "collected failure" diff --git a/kotest-assertions/kotest-assertions-shared/src/jvmTest/kotlin/io/kotest/assertions/CluesTests.kt b/kotest-assertions/kotest-assertions-shared/src/jvmTest/kotlin/io/kotest/assertions/CluesTests.kt index 595da31eb16..1e8c53b0ae5 100644 --- a/kotest-assertions/kotest-assertions-shared/src/jvmTest/kotlin/io/kotest/assertions/CluesTests.kt +++ b/kotest-assertions/kotest-assertions-shared/src/jvmTest/kotlin/io/kotest/assertions/CluesTests.kt @@ -18,7 +18,7 @@ class CluesTests : FunSpec({ val threadIds = mutableSetOf() withClue("should not fail") { threadIds.add(Thread.currentThread().id) - delay(10) + delay(50) threadIds.add(Thread.currentThread().id) } threadIds shouldHaveSize 2 diff --git a/kotest-common/api/kotest-common.api b/kotest-common/api/kotest-common.api index 2bde16e2e39..64e59536ce6 100644 --- a/kotest-common/api/kotest-common.api +++ b/kotest-common/api/kotest-common.api @@ -1,7 +1,3 @@ -public final class io/kotest/common/ConcurrentHashMapKt { - public static final fun concurrentHashMap ()Ljava/util/Map; -} - public abstract interface annotation class io/kotest/common/DelicateKotest : java/lang/annotation/Annotation { } diff --git a/kotest-common/src/commonMain/kotlin/io/kotest/common/collections.kt b/kotest-common/src/commonMain/kotlin/io/kotest/common/collections.kt deleted file mode 100644 index 28d8cac079d..00000000000 --- a/kotest-common/src/commonMain/kotlin/io/kotest/common/collections.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.kotest.common - -expect fun concurrentHashMap(): MutableMap diff --git a/kotest-common/src/desktopMain/kotlin/io/kotest/common/collections.kt b/kotest-common/src/desktopMain/kotlin/io/kotest/common/collections.kt deleted file mode 100644 index 3892d035307..00000000000 --- a/kotest-common/src/desktopMain/kotlin/io/kotest/common/collections.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.kotest.common - -actual fun concurrentHashMap(): MutableMap = mutableMapOf() diff --git a/kotest-common/src/jsMain/kotlin/io/kotest/common/concurrentHashMap.kt b/kotest-common/src/jsMain/kotlin/io/kotest/common/concurrentHashMap.kt deleted file mode 100644 index 3892d035307..00000000000 --- a/kotest-common/src/jsMain/kotlin/io/kotest/common/concurrentHashMap.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.kotest.common - -actual fun concurrentHashMap(): MutableMap = mutableMapOf() diff --git a/kotest-common/src/jvmMain/kotlin/io/kotest/common/concurrentHashMap.kt b/kotest-common/src/jvmMain/kotlin/io/kotest/common/concurrentHashMap.kt deleted file mode 100644 index 3892d035307..00000000000 --- a/kotest-common/src/jvmMain/kotlin/io/kotest/common/concurrentHashMap.kt +++ /dev/null @@ -1,3 +0,0 @@ -package io.kotest.common - -actual fun concurrentHashMap(): MutableMap = mutableMapOf() diff --git a/kotest-common/src/jvmMain/kotlin/io/kotest/mpp/replay.kt b/kotest-common/src/jvmMain/kotlin/io/kotest/mpp/replay.kt index be62f6861d2..7654e83ad75 100644 --- a/kotest-common/src/jvmMain/kotlin/io/kotest/mpp/replay.kt +++ b/kotest-common/src/jvmMain/kotlin/io/kotest/mpp/replay.kt @@ -1,9 +1,9 @@ package io.kotest.mpp -import kotlinx.coroutines.runBlocking -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicReference +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.newFixedThreadPoolContext +import kotlinx.coroutines.withContext actual suspend fun replay( times: Int, @@ -15,23 +15,15 @@ actual suspend fun replay( action(it) } } else { - val executor = Executors.newFixedThreadPool(threads, NamedThreadFactory("replay-%d")) - val error = AtomicReference(null) - for (k in 0 until times) { - executor.submit { - runBlocking { - try { - action(k) - } catch (t: Throwable) { - error.compareAndSet(null, t) + @OptIn(DelicateCoroutinesApi::class) + newFixedThreadPoolContext(threads, "replay").use { dispatcher -> + withContext(dispatcher) { + repeat(times) { + launch { + action(it) } } } } - executor.shutdown() - executor.awaitTermination(1, TimeUnit.DAYS) - - if (error.get() != null) - throw error.get() } } diff --git a/kotest-extensions/kotest-extensions-blockhound/src/jvmTest/kotlin/io/kotest/extensions/blockhound/BlockHoundTest.kt b/kotest-extensions/kotest-extensions-blockhound/src/jvmTest/kotlin/io/kotest/extensions/blockhound/BlockHoundTest.kt index 95c21df6bf4..4265679265b 100644 --- a/kotest-extensions/kotest-extensions-blockhound/src/jvmTest/kotlin/io/kotest/extensions/blockhound/BlockHoundTest.kt +++ b/kotest-extensions/kotest-extensions-blockhound/src/jvmTest/kotlin/io/kotest/extensions/blockhound/BlockHoundTest.kt @@ -2,7 +2,6 @@ package io.kotest.extensions.blockhound import io.kotest.assertions.throwables.shouldNotThrow import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.annotation.DoNotParallelize import io.kotest.core.spec.style.FunSpec import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -39,7 +38,6 @@ class BlockHoundCaseTest : FunSpec({ } }) -@DoNotParallelize class BlockHoundSpecTest : FunSpec({ extension(BlockHound()) @@ -64,4 +62,13 @@ class BlockHoundSpecTest : FunSpec({ test("nested configuration").config(extensions = listOf(BlockHound(BlockHoundMode.DISABLED))) { shouldNotThrow { blockInNonBlockingContext() } } + + test("parallelism").config(invocations = 2, threads = 2) { + shouldThrow { + withContext(Dispatchers.Default) { + @Suppress("BlockingMethodInNonBlockingContext") + Thread.sleep(2) + } + } + } }) diff --git a/kotest-extensions/src/jvmMain/kotlin/io/kotest/extensions/system/wireListeners.kt b/kotest-extensions/src/jvmMain/kotlin/io/kotest/extensions/system/wireListeners.kt index bffb69cde26..9fce6c57827 100644 --- a/kotest-extensions/src/jvmMain/kotlin/io/kotest/extensions/system/wireListeners.kt +++ b/kotest-extensions/src/jvmMain/kotlin/io/kotest/extensions/system/wireListeners.kt @@ -13,9 +13,11 @@ import java.io.PrintStream inline fun captureStandardOut(fn: () -> Unit): String { val previous = System.out val buffer = ByteArrayOutputStream() + previous.flush() System.setOut(PrintStream(buffer)) try { fn() + System.out.flush() return String(buffer.toByteArray()) } finally { System.setOut(previous) @@ -28,9 +30,11 @@ inline fun captureStandardOut(fn: () -> Unit): String { inline fun captureStandardErr(fn: () -> Unit): String { val previous = System.err val buffer = ByteArrayOutputStream() + previous.flush() System.setErr(PrintStream(buffer)) try { fn() + System.err.flush() return String(buffer.toByteArray()) } finally { System.setErr(previous) @@ -56,6 +60,7 @@ class SystemOutWireListener(private val tee: Boolean = true) : TestListener { override suspend fun beforeAny(testCase: TestCase) { buffer = ByteArrayOutputStream() previous = System.out + previous.flush() if (tee) { System.setOut(PrintStream(TeeOutputStream(previous, buffer))) } else { @@ -84,6 +89,7 @@ class SystemErrWireListener(private val tee: Boolean = true) : TestListener { override suspend fun beforeAny(testCase: TestCase) { buffer = ByteArrayOutputStream() previous = System.err + previous.flush() if (tee) { System.setErr(PrintStream(TeeOutputStream(previous, buffer))) } else { diff --git a/kotest-framework/kotest-framework-api/api/kotest-framework-api.api b/kotest-framework/kotest-framework-api/api/kotest-framework-api.api index b465c70ea05..36864951684 100644 --- a/kotest-framework/kotest-framework-api/api/kotest-framework-api.api +++ b/kotest-framework/kotest-framework-api/api/kotest-framework-api.api @@ -866,9 +866,18 @@ public final class io/kotest/core/annotation/requirestag/WrapperKt { } public abstract interface class io/kotest/core/concurrency/CoroutineDispatcherFactory { + public abstract fun close ()V public abstract fun withDispatcher (Lio/kotest/core/test/TestCase;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } +public final class io/kotest/core/concurrency/CoroutineDispatcherFactory$DefaultImpls { + public static fun close (Lio/kotest/core/concurrency/CoroutineDispatcherFactory;)V +} + +public final class io/kotest/core/concurrency/CoroutineDispatcherFactoryKt { + public static final fun use (Lio/kotest/core/concurrency/CoroutineDispatcherFactory;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; +} + public abstract class io/kotest/core/config/AbstractProjectConfig { public fun ()V public fun afterAll ()V diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/concurrency/CoroutineDispatcherFactory.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/concurrency/CoroutineDispatcherFactory.kt index de1f8719517..d22dcbbd6c8 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/concurrency/CoroutineDispatcherFactory.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/concurrency/CoroutineDispatcherFactory.kt @@ -1,6 +1,8 @@ package io.kotest.core.concurrency import io.kotest.core.test.TestCase +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract /** * Switches the [kotlinx.coroutines.CoroutineDispatcher] used for test and spec execution. @@ -12,4 +14,26 @@ interface CoroutineDispatcherFactory { * It may be the same dispatcher as the calling coroutine. */ suspend fun withDispatcher(testCase: TestCase, f: suspend () -> T): T + + /** + * Close dispatchers created by the factory, releasing resources. + */ + fun close() {} +} + +inline fun T.use(block: (factory: T) -> R): R { + contract { + callsInPlace(block, InvocationKind.EXACTLY_ONCE) + } + return try { + block(this).also { + close() + } + } catch (e: Throwable) { + try { + close() + } catch (_: Throwable) { + } + throw e + } } diff --git a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/config/UnresolvedTestConfig.kt b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/config/UnresolvedTestConfig.kt index e834e0dc69f..71c39e2882e 100644 --- a/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/config/UnresolvedTestConfig.kt +++ b/kotest-framework/kotest-framework-api/src/commonMain/kotlin/io/kotest/core/test/config/UnresolvedTestConfig.kt @@ -84,7 +84,7 @@ data class UnresolvedTestConfig( init { require(invocations == null || invocations > 0) { "Number of invocations must be greater than 0" } require(threads == null || threads > 0) { "Number of threads must be greater than 0" } - require((threads ?: 0) <= (invocations ?: 0)) { "Number of threads must be <= number of invocations" } + require((threads ?: 0) <= (invocations ?: 1)) { "Number of threads must be <= number of invocations" } require(timeout?.isPositive() ?: true) { "Timeout must be positive" } require(invocationTimeout?.isPositive() ?: true) { "Invocation timeout must be positive" } } diff --git a/kotest-framework/kotest-framework-concurrency/src/jvmTest/kotlin/io/kotest/framework/concurrency/EventuallySpec.kt b/kotest-framework/kotest-framework-concurrency/src/jvmTest/kotlin/io/kotest/framework/concurrency/EventuallySpec.kt index e036e57d2a5..6cdf8a3a76b 100644 --- a/kotest-framework/kotest-framework-concurrency/src/jvmTest/kotlin/io/kotest/framework/concurrency/EventuallySpec.kt +++ b/kotest-framework/kotest-framework-concurrency/src/jvmTest/kotlin/io/kotest/framework/concurrency/EventuallySpec.kt @@ -15,15 +15,15 @@ import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.string.shouldContain -import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.launch +import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.withContext import java.io.FileNotFoundException import java.io.IOException import java.time.Duration import java.util.concurrent.CountDownLatch -import java.util.concurrent.Executors import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.AtomicInteger @@ -31,6 +31,7 @@ private fun Int.seconds(): Long = Duration.ofSeconds(this.toLong()).toMillis() private fun Int.milliseconds(): Long = this.toLong() @ExperimentalKotest +@OptIn(DelicateCoroutinesApi::class) class EventuallySpec : FunSpec({ test("eventually should immediately pass working tests") { @@ -150,42 +151,44 @@ class EventuallySpec : FunSpec({ } test("eventually does one final iteration if we never executed before interval expired") { - val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - launch(dispatcher) { - Thread.sleep(2000) - } - val counter = AtomicInteger(0) - withContext(dispatcher) { - // we won't be able to run in here - eventually({ - duration = 1.seconds() - interval = 100.milliseconds().fixed() - }) { - counter.incrementAndGet() + newSingleThreadContext("single").use { dispatcher -> + launch(dispatcher) { + Thread.sleep(2000) + } + val counter = AtomicInteger(0) + withContext(dispatcher) { + // we won't be able to run in here + eventually({ + duration = 1.seconds() + interval = 100.milliseconds().fixed() + }) { + counter.incrementAndGet() + } } + counter.get().shouldBe(1) } - counter.get().shouldBe(1) } test("eventually does one final iteration if we only executed once and the last delay > interval") { - val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher() - // this will start immediately, free the dispatcher to allow eventually to run once, then block the thread - launch(dispatcher) { - delay(100.milliseconds()) - Thread.sleep(500) - } - val counter = AtomicInteger(0) - withContext(dispatcher) { - // this will execute once immediately, then the earlier async will steal the thread - // and then since the delay has been > interval and times == 1, we will execute once more - eventually({ - duration = 250.milliseconds() - interval = 25.milliseconds().fixed() - }) { - counter.incrementAndGet() shouldBe 2 + newSingleThreadContext("single").use { dispatcher -> + // this will start immediately, free the dispatcher to allow eventually to run once, then block the thread + launch(dispatcher) { + delay(100.milliseconds()) + Thread.sleep(500) + } + val counter = AtomicInteger(0) + withContext(dispatcher) { + // this will execute once immediately, then the earlier async will steal the thread + // and then since the delay has been > interval and times == 1, we will execute once more + eventually({ + duration = 250.milliseconds() + interval = 25.milliseconds().fixed() + }) { + counter.incrementAndGet() shouldBe 2 + } } + counter.get().shouldBe(2) } - counter.get().shouldBe(2) } test("eventually handles shouldNotBeNull") { diff --git a/kotest-framework/kotest-framework-engine/api/kotest-framework-engine.api b/kotest-framework/kotest-framework-engine/api/kotest-framework-engine.api index cb9719eb300..b5cd975cc08 100644 --- a/kotest-framework/kotest-framework-engine/api/kotest-framework-engine.api +++ b/kotest-framework/kotest-framework-engine/api/kotest-framework-engine.api @@ -93,16 +93,19 @@ public final class io/kotest/engine/TestEngineLauncher { public final class io/kotest/engine/concurrency/DedicatedThreadCoroutineDispatcherFactory : io/kotest/core/concurrency/CoroutineDispatcherFactory { public static final field INSTANCE Lio/kotest/engine/concurrency/DedicatedThreadCoroutineDispatcherFactory; + public fun close ()V public fun withDispatcher (Lio/kotest/core/test/TestCase;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class io/kotest/engine/concurrency/FixedThreadCoroutineDispatcherFactory : io/kotest/core/concurrency/CoroutineDispatcherFactory { +public final class io/kotest/engine/concurrency/FixedThreadCoroutineDispatcherFactory : io/kotest/core/concurrency/CoroutineDispatcherFactory, java/io/Closeable { public fun (IZ)V + public fun close ()V public fun withDispatcher (Lio/kotest/core/test/TestCase;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } public final class io/kotest/engine/concurrency/NoopCoroutineDispatcherFactory : io/kotest/core/concurrency/CoroutineDispatcherFactory { public static final field INSTANCE Lio/kotest/engine/concurrency/NoopCoroutineDispatcherFactory; + public fun close ()V public fun withDispatcher (Lio/kotest/core/test/TestCase;Lkotlin/jvm/functions/Function1;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } @@ -307,13 +310,17 @@ public abstract class io/kotest/engine/listener/AbstractTestEngineListener : io/ public fun testStarted (Lio/kotest/core/test/TestCase;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } -public final class io/kotest/engine/listener/CollectingTestEngineListener : io/kotest/engine/listener/AbstractTestEngineListener { +public final class io/kotest/engine/listener/CollectingTestEngineListener : io/kotest/engine/listener/AbstractTestEngineListener, kotlinx/coroutines/sync/Mutex { public fun ()V public fun engineFinished (Ljava/util/List;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun getErrors ()Z public final fun getNames ()Ljava/util/List; + public fun getOnLock ()Lkotlinx/coroutines/selects/SelectClause2; public final fun getSpecs ()Ljava/util/Map; public final fun getTests ()Ljava/util/Map; + public fun holdsLock (Ljava/lang/Object;)Z + public fun isLocked ()Z + public fun lock (Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun result (Lio/kotest/core/descriptors/Descriptor$TestDescriptor;)Lio/kotest/core/test/TestResult; public final fun result (Ljava/lang/String;)Lio/kotest/core/test/TestResult; public final fun setErrors (Z)V @@ -322,6 +329,8 @@ public final class io/kotest/engine/listener/CollectingTestEngineListener : io/k public fun testFinished (Lio/kotest/core/test/TestCase;Lio/kotest/core/test/TestResult;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public fun testIgnored (Lio/kotest/core/test/TestCase;Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; public final fun toKey (Lio/kotest/core/test/TestCase;)Lio/kotest/engine/listener/CollectingTestEngineListener$TestCaseKey; + public fun tryLock (Ljava/lang/Object;)Z + public fun unlock (Ljava/lang/Object;)V } public final class io/kotest/engine/listener/CollectingTestEngineListener$TestCaseKey { diff --git a/kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/engine/ConcurrentTestSuiteScheduler.kt b/kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/engine/ConcurrentTestSuiteScheduler.kt index c966f7c3fe8..1c5cb14358b 100644 --- a/kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/engine/ConcurrentTestSuiteScheduler.kt +++ b/kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/engine/ConcurrentTestSuiteScheduler.kt @@ -3,6 +3,7 @@ package io.kotest.engine import io.kotest.common.ExperimentalKotest import io.kotest.core.annotation.DoNotParallelize import io.kotest.core.annotation.Isolate +import io.kotest.core.concurrency.use import io.kotest.core.project.TestSuite import io.kotest.core.spec.SpecRef import io.kotest.engine.concurrency.defaultCoroutineDispatcherFactory @@ -12,9 +13,7 @@ import io.kotest.engine.listener.CollectingTestEngineListener import io.kotest.engine.spec.SpecExecutor import io.kotest.mpp.Logger import io.kotest.mpp.bestName -import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.sync.withPermit @@ -54,31 +53,32 @@ internal class ConcurrentTestSuiteScheduler( private suspend fun schedule( specs: List, concurrency: Int, - ) = coroutineScope { // we don't want this function to return until all specs are completed - val coroutineDispatcherFactory = defaultCoroutineDispatcherFactory(context.configuration) - val semaphore = Semaphore(concurrency) - val collector = CollectingTestEngineListener() - specs.map { ref -> - logger.log { Pair(ref.kclass.bestName(), "Scheduling coroutine") } - async { - semaphore.withPermit { - logger.log { Pair(ref.kclass.bestName(), "Acquired permit") } + ) = defaultCoroutineDispatcherFactory(context.configuration).use { coroutineDispatcherFactory -> + coroutineScope { // we don't want this function to return until all specs are completed + val semaphore = Semaphore(concurrency) + val collector = CollectingTestEngineListener() + specs.map { ref -> + logger.log { Pair(ref.kclass.bestName(), "Scheduling coroutine") } + launch { + semaphore.withPermit { + logger.log { Pair(ref.kclass.bestName(), "Acquired permit") } - if (context.configuration.projectWideFailFast && collector.errors) { - context.listener.specIgnored(ref.kclass, null) - } else { - try { - val executor = SpecExecutor(coroutineDispatcherFactory, context.mergeListener(collector)) - logger.log { Pair(ref.kclass.bestName(), "Executing ref") } - executor.execute(ref) - } catch (t: Throwable) { - logger.log { Pair(ref.kclass.bestName(), "Unhandled error during spec execution $t") } - throw t + if (context.configuration.projectWideFailFast && collector.errors) { + context.listener.specIgnored(ref.kclass, null) + } else { + try { + val executor = SpecExecutor(coroutineDispatcherFactory, context.mergeListener(collector)) + logger.log { Pair(ref.kclass.bestName(), "Executing ref") } + executor.execute(ref) + } catch (t: Throwable) { + logger.log { Pair(ref.kclass.bestName(), "Unhandled error during spec execution $t") } + throw t + } } } + logger.log { Pair(ref.kclass.bestName(), "Released permit") } } - logger.log { Pair(ref.kclass.bestName(), "Released permit") } } - }.joinAll() + } } } diff --git a/kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/engine/listener/CollectingTestEngineListener.kt b/kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/engine/listener/CollectingTestEngineListener.kt index e8fd6c46f1d..a2255c8f0b3 100644 --- a/kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/engine/listener/CollectingTestEngineListener.kt +++ b/kotest-framework/kotest-framework-engine/src/commonMain/kotlin/io/kotest/engine/listener/CollectingTestEngineListener.kt @@ -1,44 +1,45 @@ package io.kotest.engine.listener -import io.kotest.common.concurrentHashMap import io.kotest.core.descriptors.Descriptor import io.kotest.core.names.TestName import io.kotest.core.spec.Spec import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock import kotlin.reflect.KClass -class CollectingTestEngineListener : AbstractTestEngineListener() { +class CollectingTestEngineListener : AbstractTestEngineListener(), Mutex by Mutex() { - val specs = concurrentHashMap, TestResult>() - val tests = concurrentHashMap() + val specs = mutableMapOf, TestResult>() + val tests = mutableMapOf() val names = mutableListOf() var errors = false fun result(descriptor: Descriptor.TestDescriptor): TestResult? = tests.mapKeys { it.key.descriptor }[descriptor] fun result(testname: String): TestResult? = tests.mapKeys { it.key.name.testName }[testname] - override suspend fun specFinished(kclass: KClass<*>, result: TestResult) { + override suspend fun specFinished(kclass: KClass<*>, result: TestResult) = withLock { specs[kclass] = result if (result.isErrorOrFailure) errors = true } - override suspend fun specIgnored(kclass: KClass<*>, reason: String?) { + override suspend fun specIgnored(kclass: KClass<*>, reason: String?) = withLock { specs[kclass] = TestResult.Ignored(reason) } - override suspend fun testIgnored(testCase: TestCase, reason: String?) { + override suspend fun testIgnored(testCase: TestCase, reason: String?): Unit = withLock { tests[testCase.toKey()] = TestResult.Ignored(reason) names.add(testCase.name.testName) } - override suspend fun testFinished(testCase: TestCase, result: TestResult) { + override suspend fun testFinished(testCase: TestCase, result: TestResult): Unit = withLock { tests[testCase.toKey()] = result if (result.isFailure || result.isError) errors = true names.add(testCase.name.testName) } - override suspend fun engineFinished(t: List) { + override suspend fun engineFinished(t: List) = withLock { if (t.isNotEmpty()) errors = true } diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/concurrency/DedicatedThreadCoroutineDispatcherFactory.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/concurrency/DedicatedThreadCoroutineDispatcherFactory.kt index 4bc6b42b318..f6bb4a8171e 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/concurrency/DedicatedThreadCoroutineDispatcherFactory.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/concurrency/DedicatedThreadCoroutineDispatcherFactory.kt @@ -2,9 +2,9 @@ package io.kotest.engine.concurrency import io.kotest.core.concurrency.CoroutineDispatcherFactory import io.kotest.core.test.TestCase -import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.withContext -import java.util.concurrent.Executors /** * A [CoroutineDispatcherFactory] that creates a dedicated thread for each test case. @@ -12,14 +12,11 @@ import java.util.concurrent.Executors */ object DedicatedThreadCoroutineDispatcherFactory : CoroutineDispatcherFactory { - override suspend fun withDispatcher(testCase: TestCase, f: suspend () -> T): T { - val executor = Executors.newSingleThreadExecutor() - return try { - withContext(executor.asCoroutineDispatcher()) { + @OptIn(DelicateCoroutinesApi::class) + override suspend fun withDispatcher(testCase: TestCase, f: suspend () -> T): T = + newSingleThreadContext("dedicated").use { dispatcher -> + withContext(dispatcher) { f() } - } finally { - executor.shutdown() } - } } diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/concurrency/FixedThreadCoroutineDispatcherFactory.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/concurrency/FixedThreadCoroutineDispatcherFactory.kt index 8b506439447..7319daaf8b7 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/concurrency/FixedThreadCoroutineDispatcherFactory.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/concurrency/FixedThreadCoroutineDispatcherFactory.kt @@ -1,13 +1,14 @@ package io.kotest.engine.concurrency -import io.kotest.common.concurrentHashMap import io.kotest.core.concurrency.CoroutineDispatcherFactory import io.kotest.core.test.TestCase import io.kotest.mpp.Logger import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.withContext -import java.util.concurrent.Executors +import java.io.Closeable +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger import kotlin.reflect.KClass @@ -17,7 +18,7 @@ import kotlin.reflect.KClass * * If [affinity] is true, then the same thread will be assigned to a spec and all it's tests. * This ensures that all tests and callbacks in a single spec are using the same thread. - * This option can be overriden at the spec level. + * This option can be overridden at the spec level. * * Affinity helps avoid subtle memory model issues on the JVM for those who are not * familiar with how the JVM guarantees updates to variables are visible across threads. @@ -31,11 +32,16 @@ import kotlin.reflect.KClass class FixedThreadCoroutineDispatcherFactory( threads: Int, private val affinity: Boolean, -) : CoroutineDispatcherFactory { +) : CoroutineDispatcherFactory, Closeable { private val logger = Logger(FixedThreadCoroutineDispatcherFactory::class) - private val dispatchers = List(threads) { Executors.newSingleThreadExecutor().asCoroutineDispatcher() } - private val cursor = AtomicInteger(0) + + @OptIn(DelicateCoroutinesApi::class) + private val dispatcherPool = List(threads) { newSingleThreadContext("fixed-${it + 1}/$threads") } + + private val requestCount = AtomicInteger(0) + + private val pinnedDispatchers = ConcurrentHashMap, CoroutineDispatcher>() override suspend fun withDispatcher(testCase: TestCase, f: suspend () -> T): T { @@ -43,10 +49,10 @@ class FixedThreadCoroutineDispatcherFactory( logger.log { Pair(testCase.name.testName, "affinity=$resolvedAffinity") } // if dispatcher affinity is set to true, we pick a dispatcher for the spec and stick with it - // otherwise each test just gets a random dispatcher + // otherwise each test just gets a dispatcher from the pool in a round-robin fashion val dispatcher = when (resolvedAffinity) { - true -> dispatcherFor(testCase.spec::class) - else -> dispatchers[cursor.incrementAndGet() % dispatchers.size] + true -> pinnedDispatchers.getOrPut(testCase.spec::class, ::nextDispatcher) + else -> nextDispatcher() } logger.log { Pair(testCase.name.testName, "Switching dispatcher to $dispatcher") } @@ -55,14 +61,15 @@ class FixedThreadCoroutineDispatcherFactory( } } - /** - * Returns a consistent dispatcher for the given [kclass]. + * Close dispatchers created by the factory, releasing threads. */ - private fun dispatcherFor(kclass: KClass<*>): CoroutineDispatcher = - dispatcherAffinity[kclass] ?: dispatchers[cursor.getAndIncrement() % dispatchers.size].also { - dispatcherAffinity[kclass] = it - } + override fun close() { + dispatcherPool.forEach { it.close() } + } - private val dispatcherAffinity = concurrentHashMap, CoroutineDispatcher>() + /** + * Returns the next dispatcher from the pool in a round-robin fashion. + */ + private fun nextDispatcher() = dispatcherPool[requestCount.getAndIncrement() % dispatcherPool.size] } diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/BlockedThreadTimeoutInterceptor.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/BlockedThreadTimeoutInterceptor.kt index 21c87ae768c..a289fd4979b 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/BlockedThreadTimeoutInterceptor.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/BlockedThreadTimeoutInterceptor.kt @@ -7,20 +7,24 @@ import io.kotest.core.test.TestResult import io.kotest.core.test.TestScope import io.kotest.engine.test.scopes.withCoroutineContext import io.kotest.mpp.Logger -import io.kotest.mpp.NamedThreadFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.withContext import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit +import kotlin.coroutines.coroutineContext import kotlin.time.Duration -// this scheduler is used to issue the interrupts after timeouts -// we only need one in the JVM -private val scheduler = - Executors.newScheduledThreadPool(1, NamedThreadFactory("BlockedThreadTimeoutInterceptor-%d", daemon = true)) +// Dispatcher used for jobs to issue the interrupts after timeouts. +// All such jobs share a single daemon thread on the JVM. +@OptIn(DelicateCoroutinesApi::class) +private val timeoutDispatcher = newSingleThreadContext("blocking-thread-timeout") /** - * If [io.kotest.core.test.TestCaseConfig.blockingTest] is enabled, then switches the execution + * If [io.kotest.core.test.config.ResolvedTestConfig.blockingTest] is enabled, then switches the execution * to a new thread, so it can be interrupted if the test times out. */ internal actual fun blockedThreadTimeoutInterceptor( @@ -39,23 +43,30 @@ internal class BlockedThreadTimeoutInterceptor( scope: TestScope, test: suspend (TestCase, TestScope) -> TestResult ): TestResult { - return if (testCase.config.blockingTest) { - - // we must switch execution onto a throwaway thread so the interruption task - // doesn't play havok with a thread in use elsewhere + return if (testCase.config.blockingTest) { + // we must switch execution onto a throwaway thread so an interruption + // doesn't play havoc with a thread in use elsewhere val executor = Executors.newSingleThreadExecutor() - // we schedule a task that will interrupt the coroutine after the timeout has expired - // this task will use the values in the coroutine status element to know which thread to interrupt - logger.log { Pair(testCase.name.testName, "Scheduler will interrupt this test in ${testCase.config.timeout}") } - val task = scheduler.schedule({ - logger.log { Pair(testCase.name.testName, "Scheduled timeout has hit") } - executor.shutdownNow() - }, testCase.config.timeout?.inWholeMilliseconds ?: 10000000000L, TimeUnit.MILLISECONDS) + val timeoutJob = testCase.config.timeout?.let { timeout -> + logger.log { Pair(testCase.name.testName, "this test will time out in $timeout") } + + CoroutineScope(coroutineContext).launch(timeoutDispatcher) { + delay(timeout) + logger.log { Pair(testCase.name.testName, "Scheduled timeout has hit") } + executor.shutdownNow() + } + } try { - withContext(executor.asCoroutineDispatcher()) { - test(testCase, scope.withCoroutineContext(coroutineContext)) + executor.asCoroutineDispatcher().use { dispatcher -> + withContext(dispatcher) { + try { + test(testCase, scope.withCoroutineContext(coroutineContext)) + } finally { + timeoutJob?.cancel() + } + } } } catch (t: InterruptedException) { logger.log { Pair(testCase.name.testName, "Caught InterruptedException ${t.message}") } @@ -63,12 +74,6 @@ internal class BlockedThreadTimeoutInterceptor( start.elapsedNow(), BlockedThreadTestTimeoutException(testCase.config.timeout ?: Duration.INFINITE, testCase.name.testName) ) - } finally { - // we should stop the scheduled task from running just to be tidy - if (!task.isDone) { - logger.log { Pair(testCase.name.testName, "Cancelling scheduled task ${System.identityHashCode(task)}") } - task.cancel(false) - } } } else { test(testCase, scope) diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/CoroutineErrorCollectorInterceptor.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/CoroutineErrorCollectorInterceptor.kt index cb6555ec2bd..dc5cdae4833 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/CoroutineErrorCollectorInterceptor.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/CoroutineErrorCollectorInterceptor.kt @@ -1,5 +1,6 @@ package io.kotest.engine.test.interceptors +import io.kotest.assertions.assertionCounterContextElement import io.kotest.assertions.errorCollectorContextElement import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult @@ -23,8 +24,13 @@ internal object CoroutineErrorCollectorInterceptor : TestExecutionInterceptor { scope: TestScope, test: suspend (TestCase, TestScope) -> TestResult ): TestResult { - logger.log { Pair(testCase.name.testName, "Adding $errorCollectorContextElement to coroutine context") } - return withContext(errorCollectorContextElement) { + logger.log { + Pair( + testCase.name.testName, + "Adding $errorCollectorContextElement and $assertionCounterContextElement to coroutine context" + ) + } + return withContext(errorCollectorContextElement + assertionCounterContextElement) { test(testCase, scope) } } diff --git a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/coroutineDispatcherFactoryInterceptor.kt b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/coroutineDispatcherFactoryInterceptor.kt index 2dbb4268541..a1c232cd4df 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/coroutineDispatcherFactoryInterceptor.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmMain/kotlin/io/kotest/engine/test/interceptors/coroutineDispatcherFactoryInterceptor.kt @@ -8,7 +8,6 @@ import io.kotest.engine.concurrency.FixedThreadCoroutineDispatcherFactory import io.kotest.engine.test.scopes.withCoroutineContext import io.kotest.mpp.Logger import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.TestDispatcher import kotlin.coroutines.coroutineContext @@ -46,15 +45,21 @@ internal class CoroutineDispatcherFactoryInterceptor( logger.log { Pair(testCase.name.testName, "userFactory=$userFactory; threads=$threads") } - val f = when { - userFactory != null -> userFactory - threads > 1 -> FixedThreadCoroutineDispatcherFactory(threads, false) - else -> defaultCoroutineDispatcherFactory + val (factory, factoryIsEphemeral) = when { + userFactory != null -> Pair(userFactory, false) + threads > 1 -> Pair(FixedThreadCoroutineDispatcherFactory(threads, false), true) + else -> Pair(defaultCoroutineDispatcherFactory, false) } - logger.log { Pair(testCase.name.testName, "Switching dispatcher using factory $f") } - f.withDispatcher(testCase) { - test(testCase, scope.withCoroutineContext(coroutineContext)) + try { + logger.log { Pair(testCase.name.testName, "Switching dispatcher using factory $factory") } + factory.withDispatcher(testCase) { + test(testCase, scope.withCoroutineContext(coroutineContext)) + } + } finally { + if (factoryIsEphemeral) { + factory.close() + } } } } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/coroutines/provokeThreadSwitch.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/coroutines/provokeThreadSwitch.kt new file mode 100644 index 00000000000..8f16f088ff9 --- /dev/null +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/coroutines/provokeThreadSwitch.kt @@ -0,0 +1,10 @@ +package com.sksamuel.kotest.engine.coroutines + +/** + * Provoke the dispatcher to switch coroutine threads by keeping the coroutine alive for a sufficient time period. + * + * This function is not suspending, but intended to be invoked in a coroutine exclusively. + */ +internal fun provokeThreadSwitch() { + Thread.sleep(50) +} diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/spec/coroutine/FeatureSpecCoroutineTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/spec/coroutine/FeatureSpecCoroutineTest.kt index a8c35623ff4..e60d2c01deb 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/spec/coroutine/FeatureSpecCoroutineTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/spec/coroutine/FeatureSpecCoroutineTest.kt @@ -1,5 +1,6 @@ package com.sksamuel.kotest.engine.spec.coroutine +import com.sksamuel.kotest.engine.coroutines.provokeThreadSwitch import io.kotest.core.spec.style.FeatureSpec import io.kotest.core.test.TestCase import io.kotest.core.test.TestResult @@ -54,18 +55,20 @@ class FeatureSpecCoroutineTest : FeatureSpec() { count.get() shouldBe 20 } scenario("multiple invocations and parallelism").config(invocations = 20, threads = 10) { - delay(5) count.incrementAndGet() + provokeThreadSwitch() } scenario("previous test result 2") { count.get() shouldBe 40 } // we need enough invocation to ensure all the threads get used up - scenario("mutliple threads should use a thread pool for the coroutines").config( - invocations = 200, // needs to be big enough to ensure all 6 threads get used + scenario("multiple threads should use a thread pool for the coroutines").config( + invocations = 6, threads = 6 ) { - logThreadName() + // strip off the coroutine suffix + threadnames.add(currentThreadWithoutCoroutine()) + provokeThreadSwitch() } scenario("previous test result 3") { threadnames.size shouldBe 6 @@ -80,11 +83,4 @@ class FeatureSpecCoroutineTest : FeatureSpec() { delay(500) longOpCompleted = true } - - private suspend fun logThreadName() { - delay(10) - Thread.sleep(10) - // strip off the coroutine suffix - threadnames.add(currentThreadWithoutCoroutine()) - } } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/spec/coroutine/WordSpecCoroutineTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/spec/coroutine/WordSpecCoroutineTest.kt index 72099588206..edd41a66308 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/spec/coroutine/WordSpecCoroutineTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/spec/coroutine/WordSpecCoroutineTest.kt @@ -1,5 +1,6 @@ package com.sksamuel.kotest.engine.spec.coroutine +import com.sksamuel.kotest.engine.coroutines.provokeThreadSwitch import io.kotest.core.spec.style.WordSpec import io.kotest.core.test.TestCase import io.kotest.core.test.TestCaseOrder @@ -60,8 +61,8 @@ class WordSpecCoroutineTest : WordSpec() { } "multiple invocations and parallelism".config(invocations = 20, threads = 10) { - delay(5) count.incrementAndGet() + provokeThreadSwitch() } "previous test result 2" { @@ -70,10 +71,11 @@ class WordSpecCoroutineTest : WordSpec() { // we need enough invocation to ensure all the threads get used up "mutliple threads should use a thread pool for the coroutines".config( - invocations = 200, // needs to be big enough to ensure all 6 threads get used + invocations = 6, threads = 6 ) { logThreadName() + provokeThreadSwitch() } "previous test result 3" { diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/test/InvocationThreadTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/test/InvocationThreadTest.kt index 0cdfa431c5f..8d0364af852 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/test/InvocationThreadTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/test/InvocationThreadTest.kt @@ -1,46 +1,37 @@ package com.sksamuel.kotest.engine.test +import com.sksamuel.kotest.engine.coroutines.provokeThreadSwitch import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.maps.shouldHaveSize import io.kotest.matchers.shouldBe import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicInteger -import kotlin.concurrent.getOrSet class InvocationThreadTest : FunSpec({ - val singleThreadSingleInvocationCounter = AtomicInteger(0) - val singleThreadMultipleInvocationCounter = PersistentThreadLocal() - val multipleThreadMultipleInvocationCounter = PersistentThreadLocal() + val singleThreadSingleInvocationCallCount = AtomicInteger(0) + val singleThreadMultipleInvocationCallCount = AtomicInteger(0) + val multipleThreadMultipleInvocationCallCount = AtomicInteger(0) + val multipleThreadMultipleInvocationThreadIds = ConcurrentHashMap() // use as concurrent set afterSpec { - singleThreadSingleInvocationCounter.get() shouldBe 1 - singleThreadMultipleInvocationCounter.map.values.sum() shouldBe 5 - multipleThreadMultipleInvocationCounter.map.shouldHaveSize(3) - multipleThreadMultipleInvocationCounter.map.values.sum() shouldBe 10 + singleThreadSingleInvocationCallCount.get() shouldBe 1 + singleThreadMultipleInvocationCallCount.get() shouldBe 5 + multipleThreadMultipleInvocationCallCount.get() shouldBe 3 + multipleThreadMultipleInvocationThreadIds.shouldHaveSize(3) } test("single thread / single invocation").config(invocations = 1, threads = 1) { - singleThreadSingleInvocationCounter.incrementAndGet() + singleThreadSingleInvocationCallCount.incrementAndGet() } test("single thread / multiple invocations").config(invocations = 5) { - val counter = singleThreadMultipleInvocationCounter.getOrSet { 0 } - singleThreadMultipleInvocationCounter.set(counter + 1) + singleThreadMultipleInvocationCallCount.incrementAndGet() } - test("multiple threads / multiple invocations").config(invocations = 10, threads = 3) { - val counter = multipleThreadMultipleInvocationCounter.getOrSet { 0 } - multipleThreadMultipleInvocationCounter.set(counter + 1) + test("multiple threads / multiple invocations").config(invocations = 3, threads = 3) { + multipleThreadMultipleInvocationCallCount.incrementAndGet() + multipleThreadMultipleInvocationThreadIds[Thread.currentThread().id] = Unit + provokeThreadSwitch() } }) - -class PersistentThreadLocal : ThreadLocal() { - - val map = ConcurrentHashMap() - - override fun set(value: T) { - super.set(value) - map[Thread.currentThread().id] = value - } -} diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/test/interceptors/CoroutineDispatcherInterceptorTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/test/interceptors/CoroutineDispatcherInterceptorTest.kt index 90062d74fb7..7382db012f9 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/test/interceptors/CoroutineDispatcherInterceptorTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/test/interceptors/CoroutineDispatcherInterceptorTest.kt @@ -12,9 +12,9 @@ import io.kotest.core.test.TestType import io.kotest.engine.test.interceptors.CoroutineDispatcherFactoryInterceptor import io.kotest.engine.test.scopes.NoopTestScope import io.kotest.matchers.string.shouldStartWith -import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.withContext -import java.util.concurrent.Executors import kotlin.time.Duration.Companion.milliseconds @ExperimentalStdlibApi @@ -32,16 +32,13 @@ class CoroutineDispatcherInterceptorTest : DescribeSpec() { ) val controller = object : CoroutineDispatcherFactory { - override suspend fun withDispatcher(testCase: TestCase, f: suspend () -> T): T { - val executor = Executors.newSingleThreadExecutor { - val t = Thread(it) - t.name = "foo" - t + @OptIn(DelicateCoroutinesApi::class) + override suspend fun withDispatcher(testCase: TestCase, f: suspend () -> T): T = + newSingleThreadContext("foo").use { dispatcher -> + withContext(dispatcher) { + f() + } } - return withContext(executor.asCoroutineDispatcher()) { - f() - } - } } CoroutineDispatcherFactoryInterceptor(controller).intercept( diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentInstancePerLeafTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentInstancePerLeafTest.kt index 0daed975bb5..da33558e080 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentInstancePerLeafTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentInstancePerLeafTest.kt @@ -3,7 +3,6 @@ package com.sksamuel.kotest.engine.threads import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @@ -22,7 +21,7 @@ class SpecThreadBeforeTestConcurrentInstancePerLeafTest : FunSpec({ if (isLockAcquired) { lock.lock() try { - delay(300) + Thread.sleep(300) } finally { lock.unlock() } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentInstancePerTestTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentInstancePerTestTest.kt index d9885b574da..a5486b32f73 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentInstancePerTestTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentInstancePerTestTest.kt @@ -3,7 +3,6 @@ package com.sksamuel.kotest.engine.threads import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @@ -21,7 +20,7 @@ class SpecThreadBeforeTestConcurrentInstancePerTestTest : FunSpec({ if (isLockAcquired) { lock.lock() try { - delay(300) + Thread.sleep(300) } finally { lock.unlock() } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentSingleInstanceTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentSingleInstanceTest.kt index bbf186a6ca4..d1a5b209a12 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentSingleInstanceTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/BeforeTestConcurrentSingleInstanceTest.kt @@ -3,7 +3,6 @@ package com.sksamuel.kotest.engine.threads import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @@ -22,7 +21,7 @@ class SpecThreadBeforeTestConcurrentSingleInstanceTest : FunSpec({ if (isLockAcquired) { lock.lock() try { - delay(300) + Thread.sleep(300) } finally { lock.unlock() } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentInstancePerLeafTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentInstancePerLeafTest.kt index fd77a71e6fa..3b098d02aa1 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentInstancePerLeafTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentInstancePerLeafTest.kt @@ -3,7 +3,6 @@ package com.sksamuel.kotest.engine.threads import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @@ -22,7 +21,7 @@ class SpecThreadTestCaseExtensionConcurrentInstancePerLeafTest : FunSpec({ if (isLockAcquired) { lock.lock() try { - delay(300) + Thread.sleep(300) } finally { lock.unlock() } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentInstancePerTestTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentInstancePerTestTest.kt index b8000260b58..b72e9e961b2 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentInstancePerTestTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentInstancePerTestTest.kt @@ -3,7 +3,6 @@ package com.sksamuel.kotest.engine.threads import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @@ -22,7 +21,7 @@ class TestCaseExtensionConcurrentInstancePerTestTest : FunSpec({ if (isLockAcquired) { lock.lock() try { - delay(300) + Thread.sleep(300) } finally { lock.unlock() } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentSingleInstanceTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentSingleInstanceTest.kt index 7ce179274b6..4268d2e4c7d 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentSingleInstanceTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/TestCaseExtensionConcurrentSingleInstanceTest.kt @@ -3,7 +3,6 @@ package com.sksamuel.kotest.engine.threads import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.locks.ReentrantLock @@ -22,7 +21,7 @@ class SpecThreadTestCaseExtensionConcurrentSingleInstanceTest : FunSpec({ if (isLockAcquired) { lock.lock() try { - delay(300) + Thread.sleep(300) } finally { lock.unlock() } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksInstancePerLeafTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksInstancePerLeafTest.kt index 6f3088c6617..37701582c50 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksInstancePerLeafTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksInstancePerLeafTest.kt @@ -5,7 +5,6 @@ import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantLock @@ -27,7 +26,7 @@ class WithLocksInstancePerLeafTest : FunSpec({ objects.add(lock) lock.lock() try { - delay(1000) + Thread.sleep(1000) } finally { lock.unlock() } @@ -36,13 +35,13 @@ class WithLocksInstancePerLeafTest : FunSpec({ test("lock should be unlocked because lock object is different") { objects.add(lock) - delay(300) + Thread.sleep(300) lock.isLocked shouldBe false } test("lock should be unlocked too") { objects.add(lock) - delay(300) + Thread.sleep(300) shouldThrow { lock.isLocked shouldBe true } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksInstancePerTestTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksInstancePerTestTest.kt index 2bc6fa3b969..21a5d550bec 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksInstancePerTestTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksInstancePerTestTest.kt @@ -5,7 +5,6 @@ import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantLock @@ -27,7 +26,7 @@ class SpecThreadInstancePerTestWithLockTest : FunSpec({ objects.add(lock) lock.lock() try { - delay(1000) + Thread.sleep(1000) } finally { lock.unlock() } @@ -36,13 +35,13 @@ class SpecThreadInstancePerTestWithLockTest : FunSpec({ test("lock should be unlocked because lock object is different") { objects.add(lock) - delay(300) + Thread.sleep(300) lock.isLocked shouldBe false } test("lock should be unlocked too") { objects.add(lock) - delay(300) + Thread.sleep(300) shouldThrow { lock.isLocked shouldBe true } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedInstancePerLeafTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedInstancePerLeafTest.kt index 83f6ee0f2e2..e4967fe88c6 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedInstancePerLeafTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedInstancePerLeafTest.kt @@ -4,7 +4,6 @@ import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantLock @@ -36,7 +35,7 @@ class NestedTestsWithLockInstancePerLeafTest : FunSpec({ innerLock.lock() outerContextLock.lock() try { - delay(1000) + Thread.sleep(1000) } finally { outerContextLock.unlock() innerLock.unlock() @@ -49,7 +48,7 @@ class NestedTestsWithLockInstancePerLeafTest : FunSpec({ locks.add(innerLock) locks.add(outerContextLock) - delay(300) + Thread.sleep(300) outerContextLock.isLocked shouldBe false innerLock.isLocked shouldBe false } @@ -68,7 +67,7 @@ class NestedTestsWithLockInstancePerLeafTest : FunSpec({ innerLock.lock() outerContextLock.lock() try { - delay(1000) + Thread.sleep(1000) } finally { outerContextLock.unlock() innerLock.unlock() @@ -80,7 +79,7 @@ class NestedTestsWithLockInstancePerLeafTest : FunSpec({ locks.add(innerLock) locks.add(outerContextLock) - delay(300) + Thread.sleep(300) outerContextLock.isLocked shouldBe false innerLock.isLocked shouldBe false } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedInstancePerTestTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedInstancePerTestTest.kt index b66b3de7696..cb08a9becab 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedInstancePerTestTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedInstancePerTestTest.kt @@ -4,7 +4,6 @@ import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.locks.ReentrantLock @@ -36,7 +35,7 @@ class SpecThreadWithNestedTestWithLockInstancePerTestTest : FunSpec({ innerLock.lock() outerContextLock.lock() try { - delay(1000) + Thread.sleep(1000) } finally { outerContextLock.unlock() innerLock.unlock() @@ -48,7 +47,7 @@ class SpecThreadWithNestedTestWithLockInstancePerTestTest : FunSpec({ locks.add(innerLock) locks.add(outerContextLock) - delay(300) + Thread.sleep(300) outerContextLock.isLocked shouldBe false innerLock.isLocked shouldBe false } @@ -67,7 +66,7 @@ class SpecThreadWithNestedTestWithLockInstancePerTestTest : FunSpec({ innerLock.lock() outerContextLock.lock() try { - delay(1000) + Thread.sleep(1000) } finally { outerContextLock.unlock() innerLock.unlock() @@ -79,7 +78,7 @@ class SpecThreadWithNestedTestWithLockInstancePerTestTest : FunSpec({ locks.add(innerLock) locks.add(outerContextLock) - delay(300) + Thread.sleep(300) outerContextLock.isLocked shouldBe false innerLock.isLocked shouldBe false } diff --git a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedSingleInstanceTest.kt b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedSingleInstanceTest.kt index c16b7521f26..27a9db3b83f 100644 --- a/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedSingleInstanceTest.kt +++ b/kotest-framework/kotest-framework-engine/src/jvmTest/kotlin/com/sksamuel/kotest/engine/threads/WithLocksNestedSingleInstanceTest.kt @@ -4,7 +4,6 @@ import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.IsolationMode import io.kotest.core.spec.style.FunSpec import io.kotest.matchers.shouldBe -import kotlinx.coroutines.delay import java.util.concurrent.locks.ReentrantLock class WithLocksNestedSingleInstanceTest : FunSpec({ @@ -18,17 +17,17 @@ class WithLocksNestedSingleInstanceTest : FunSpec({ test("test should lock object") { lock.lock() - delay(1000) + Thread.sleep(1000) lock.unlock() } test("lock should be unlocked") { - delay(300) + Thread.sleep(300) lock.isLocked shouldBe false } test("lock should be unlocked too") { - delay(300) + Thread.sleep(300) shouldThrow { lock.isLocked shouldBe true } diff --git a/kotest-property/src/jvmTest/kotlin/com/sksamuel/kotest/property/arbitrary/StringPatternTest.kt b/kotest-property/src/jvmTest/kotlin/com/sksamuel/kotest/property/arbitrary/StringPatternTest.kt index a6c1a174dfc..71766169f6c 100644 --- a/kotest-property/src/jvmTest/kotlin/com/sksamuel/kotest/property/arbitrary/StringPatternTest.kt +++ b/kotest-property/src/jvmTest/kotlin/com/sksamuel/kotest/property/arbitrary/StringPatternTest.kt @@ -5,10 +5,10 @@ import io.kotest.matchers.string.shouldMatch import io.kotest.property.Arb import io.kotest.property.arbitrary.stringPattern import io.kotest.property.arbitrary.take -import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll -import java.util.concurrent.Executors +import kotlinx.coroutines.newFixedThreadPoolContext class StringPatternTest : FunSpec({ @@ -24,11 +24,13 @@ class StringPatternTest : FunSpec({ test("should be quick") { val arbPattern = Arb.stringPattern("[a-zA-Z0-9]+") - val testDispatcher = Executors.newFixedThreadPool(10).asCoroutineDispatcher() - generateSequence { async(testDispatcher) { arbPattern.take(100000).last() } } - .take(10) - .toList() - .awaitAll() + @OptIn(DelicateCoroutinesApi::class) + newFixedThreadPoolContext(10, "pool").use { testDispatcher -> + generateSequence { async(testDispatcher) { arbPattern.take(100000).last() } } + .take(10) + .toList() + .awaitAll() + } } } })