From 0a21aa95c0c8949ea6a51124c389e6708d9b1bf1 Mon Sep 17 00:00:00 2001 From: Alejandro Serrano Mena Date: Wed, 8 Nov 2023 21:21:19 +0100 Subject: [PATCH] Finish transition to `kotlin.test` of `retrofit` and `fx-coroutines` --- .../core/arrow-core-retrofit/build.gradle.kts | 2 - .../NetworkEitherCallAdapterTest.kt | 133 ++++++++-- .../fx/arrow-fx-coroutines/build.gradle.kts | 3 - .../fx/coroutines/DeprecationWarnings.kt | 4 - .../kotlin/arrow/fx/coroutines/Resource.kt | 2 - .../kotlin/arrow/fx/coroutines/FlowTest.kt | 2 - .../arrow/fx/coroutines/KotestConfig.kt | 10 - .../kotlin/arrow/fx/coroutines/ParZip2Test.kt | 8 +- .../kotlin/arrow/fx/coroutines/ParZip3Test.kt | 2 +- .../kotlin/arrow/fx/coroutines/ParZip4Test.kt | 2 +- .../kotlin/arrow/fx/coroutines/ParZip5Test.kt | 2 +- .../kotlin/arrow/fx/coroutines/ParZip6Test.kt | 2 +- .../kotlin/arrow/fx/coroutines/ParZip7Test.kt | 2 +- .../kotlin/arrow/fx/coroutines/ParZip8Test.kt | 2 +- .../kotlin/arrow/fx/coroutines/ParZip9Test.kt | 2 +- .../kotlin/arrow/fx/coroutines/RaceNTest.kt | 240 +++++++++--------- .../arrow/fx/coroutines/ResourceTest.kt | 89 ++++--- .../kotlin/arrow/fx/coroutines/FlowJvmTest.kt | 2 - 18 files changed, 294 insertions(+), 215 deletions(-) delete mode 100644 arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/DeprecationWarnings.kt delete mode 100644 arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/KotestConfig.kt diff --git a/arrow-libs/core/arrow-core-retrofit/build.gradle.kts b/arrow-libs/core/arrow-core-retrofit/build.gradle.kts index 8a1af47a255..7dc88f6b054 100644 --- a/arrow-libs/core/arrow-core-retrofit/build.gradle.kts +++ b/arrow-libs/core/arrow-core-retrofit/build.gradle.kts @@ -25,11 +25,9 @@ dependencies { testImplementation(projects.arrowCore) testImplementation(libs.kotlin.test) testImplementation(libs.coroutines.test) - testImplementation(libs.kotest.frameworkEngine) testImplementation(libs.kotest.assertionsCore) testImplementation(libs.kotest.property) testCompileOnly(libs.kotlin.reflect) - testRuntimeOnly(libs.kotest.runnerJUnit5) testImplementation(libs.squareup.okhttpMockWebServer) testImplementation(libs.squareup.retrofitConverterGson) testImplementation(libs.squareup.retrofitConverterMoshi) diff --git a/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/networkhandling/NetworkEitherCallAdapterTest.kt b/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/networkhandling/NetworkEitherCallAdapterTest.kt index 5f211a1c195..6ef4a2de486 100644 --- a/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/networkhandling/NetworkEitherCallAdapterTest.kt +++ b/arrow-libs/core/arrow-core-retrofit/src/test/kotlin/arrow/retrofit/adapter/either/networkhandling/NetworkEitherCallAdapterTest.kt @@ -6,11 +6,9 @@ import arrow.core.right import arrow.retrofit.adapter.either.EitherCallAdapterFactory import arrow.retrofit.adapter.mock.ResponseMock import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory -import io.kotest.core.spec.style.StringSpec -import io.kotest.core.spec.style.stringSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf -import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient @@ -24,21 +22,17 @@ import retrofit2.converter.moshi.MoshiConverterFactory import java.net.SocketException import java.net.SocketTimeoutException import java.util.concurrent.TimeUnit +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test -@ExperimentalSerializationApi -class NetworkEitherCallAdapterTestSuite : StringSpec({ - include(networkEitherCallAdapterTests(GsonConverterFactory.create())) - include(networkEitherCallAdapterTests(MoshiConverterFactory.create())) - include(networkEitherCallAdapterTests(Json.asConverterFactory("application/json".toMediaType()))) -}) +abstract class NetworkEitherCallAdapterTest( + private val jsonConverterFactory: Converter.Factory, +) { + private var server: MockWebServer? = null + private var service: CallErrorTestClient? = null -private fun networkEitherCallAdapterTests( - jsonConverterFactory: Converter.Factory, -) = stringSpec { - var server: MockWebServer? = null - var service: CallErrorTestClient? = null - - beforeAny { + open fun initialize() { server = MockWebServer() server!!.start() val client = OkHttpClient.Builder() @@ -52,9 +46,12 @@ private fun networkEitherCallAdapterTests( .build() .create(CallErrorTestClient::class.java) } - afterAny { server!!.shutdown() } - "should return ResponseMock for 200 with valid JSON" { + open fun shutdown() { + server!!.shutdown() + } + + open fun shouldReturn200ForValidJson() = runTest { server!!.enqueue(MockResponse().setBody("""{"response":"Arrow rocks"}""")) val body = service!!.getEither() @@ -62,7 +59,7 @@ private fun networkEitherCallAdapterTests( body shouldBe ResponseMock("Arrow rocks").right() } - "should return HttpError for 400" { + open fun shouldReturnHttpErrorFor404() = runTest { server!!.enqueue(MockResponse().setBody("""{"errorCode":666}""").setResponseCode(400)) val body = service!!.getEither() @@ -74,7 +71,7 @@ private fun networkEitherCallAdapterTests( ).left() } - "should return CallError for 200 with invalid JSON" { + open fun shouldReturnCallErrorFor200InvalidJson() = runTest { server!!.enqueue(MockResponse().setBody("""not a valid JSON""")) val body = service!!.getEither() @@ -83,7 +80,7 @@ private fun networkEitherCallAdapterTests( .value.shouldBeInstanceOf() } - "should return HttpError for 400 and invalid JSON" { + open fun shouldReturnHttpErrorFor404InvalidJson() = runTest { server!!.enqueue(MockResponse().setBody("""not a valid JSON""").setResponseCode(400)) val body = service!!.getEither() @@ -95,7 +92,7 @@ private fun networkEitherCallAdapterTests( ).left() } - "should return IOError when server disconnects" { + open fun shouldReturnIOErrorDisconnect() = runTest { server!!.enqueue(MockResponse().apply { socketPolicy = SocketPolicy.DISCONNECT_AT_START }) val body = service!!.getEither() @@ -105,7 +102,7 @@ private fun networkEitherCallAdapterTests( .cause.shouldBeInstanceOf() } - "should return IOError when no response" { + open fun shouldReturnIOErrorNoResponse() = runTest { server!!.enqueue(MockResponse().apply { socketPolicy = SocketPolicy.NO_RESPONSE }) val body = service!!.getEither() @@ -115,7 +112,7 @@ private fun networkEitherCallAdapterTests( .cause.shouldBeInstanceOf() } - "should return Unit when service method returns Unit and null body received" { + open fun shouldReturnUnitForNullBody() = runTest { server!!.enqueue(MockResponse().setResponseCode(204)) val body = service!!.postSomething("Sample string") @@ -123,7 +120,7 @@ private fun networkEitherCallAdapterTests( body shouldBe Unit.right() } - "should return Unit when service method returns Unit and JSON body received" { + open fun shouldReturnUnitForUnitBody() = runTest { server!!.enqueue(MockResponse().setBody("""{"response":"Arrow rocks"}""")) val body = service!!.postSomething("Sample string") @@ -131,7 +128,7 @@ private fun networkEitherCallAdapterTests( body shouldBe Unit.right() } - "should return CallError when service method returns type other than Unit but null body received" { + open fun shouldReturnCallErrorWithUnitForNonNullBody() = runTest { server!!.enqueue(MockResponse()) val body = service!!.getEither() @@ -140,3 +137,87 @@ private fun networkEitherCallAdapterTests( .value.shouldBeInstanceOf() } } + +class NetworkEitherCallAdapterTestGson : NetworkEitherCallAdapterTest(GsonConverterFactory.create()) { + @BeforeTest override fun initialize() { + super.initialize() + } + + @AfterTest override fun shutdown() { + super.shutdown() + } + + @Test override fun shouldReturn200ForValidJson() = super.shouldReturn200ForValidJson() + + @Test override fun shouldReturnHttpErrorFor404() = super.shouldReturnHttpErrorFor404() + + @Test override fun shouldReturnCallErrorFor200InvalidJson() = super.shouldReturnCallErrorFor200InvalidJson() + + @Test override fun shouldReturnHttpErrorFor404InvalidJson() = super.shouldReturnHttpErrorFor404InvalidJson() + + @Test override fun shouldReturnIOErrorDisconnect() = super.shouldReturnIOErrorDisconnect() + + @Test override fun shouldReturnIOErrorNoResponse() = super.shouldReturnIOErrorNoResponse() + + @Test override fun shouldReturnUnitForNullBody() = super.shouldReturnUnitForNullBody() + + @Test override fun shouldReturnUnitForUnitBody() = super.shouldReturnUnitForUnitBody() + + @Test override fun shouldReturnCallErrorWithUnitForNonNullBody() = super.shouldReturnCallErrorWithUnitForNonNullBody() +} + +class NetworkEitherCallAdapterTestMoshi : NetworkEitherCallAdapterTest(MoshiConverterFactory.create()) { + @BeforeTest override fun initialize() { + super.initialize() + } + + @AfterTest override fun shutdown() { + super.shutdown() + } + + @Test override fun shouldReturn200ForValidJson() = super.shouldReturn200ForValidJson() + + @Test override fun shouldReturnHttpErrorFor404() = super.shouldReturnHttpErrorFor404() + + @Test override fun shouldReturnCallErrorFor200InvalidJson() = super.shouldReturnCallErrorFor200InvalidJson() + + @Test override fun shouldReturnHttpErrorFor404InvalidJson() = super.shouldReturnHttpErrorFor404InvalidJson() + + @Test override fun shouldReturnIOErrorDisconnect() = super.shouldReturnIOErrorDisconnect() + + @Test override fun shouldReturnIOErrorNoResponse() = super.shouldReturnIOErrorNoResponse() + + @Test override fun shouldReturnUnitForNullBody() = super.shouldReturnUnitForNullBody() + + @Test override fun shouldReturnUnitForUnitBody() = super.shouldReturnUnitForUnitBody() + + @Test override fun shouldReturnCallErrorWithUnitForNonNullBody() = super.shouldReturnCallErrorWithUnitForNonNullBody() +} + +class NetworkEitherCallAdapterTestKotlinxSerialization : NetworkEitherCallAdapterTest(Json.asConverterFactory("application/json".toMediaType())) { + @BeforeTest override fun initialize() { + super.initialize() + } + + @AfterTest override fun shutdown() { + super.shutdown() + } + + @Test override fun shouldReturn200ForValidJson() = super.shouldReturn200ForValidJson() + + @Test override fun shouldReturnHttpErrorFor404() = super.shouldReturnHttpErrorFor404() + + @Test override fun shouldReturnCallErrorFor200InvalidJson() = super.shouldReturnCallErrorFor200InvalidJson() + + @Test override fun shouldReturnHttpErrorFor404InvalidJson() = super.shouldReturnHttpErrorFor404InvalidJson() + + @Test override fun shouldReturnIOErrorDisconnect() = super.shouldReturnIOErrorDisconnect() + + @Test override fun shouldReturnIOErrorNoResponse() = super.shouldReturnIOErrorNoResponse() + + @Test override fun shouldReturnUnitForNullBody() = super.shouldReturnUnitForNullBody() + + @Test override fun shouldReturnUnitForUnitBody() = super.shouldReturnUnitForUnitBody() + + @Test override fun shouldReturnCallErrorWithUnitForNonNullBody() = super.shouldReturnCallErrorWithUnitForNonNullBody() +} diff --git a/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts b/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts index 884445b8ca9..fdcf04558c3 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts +++ b/arrow-libs/fx/arrow-fx-coroutines/build.gradle.kts @@ -14,8 +14,6 @@ spotless { } } -apply(plugin = "io.kotest.multiplatform") - kotlin { sourceSets { commonMain { @@ -31,7 +29,6 @@ kotlin { implementation(projects.arrowCore) implementation(libs.kotlin.test) implementation(libs.coroutines.test) - implementation(libs.kotest.frameworkEngine) implementation(libs.kotest.assertionsCore) implementation(libs.kotest.property) } diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/DeprecationWarnings.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/DeprecationWarnings.kt deleted file mode 100644 index 562367c2cd6..00000000000 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/DeprecationWarnings.kt +++ /dev/null @@ -1,4 +0,0 @@ -package arrow.fx.coroutines - -internal const val deprecatedInFavorOfArrowFxResilience = - "This is now part of the arrow-fx-resilience module, and will be removed from arrow-fx-coroutines in version 2.0. Please update your project dependencies." diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt index 3626d8d5934..e8a745cfa0d 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonMain/kotlin/arrow/fx/coroutines/Resource.kt @@ -7,7 +7,6 @@ import arrow.core.identity import arrow.core.prependTo import arrow.fx.coroutines.ExitCase.Companion.ExitCase import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow @@ -466,7 +465,6 @@ public fun Resource.asFlow(): Flow = * instead the caller is responsible for correctly invoking the `release` handler at the appropriate time. * This API is useful for building inter-op APIs between [Resource] and non-suspending code, such as Java libraries. */ -@DelicateCoroutinesApi public suspend fun Resource.allocated(): Pair Unit> { val effect = ResourceScopeImpl() val allocated: A = invoke(effect) diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/FlowTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/FlowTest.kt index e79d057391d..67e1a65aa97 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/FlowTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/FlowTest.kt @@ -27,10 +27,8 @@ import kotlinx.coroutines.test.currentTime import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeoutOrNull import kotlin.test.Test -import kotlin.time.ExperimentalTime @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) -@ExperimentalTime class FlowTest { @Test diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/KotestConfig.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/KotestConfig.kt deleted file mode 100644 index e2c196cdbed..00000000000 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/KotestConfig.kt +++ /dev/null @@ -1,10 +0,0 @@ -package arrow.fx.coroutines - -import io.kotest.core.config.AbstractProjectConfig -import io.kotest.property.PropertyTesting - -class KotestConfig : AbstractProjectConfig() { - override suspend fun beforeProject() { - PropertyTesting.defaultIterationCount = 250 - } -} diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip2Test.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip2Test.kt index 33b2b0f31e9..6955fe0e5e3 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip2Test.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip2Test.kt @@ -1,15 +1,9 @@ -package arrow.fx.coroutines.parZip +package arrow.fx.coroutines import arrow.atomic.AtomicInt import arrow.atomic.update import arrow.atomic.value import arrow.core.Either -import arrow.fx.coroutines.ExitCase -import arrow.fx.coroutines.awaitExitCase -import arrow.fx.coroutines.guaranteeCase -import arrow.fx.coroutines.leftException -import arrow.fx.coroutines.parZip -import arrow.fx.coroutines.throwable import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeTypeOf diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip3Test.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip3Test.kt index 1e2d05ff4c0..499dcfe88b7 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip3Test.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip3Test.kt @@ -1,4 +1,4 @@ -package arrow.fx.coroutines.parZip +package arrow.fx.coroutines import arrow.atomic.Atomic import arrow.atomic.update diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip4Test.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip4Test.kt index f3bd76a59e5..57e3bd5753b 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip4Test.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip4Test.kt @@ -1,4 +1,4 @@ -package arrow.fx.coroutines.parZip +package arrow.fx.coroutines import arrow.atomic.Atomic import arrow.atomic.update diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip5Test.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip5Test.kt index 8af58f145c2..3380a069a63 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip5Test.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip5Test.kt @@ -1,4 +1,4 @@ -package arrow.fx.coroutines.parZip +package arrow.fx.coroutines import arrow.atomic.Atomic import arrow.atomic.update diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip6Test.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip6Test.kt index 77eebeed8a9..cbf6ad66b5a 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip6Test.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip6Test.kt @@ -1,4 +1,4 @@ -package arrow.fx.coroutines.parZip +package arrow.fx.coroutines import arrow.atomic.Atomic import arrow.atomic.update diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip7Test.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip7Test.kt index 3c4be417dd5..5e19e5aa6fe 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip7Test.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip7Test.kt @@ -1,4 +1,4 @@ -package arrow.fx.coroutines.parZip +package arrow.fx.coroutines import arrow.atomic.Atomic import arrow.atomic.update diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip8Test.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip8Test.kt index b92c6677542..97511c4e3e9 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip8Test.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip8Test.kt @@ -1,4 +1,4 @@ -package arrow.fx.coroutines.parZip +package arrow.fx.coroutines import arrow.atomic.Atomic import arrow.atomic.update diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip9Test.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip9Test.kt index 8be40bbd564..448204f2c71 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip9Test.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ParZip9Test.kt @@ -1,4 +1,4 @@ -package arrow.fx.coroutines.parZip +package arrow.fx.coroutines import arrow.atomic.Atomic import arrow.atomic.update diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/RaceNTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/RaceNTest.kt index 8a1e9e3e3e0..7297f0b5faa 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/RaceNTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/RaceNTest.kt @@ -4,7 +4,6 @@ import arrow.core.Either import arrow.core.identity import arrow.core.merge import io.kotest.assertions.retry -import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.should import io.kotest.matchers.shouldBe import io.kotest.matchers.types.shouldBeInstanceOf @@ -18,154 +17,117 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.test.runTest +import kotlin.test.Test import kotlin.time.Duration.Companion.seconds fun Either.rethrow(): A = fold({ throw it }, ::identity) -class RaceNTest : StringSpec({ - "race2 can join first" { - checkAll(Arb.int()) { i -> - raceN({ i }, { awaitCancellation() }) shouldBe Either.Left(i) - } +class RaceNTest { + @Test fun race2JoinFirst() = runTest { + checkAll(Arb.int()) { i -> + raceN({ i }, { awaitCancellation() }) shouldBe Either.Left(i) } + } - "race2 can join second" { - checkAll(Arb.int()) { i -> - raceN({ awaitCancellation() }, { i }) shouldBe Either.Right(i) - } + @Test fun race2JoinSecond() = runTest { + checkAll(Arb.int()) { i -> + raceN({ awaitCancellation() }, { i }) shouldBe Either.Right(i) } + } - "Cancelling race 2 cancels all participants" { - checkAll(Arb.int(), Arb.int()) { a, b -> - val s = Channel() - val pa = CompletableDeferred>() - val pb = CompletableDeferred>() + @Test fun race2CancelsAll() = runTest { + checkAll(Arb.int(), Arb.int()) { a, b -> + val s = Channel() + val pa = CompletableDeferred>() + val pb = CompletableDeferred>() - val loserA: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pa.complete(Pair(a, ex)) } } - val loserB: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pb.complete(Pair(b, ex)) } } + val loserA: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pa.complete(Pair(a, ex)) } } + val loserB: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pb.complete(Pair(b, ex)) } } - val f = async { raceN(loserA, loserB) } + val f = async { raceN(loserA, loserB) } - // Suspend until all racers started - s.send(Unit) - s.send(Unit) - f.cancel() + // Suspend until all racers started + s.send(Unit) + s.send(Unit) + f.cancel() - pa.await().let { (res, exit) -> - res shouldBe a - exit.shouldBeInstanceOf() - } - pb.await().let { (res, exit) -> - res shouldBe b - exit.shouldBeInstanceOf() - } + pa.await().let { (res, exit) -> + res shouldBe a + exit.shouldBeInstanceOf() } - } - - "race 2 cancels losers with first success or failure determining winner" { - checkAll( - Arb.either(Arb.throwable(), Arb.int()), - Arb.boolean(), - Arb.int() - ) { eith, leftWinner, a -> - val s = Channel() - val pa = CompletableDeferred>() - - val winner: suspend CoroutineScope.() -> Int = { s.send(Unit); eith.rethrow() } - val loserA: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pa.complete(Pair(a, ex)) } } - - val res = Either.catch { - if (leftWinner) raceN(winner, loserA) - else raceN(loserA, winner) - }.map { it.merge() } - - pa.await().let { (res, exit) -> - res shouldBe a - exit.shouldBeInstanceOf() - } - res shouldBe either(eith) + pb.await().let { (res, exit) -> + res shouldBe b + exit.shouldBeInstanceOf() } } + } - "race3 can join first" { - checkAll(Arb.int()) { i -> - raceN({ i }, { awaitCancellation() }, { awaitCancellation() }) shouldBe Race3.First(i) + @Test fun race2CancelsWithFirstSuccess() = runTest { + checkAll( + Arb.either(Arb.throwable(), Arb.int()), + Arb.boolean(), + Arb.int() + ) { eith, leftWinner, a -> + val s = Channel() + val pa = CompletableDeferred>() + + val winner: suspend CoroutineScope.() -> Int = { s.send(Unit); eith.rethrow() } + val loserA: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pa.complete(Pair(a, ex)) } } + + val res = Either.catch { + if (leftWinner) raceN(winner, loserA) + else raceN(loserA, winner) + }.map { it.merge() } + + pa.await().let { (res, exit) -> + res shouldBe a + exit.shouldBeInstanceOf() } + res shouldBe either(eith) } + } - "race3 can join second" { - checkAll(Arb.int()) { i -> - raceN({ awaitCancellation() }, { i }, { awaitCancellation() }) shouldBe Race3.Second(i) - } + @Test fun race3JoinFirst() = runTest { + checkAll(Arb.int()) { i -> + raceN({ i }, { awaitCancellation() }, { awaitCancellation() }) shouldBe Race3.First(i) } + } - "race3 can join third" { - checkAll(Arb.int()) { i -> - raceN({ awaitCancellation() }, { awaitCancellation() }, { i }) shouldBe Race3.Third(i) - } + @Test fun race3JoinSecond() = runTest { + checkAll(Arb.int()) { i -> + raceN({ awaitCancellation() }, { i }, { awaitCancellation() }) shouldBe Race3.Second(i) } + } - "Cancelling race 3 cancels all participants" { - retry(10, 1.seconds) { - checkAll(Arb.int(), Arb.int(), Arb.int()) { a, b, c -> - val s = Channel() - val pa = CompletableDeferred>() - val pb = CompletableDeferred>() - val pc = CompletableDeferred>() - - val loserA: suspend CoroutineScope.() -> Int = - { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pa.complete(Pair(a, ex)) } } - val loserB: suspend CoroutineScope.() -> Int = - { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pb.complete(Pair(b, ex)) } } - val loserC: suspend CoroutineScope.() -> Int = - { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pc.complete(Pair(c, ex)) } } - - val f = async { raceN(loserA, loserB, loserC) } - - s.send(Unit) // Suspend until all racers started - s.send(Unit) - s.send(Unit) - f.cancel() - - pa.await().let { (res, exit) -> - res shouldBe a - exit.shouldBeInstanceOf() - } - pb.await().let { (res, exit) -> - res shouldBe b - exit.shouldBeInstanceOf() - } - pc.await().let { (res, exit) -> - res shouldBe c - exit.shouldBeInstanceOf() - } - } - } + @Test fun race3JoinThird() = runTest { + checkAll(Arb.int()) { i -> + raceN({ awaitCancellation() }, { awaitCancellation() }, { i }) shouldBe Race3.Third(i) } + } - "race 3 cancels losers with first success or failure determining winner" { - checkAll( - Arb.either(Arb.throwable(), Arb.int()), - Arb.element(listOf(1, 2, 3)), - Arb.int(), - Arb.int() - ) { eith, leftWinner, a, b -> + @Test fun race3CancelsAll() = runTest { + retry(10, 1.seconds) { + checkAll(Arb.int(), Arb.int(), Arb.int()) { a, b, c -> val s = Channel() val pa = CompletableDeferred>() val pb = CompletableDeferred>() + val pc = CompletableDeferred>() - val winner: suspend CoroutineScope.() -> Int = { s.send(Unit); s.send(Unit); eith.rethrow() } - val loserA: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pa.complete(Pair(a, ex)) } } - val loserB: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pb.complete(Pair(b, ex)) } } + val loserA: suspend CoroutineScope.() -> Int = + { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pa.complete(Pair(a, ex)) } } + val loserB: suspend CoroutineScope.() -> Int = + { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pb.complete(Pair(b, ex)) } } + val loserC: suspend CoroutineScope.() -> Int = + { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pc.complete(Pair(c, ex)) } } - val res = Either.catch { - when (leftWinner) { - 1 -> raceN(winner, loserA, loserB) - 2 -> raceN(loserA, winner, loserB) - else -> raceN(loserA, loserB, winner) - } - }.map { it.fold(::identity, ::identity, ::identity) } + val f = async { raceN(loserA, loserB, loserC) } + + s.send(Unit) // Suspend until all racers started + s.send(Unit) + s.send(Unit) + f.cancel() pa.await().let { (res, exit) -> res shouldBe a @@ -175,8 +137,46 @@ class RaceNTest : StringSpec({ res shouldBe b exit.shouldBeInstanceOf() } - res should either(eith) + pc.await().let { (res, exit) -> + res shouldBe c + exit.shouldBeInstanceOf() + } + } + } + } + + @Test fun race3CancelsWithFirstSuccess() = runTest { + checkAll( + Arb.either(Arb.throwable(), Arb.int()), + Arb.element(listOf(1, 2, 3)), + Arb.int(), + Arb.int() + ) { eith, leftWinner, a, b -> + val s = Channel() + val pa = CompletableDeferred>() + val pb = CompletableDeferred>() + + val winner: suspend CoroutineScope.() -> Int = { s.send(Unit); s.send(Unit); eith.rethrow() } + val loserA: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pa.complete(Pair(a, ex)) } } + val loserB: suspend CoroutineScope.() -> Int = { guaranteeCase({ s.receive(); awaitCancellation() }) { ex -> pb.complete(Pair(b, ex)) } } + + val res = Either.catch { + when (leftWinner) { + 1 -> raceN(winner, loserA, loserB) + 2 -> raceN(loserA, winner, loserB) + else -> raceN(loserA, loserB, winner) + } + }.map { it.fold(::identity, ::identity, ::identity) } + + pa.await().let { (res, exit) -> + res shouldBe a + exit.shouldBeInstanceOf() + } + pb.await().let { (res, exit) -> + res shouldBe b + exit.shouldBeInstanceOf() } + res should either(eith) } } -) +} diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt index cb1e08161f6..5e03d7367ed 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/commonTest/kotlin/arrow/fx/coroutines/ResourceTest.kt @@ -6,7 +6,6 @@ import arrow.core.raise.either import arrow.fx.coroutines.ExitCase.Companion.ExitCase import io.kotest.assertions.fail import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.should @@ -24,14 +23,18 @@ import io.kotest.property.checkAll import io.kotest.property.arbitrary.string import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.async import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.test.runTest +import kotlin.test.Test -class ResourceTest : StringSpec({ +class ResourceTest { - "acquire - success - identity" { + @Test + fun acquireSuccessIdentity() = runTest { checkAll(Arb.int()) { n -> resourceScope { install({ n }) { _, _ -> } shouldBe n @@ -39,7 +42,8 @@ class ResourceTest : StringSpec({ } } - "respect FIFO order installed release functions" { + @Test + fun respectFIFOOrderInstalledFunction() = runTest { checkAll(Arb.positiveInt(), Arb.negativeInt()) { a, b -> val order = mutableListOf() @@ -56,7 +60,8 @@ class ResourceTest : StringSpec({ } } - "value resource is released with Complete" { + @Test + fun resourceReleasedWithComplete() = runTest { checkAll(Arb.int()) { n -> val p = CompletableDeferred() resourceScope { @@ -66,7 +71,8 @@ class ResourceTest : StringSpec({ } } - "error resource finishes with error" { + @Test + fun errorFinishesWithError() = runTest { checkAll(Arb.throwable()) { e -> val p = CompletableDeferred() suspend fun ResourceScope.failingScope(): Nothing = @@ -78,7 +84,8 @@ class ResourceTest : StringSpec({ } } - "never use can be cancelled with ExitCase.Completed" { + @Test + fun neverCancelled() = runTest { checkAll(Arb.int()) { n -> val p = CompletableDeferred() val start = CompletableDeferred() @@ -98,7 +105,8 @@ class ResourceTest : StringSpec({ } } - "Map + bind (traverse)" { + @Test + fun mapBind() = runTest { checkAll( Arb.list(Arb.int()), Arb.functionAToB(Arb.string()) @@ -111,7 +119,8 @@ class ResourceTest : StringSpec({ } } - "Resource can close from either" { + @Test + fun resourceCloseFromEither() = runTest { val exit = CompletableDeferred() either { resourceScope { @@ -133,7 +142,8 @@ class ResourceTest : StringSpec({ } } - "parZip - success" { + @Test + fun parZipSuccess() = runTest { suspend fun ResourceScope.closeable(): CheckableAutoClose = install({ CheckableAutoClose() }) { a: CheckableAutoClose, _: ExitCase -> a.close() } @@ -159,7 +169,8 @@ class ResourceTest : StringSpec({ return Pair(promises.map { it.second }, res) } - "parZip - deep finalizers are called when final one blows" { + @Test + fun parZipFinalizersBlow() = runTest { checkAll(3, Arb.int(10..100)) { val (promises, resource) = generate() shouldThrow { @@ -178,7 +189,8 @@ class ResourceTest : StringSpec({ } } - "parZip - deep finalizers are called when final one cancels" { + @Test + fun parZipFinalizersCancel() = runTest { checkAll(3, Arb.int(10..100)) { val cancel = CancellationException(null, null) val (promises, resource) = generate() @@ -199,7 +211,8 @@ class ResourceTest : StringSpec({ } // Test multiple release triggers on acquire fail. - "parZip - Deep finalizers get called on left or right cancellation" { + @Test + fun parZipFinalizersLeftOrRightCancellation() = runTest { checkAll(Arb.boolean()) { isLeft -> val cancel = CancellationException(null, null) val (promises, resource) = generate() @@ -233,7 +246,8 @@ class ResourceTest : StringSpec({ } } - "parZip - Right CancellationException on acquire" { + @Test + fun parZipRightCancellationExceptionOnAcquire() = runTest { checkAll(Arb.int()) { i -> val cancel = CancellationException(null, null) val released = CompletableDeferred>() @@ -261,7 +275,8 @@ class ResourceTest : StringSpec({ } } - "parZip - Left CancellationException on acquire" { + @Test + fun parZipLeftCancellationExceptionOnAcquire() = runTest { checkAll(Arb.int()) { i -> val cancel = CancellationException(null, null) val released = CompletableDeferred>() @@ -290,7 +305,8 @@ class ResourceTest : StringSpec({ } } - "parZip - Right error on acquire" { + @Test + fun parZipRightErrorOnAcquire() = runTest { checkAll(Arb.int(), Arb.throwable()) { i, throwable -> val released = CompletableDeferred>() val started = CompletableDeferred() @@ -317,7 +333,8 @@ class ResourceTest : StringSpec({ } } - "parZip - Left error on acquire" { + @Test + fun parZipLeftErrorOnAcquire() = runTest { checkAll(Arb.int(), Arb.throwable()) { i, throwable -> val released = CompletableDeferred>() val started = CompletableDeferred() @@ -343,7 +360,8 @@ class ResourceTest : StringSpec({ } } - "parZip - Right CancellationException on release" { + @Test + fun parZipRightCancellationExceptionOnRelease() = runTest { checkAll(Arb.int()) { i -> val cancel = CancellationException(null, null) val released = CompletableDeferred>() @@ -364,7 +382,8 @@ class ResourceTest : StringSpec({ } } - "parZip - Left CancellationException on release" { + @Test + fun parZipLeftCancellationExceptionOnRelease() = runTest { checkAll(Arb.int()) { i -> val cancel = CancellationException(null, null) val released = CompletableDeferred>() @@ -385,7 +404,8 @@ class ResourceTest : StringSpec({ } } - "parZip - Right error on release" { + @Test + fun parZipRightErrorOnRelease() = runTest { checkAll(Arb.int(), Arb.throwable()) { i, throwable -> val released = CompletableDeferred>() @@ -405,7 +425,8 @@ class ResourceTest : StringSpec({ } } - "parZip - Left error on release" { + @Test + fun parZipLeftErrorOnRelease() = runTest { checkAll(Arb.int(), Arb.throwable()) { i, throwable -> val released = CompletableDeferred>() @@ -425,7 +446,8 @@ class ResourceTest : StringSpec({ } } - "parZip - error in use" { + @Test + fun parZipErrorInUse() = runTest { checkAll(Arb.int(), Arb.int(), Arb.throwable()) { a, b, throwable -> val releasedA = CompletableDeferred>() val releasedB = CompletableDeferred>() @@ -451,7 +473,8 @@ class ResourceTest : StringSpec({ } } - "parZip - cancellation in use" { + @Test + fun parZipCancellationInUse() = runTest { checkAll(Arb.int(), Arb.int()) { a, b -> val releasedA = CompletableDeferred>() val releasedB = CompletableDeferred>() @@ -477,7 +500,8 @@ class ResourceTest : StringSpec({ } } - "resource.asFlow()" { + @Test + fun resourceAsFlow() = runTest { checkAll(Arb.int()) { n -> val released = CompletableDeferred() val r = resource({ n }, { _, ex -> require(released.complete(ex)) }) @@ -488,7 +512,8 @@ class ResourceTest : StringSpec({ } } - "resource.asFlow() - failed" { + @Test + fun resourceAsFlowFail() = runTest { checkAll(Arb.int(), Arb.throwable()) { n, throwable -> val released = CompletableDeferred() val r = resource({ n }, { _, ex -> require(released.complete(ex)) }) @@ -501,7 +526,8 @@ class ResourceTest : StringSpec({ } } - "resource.asFlow() - cancelled" { + @Test + fun resourceAsFlowCancel() = runTest { checkAll(Arb.int()) { n -> val released = CompletableDeferred() val r = resource({ n }, { _, ex -> require(released.complete(ex)) }) @@ -514,7 +540,8 @@ class ResourceTest : StringSpec({ } } - "allocated" { + @Test + fun allocatedWorks() = runTest { checkAll(Arb.int()) { seed -> val released = CompletableDeferred() val (allocate, release) = resource({ seed }) { _, exitCase -> released.complete(exitCase) } @@ -526,7 +553,8 @@ class ResourceTest : StringSpec({ } } - "allocated - suppressed exception" { + @Test + fun allocatedSupressedException() = runTest { checkAll( Arb.int(), Arb.string().map(::RuntimeException), @@ -554,7 +582,8 @@ class ResourceTest : StringSpec({ } } - "allocated - cancellation exception" { + @Test + fun allocatedCancellationException() = runTest { checkAll( Arb.int(), Arb.string().map { CancellationException(it, null) }, @@ -581,4 +610,4 @@ class ResourceTest : StringSpec({ released.await().shouldBeTypeOf() } } -}) +} diff --git a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/FlowJvmTest.kt b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/FlowJvmTest.kt index 1ca0f21b4d9..c7cf8cb3a56 100644 --- a/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/FlowJvmTest.kt +++ b/arrow-libs/fx/arrow-fx-coroutines/src/jvmTest/kotlin/arrow/fx/coroutines/FlowJvmTest.kt @@ -10,12 +10,10 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toSet -import kotlin.time.ExperimentalTime import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.test.runTest @OptIn(FlowPreview::class, ExperimentalCoroutinesApi::class) -@ExperimentalTime class FlowJvmTest { @Test fun parMapSingleThreadIdentity() = runTest { resourceScope {