resolve flaky jvm multi threaded coroutine tests

By switching to using the jvm only (hopefully by 1.6 in common) coroutine test lib we can swap to using a fake dispatcher instead of defining threads which should resolve any odd timing issues.
This commit is contained in:
Tristan 2021-06-15 19:26:27 +01:00 committed by Michael Bull
parent 08cf52bfd4
commit 07b4d61d3f
3 changed files with 25 additions and 20 deletions

View File

@ -3,6 +3,7 @@ object Versions {
const val kotlin = "1.5.10" const val kotlin = "1.5.10"
const val kotlinBenchmark = "0.2.0-dev-20" const val kotlinBenchmark = "0.2.0-dev-20"
const val kotlinCoroutines = "1.5.0" const val kotlinCoroutines = "1.5.0"
const val kotlinCoroutinesTest = "1.5.0"
const val ktor = "1.5.4" const val ktor = "1.5.4"
const val logback = "1.2.3" const val logback = "1.2.3"
const val versionsPlugin = "0.38.0" const val versionsPlugin = "0.38.0"

View File

@ -31,6 +31,7 @@ kotlin {
dependencies { dependencies {
implementation(kotlin("test-junit")) implementation(kotlin("test-junit"))
implementation(kotlin("test")) implementation(kotlin("test"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:${Versions.kotlinCoroutinesTest}")
} }
} }

View File

@ -3,19 +3,19 @@ package com.github.michaelbull.result.coroutines.binding
import com.github.michaelbull.result.Err import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result import com.github.michaelbull.result.Result
import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.concurrent.Executors import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.test.runBlockingTest
import kotlin.test.Test import kotlin.test.Test
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse import kotlin.test.assertFalse
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ExperimentalCoroutinesApi
class AsyncSuspendableBindingTest { class AsyncSuspendableBindingTest {
private sealed class BindingError { private sealed class BindingError {
@ -35,7 +35,7 @@ class AsyncSuspendableBindingTest {
return Ok(2) return Ok(2)
} }
runBlocking { runBlockingTest {
val result = binding<Int, BindingError> { val result = binding<Int, BindingError> {
val x = async { provideX().bind() } val x = async { provideX().bind() }
val y = async { provideY().bind() } val y = async { provideY().bind() }
@ -67,7 +67,7 @@ class AsyncSuspendableBindingTest {
return Err(BindingError.BindingErrorB) return Err(BindingError.BindingErrorB)
} }
runBlocking { runBlockingTest {
val result = binding<Int, BindingError> { val result = binding<Int, BindingError> {
val x = async { provideX().bind() } val x = async { provideX().bind() }
val y = async { provideY().bind() } val y = async { provideY().bind() }
@ -95,18 +95,19 @@ class AsyncSuspendableBindingTest {
} }
suspend fun provideY(): Result<Int, BindingError.BindingErrorB> { suspend fun provideY(): Result<Int, BindingError.BindingErrorB> {
// as this test uses a new thread for each coroutine, we want to set this delay to a high enough number that delay(3)
// there isn't any chance of a jvm run actually completing this suspending function in this thread first
// otherwise the assertions might fail.
delay(100)
yStateChange = true yStateChange = true
return Err(BindingError.BindingErrorB) return Err(BindingError.BindingErrorB)
} }
val dispatcherA = TestCoroutineDispatcher()
val dispatcherB = TestCoroutineDispatcher()
runBlocking { runBlocking {
val result = binding<Int, BindingError> { val result = binding<Int, BindingError> {
val x = async(newThread("ThreadA")) { provideX().bind() } val x = async(dispatcherA) { provideX().bind() }
val y = async(newThread("ThreadB")) { provideY().bind() } val y = async(dispatcherB) { provideY().bind() }
dispatcherA.advanceTimeBy(2)
x.await() + y.await() x.await() + y.await()
} }
@ -144,11 +145,17 @@ class AsyncSuspendableBindingTest {
return Err(BindingError.BindingErrorB) return Err(BindingError.BindingErrorB)
} }
val dispatcherA = TestCoroutineDispatcher()
val dispatcherB = TestCoroutineDispatcher()
val dispatcherC = TestCoroutineDispatcher()
runBlocking { runBlocking {
val result = binding<Unit, BindingError> { val result = binding<Unit, BindingError> {
launch(newThread("Thread A")) { provideX().bind() } launch(dispatcherA) { provideX().bind() }
launch(newThread("Thread B")) { provideY().bind() } dispatcherA.advanceTimeBy(20)
launch(newThread("Thread C")) { provideZ().bind() } launch(dispatcherB) { provideY().bind() }
dispatcherB.advanceTimeBy(20)
launch(dispatcherC) { provideZ().bind() }
} }
assertTrue(result is Err) assertTrue(result is Err)
@ -161,8 +168,4 @@ class AsyncSuspendableBindingTest {
assertFalse(zStateChange) assertFalse(zStateChange)
} }
} }
private fun newThread(name: String): CoroutineContext {
return Executors.newSingleThreadExecutor().asCoroutineDispatcher() + CoroutineName(name)
}
} }