diff --git a/arrow-libs/fx/arrow-fx-stm/build.gradle.kts b/arrow-libs/fx/arrow-fx-stm/build.gradle.kts index 31786faea44..d8448e947e5 100644 --- a/arrow-libs/fx/arrow-fx-stm/build.gradle.kts +++ b/arrow-libs/fx/arrow-fx-stm/build.gradle.kts @@ -5,7 +5,6 @@ plugins { alias(libs.plugins.arrowGradleConfig.kotlin) alias(libs.plugins.arrowGradleConfig.publish) alias(libs.plugins.kotlinx.kover) - alias(libs.plugins.kotest.multiplatform) alias(libs.plugins.spotless) } @@ -22,7 +21,6 @@ kotlin { commonMain { dependencies { api(projects.arrowCore) - compileOnly(libs.kotlin.stdlibCommon) implementation(libs.coroutines.core) } } @@ -30,25 +28,10 @@ kotlin { commonTest { dependencies { implementation(projects.arrowFxCoroutines) - implementation(libs.kotest.frameworkEngine) implementation(libs.kotest.assertionsCore) implementation(libs.kotest.property) - } - } - jvmTest { - dependencies { - runtimeOnly(libs.kotest.runnerJUnit5) - } - } - - jvmMain { - dependencies { - implementation(libs.kotlin.stdlib) - } - } - jsMain { - dependencies { - implementation(libs.kotlin.stdlibJS) + implementation(libs.kotlin.test) + implementation(libs.coroutines.test) } } } @@ -61,3 +44,7 @@ kotlin { } } } + +tasks.withType { + useJUnitPlatform() +} diff --git a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/KotestConfig.kt b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/KotestConfig.kt deleted file mode 100644 index ed1e6d93002..00000000000 --- a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/KotestConfig.kt +++ /dev/null @@ -1,10 +0,0 @@ -package arrow.fx.stm - -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-stm/src/commonTest/kotlin/arrow/fx/stm/STMTest.kt b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/STMTest.kt index 38264295207..2623f4dace7 100644 --- a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/STMTest.kt +++ b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/STMTest.kt @@ -4,7 +4,6 @@ import arrow.fx.coroutines.parMap import arrow.fx.coroutines.parZip import arrow.fx.stm.internal.BlockedIndefinitely import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.ints.shouldBeInRange import io.kotest.matchers.shouldBe @@ -15,257 +14,278 @@ import io.kotest.property.checkAll import kotlinx.coroutines.async import kotlinx.coroutines.delay import kotlinx.coroutines.joinAll +import kotlinx.coroutines.test.runTest import kotlinx.coroutines.withTimeoutOrNull +import kotlin.test.Ignore +import kotlin.test.Test import kotlin.time.Duration.Companion.microseconds import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime @ExperimentalTime -class STMTest : StringSpec({ - "no-effects" { - atomically { 10 } shouldBeExactly 10 - } - "reading from vars" { - checkAll(Arb.int()) { i: Int -> - val tv = TVar.new(i) - atomically { - tv.read() - } shouldBeExactly i - tv.unsafeRead() shouldBeExactly i - } +class STMTest { + + @Test fun noEffects() = runTest { + atomically { 10 } shouldBeExactly 10 + } + + @Test fun readingFromVars() = runTest { + checkAll(Arb.int()) { i: Int -> + val tv = TVar.new(i) + atomically { + tv.read() + } shouldBeExactly i + tv.unsafeRead() shouldBeExactly i } - "reading and writing" { - checkAll(Arb.int(), Arb.int()) { i: Int, j: Int -> - val tv = TVar.new(i) - atomically { tv.write(j) } - tv.unsafeRead() shouldBeExactly j - } + } + + @Test fun readingAndWriting() = runTest { + checkAll(Arb.int(), Arb.int()) { i: Int, j: Int -> + val tv = TVar.new(i) + atomically { tv.write(j) } + tv.unsafeRead() shouldBeExactly j } - "read after a write should have the updated value" { - checkAll(Arb.int(), Arb.int()) { i: Int, j: Int -> - val tv = TVar.new(i) - atomically { tv.write(j); tv.read() } shouldBeExactly j - tv.unsafeRead() shouldBeExactly j - } + } + + @Test fun readAfterAWriteShouldHaveTheUpdatedValue() = runTest { + checkAll(Arb.int(), Arb.int()) { i: Int, j: Int -> + val tv = TVar.new(i) + atomically { tv.write(j); tv.read() } shouldBeExactly j + tv.unsafeRead() shouldBeExactly j } - "reading multiple variables" { - checkAll(Arb.int(), Arb.int(), Arb.int()) { i: Int, j: Int, k: Int -> - val v1 = TVar.new(i) - val v2 = TVar.new(j) - val v3 = TVar.new(k) - atomically { v1.read() + v2.read() + v3.read() } shouldBeExactly i + j + k - v1.unsafeRead() shouldBeExactly i - v2.unsafeRead() shouldBeExactly j - v3.unsafeRead() shouldBeExactly k - } + } + + @Test fun readingMultipleVariables() = runTest { + checkAll(Arb.int(), Arb.int(), Arb.int()) { i: Int, j: Int, k: Int -> + val v1 = TVar.new(i) + val v2 = TVar.new(j) + val v3 = TVar.new(k) + atomically { v1.read() + v2.read() + v3.read() } shouldBeExactly i + j + k + v1.unsafeRead() shouldBeExactly i + v2.unsafeRead() shouldBeExactly j + v3.unsafeRead() shouldBeExactly k } - "reading and writing multiple variables" { - checkAll(Arb.int(), Arb.int(), Arb.int()) { i: Int, j: Int, k: Int -> - val v1 = TVar.new(i) - val v2 = TVar.new(j) - val v3 = TVar.new(k) - val sum = TVar.new(0) - atomically { - val s = v1.read() + v2.read() + v3.read() - sum.write(s) - } - v1.unsafeRead() shouldBeExactly i - v2.unsafeRead() shouldBeExactly j - v3.unsafeRead() shouldBeExactly k - sum.unsafeRead() shouldBeExactly i + j + k + } + + @Test fun readingAndWritingMultipleVariables() = runTest { + checkAll(Arb.int(), Arb.int(), Arb.int()) { i: Int, j: Int, k: Int -> + val v1 = TVar.new(i) + val v2 = TVar.new(j) + val v3 = TVar.new(k) + val sum = TVar.new(0) + atomically { + val s = v1.read() + v2.read() + v3.read() + sum.write(s) } + v1.unsafeRead() shouldBeExactly i + v2.unsafeRead() shouldBeExactly j + v3.unsafeRead() shouldBeExactly k + sum.unsafeRead() shouldBeExactly i + j + k } - "retry without prior reads throws an exception" { - shouldThrow { atomically { retry() } } - } - "retry should suspend forever if no read variable changes" { - withTimeoutOrNull(500.milliseconds) { - val tv = TVar.new(0) - atomically { - if (tv.read() == 0) retry() - else 200 - } - } shouldBe null - } - "a suspended transaction will resume if a variable changes" { + } + + @Test fun retryWithoutPriorReadsThrowsAnException() = runTest { + shouldThrow { atomically { retry() } } + } + + @Test fun retryShouldSuspendForeverIfNoReadVariableChanges() = runTest { + withTimeoutOrNull(500.milliseconds) { val tv = TVar.new(0) - val f = async { - delay(500.milliseconds) - atomically { tv.modify { it + 1 } } - } atomically { - when (val i = tv.read()) { - 0 -> retry() - else -> i - } - } shouldBeExactly 1 - f.join() - } - "a suspended transaction will resume if any variable changes" { - val v1 = TVar.new(0) - val v2 = TVar.new(0) - val v3 = TVar.new(0) - val f = async { - delay(500.milliseconds) - atomically { v1.modify { it + 1 } } - delay(500.milliseconds) - atomically { v2.modify { it + 1 } } - delay(500.milliseconds) - atomically { v3.modify { it + 1 } } + if (tv.read() == 0) retry() + else 200 } - atomically { - val i = v1.read() + v2.read() + v3.read() - check(i >= 3) - i - } shouldBeExactly 3 - f.join() - } - "retry + orElse: retry orElse t1 = t1" { - atomically { - stm { retry() } orElse { 10 } - } shouldBeExactly 10 + } shouldBe null + } + + @Test fun aSuspendedTransactionWillResumeIfAVariableChanges() = runTest { + val tv = TVar.new(0) + val f = async { + delay(500.milliseconds) + atomically { tv.modify { it + 1 } } } - "retry + orElse: t1 orElse retry = t1" { - atomically { - stm { 10 } orElse { retry() } - } shouldBeExactly 10 + atomically { + when (val i = tv.read()) { + 0 -> retry() + else -> i + } + } shouldBeExactly 1 + f.join() + } + + @Test fun aSuspendedTransactionWillResumeIfAnyVariableChanges() = runTest { + val v1 = TVar.new(0) + val v2 = TVar.new(0) + val v3 = TVar.new(0) + val f = async { + delay(500.milliseconds) + atomically { v1.modify { it + 1 } } + delay(500.milliseconds) + atomically { v2.modify { it + 1 } } + delay(500.milliseconds) + atomically { v3.modify { it + 1 } } } - "retry + orElse: associativity" { - checkAll(Arb.boolean(), Arb.boolean(), Arb.boolean()) { b1: Boolean, b2: Boolean, b3: Boolean -> - if ((b1 || b2 || b3).not()) { - shouldThrow { - atomically { - stm { stm { check(b1) } orElse { check(b2) } } orElse { check(b3) } - } - } shouldBe shouldThrow { - atomically { - stm { check(b1) } orElse { stm { check(b2) } orElse { check(b3) } } - } - } - } else { + atomically { + val i = v1.read() + v2.read() + v3.read() + check(i >= 3) + i + } shouldBeExactly 3 + f.join() + } + + @Test fun retryOrElseRetryOrElseT1T1() = runTest { + atomically { + stm { retry() } orElse { 10 } + } shouldBeExactly 10 + } + + @Test fun retryOrElseT1OrElseRetryT1() = runTest { + atomically { + stm { 10 } orElse { retry() } + } shouldBeExactly 10 + } + + @Test fun retryOrElseAssociativity() = runTest { + checkAll(Arb.boolean(), Arb.boolean(), Arb.boolean()) { b1: Boolean, b2: Boolean, b3: Boolean -> + if ((b1 || b2 || b3).not()) { + shouldThrow { atomically { stm { stm { check(b1) } orElse { check(b2) } } orElse { check(b3) } - } shouldBe atomically { + } + } shouldBe shouldThrow { + atomically { stm { check(b1) } orElse { stm { check(b2) } orElse { check(b3) } } } } + } else { + atomically { + stm { stm { check(b1) } orElse { check(b2) } } orElse { check(b3) } + } shouldBe atomically { + stm { check(b1) } orElse { stm { check(b2) } orElse { check(b3) } } + } } } - "suspended transactions are resumed for variables accessed in orElse" { - val tv = TVar.new(0) - val f = async { - delay(10.microseconds) - atomically { tv.modify { it + 1 } } + } + + @Test fun suspendedTransactionsAreResumedForVariablesAccessedInOrElse() = runTest { + val tv = TVar.new(0) + val f = async { + delay(10.microseconds) + atomically { tv.modify { it + 1 } } + } + atomically { + stm { + when (val i = tv.read()) { + 0 -> retry() + else -> i } + } orElse { retry() } + } shouldBeExactly 1 + f.join() + } + + @Test fun onASingleVariableConcurrentTransactionsShouldBeLinear() = runTest { + val tv = TVar.new(0) + val res = TQueue.new() + + (0..100).map { + async { atomically { - stm { - when (val i = tv.read()) { - 0 -> retry() - else -> i - } - } orElse { retry() } - } shouldBeExactly 1 - f.join() - } - "on a single variable concurrent transactions should be linear" { - val tv = TVar.new(0) - val res = TQueue.new() + val r = tv.read().also { tv.write(it + 1) } + res.write(r) + } + } + }.joinAll() - (0..100).map { - async { - atomically { - val r = tv.read().also { tv.write(it + 1) } - res.write(r) - } - } - }.joinAll() + atomically { res.flush() } shouldBe (0..100).toList() + } - atomically { res.flush() } shouldBe (0..100).toList() - } - "atomically rethrows exceptions" { - shouldThrow { atomically { throw IllegalArgumentException("Test") } } + @Test fun atomicallyRethrowsExceptions() = runTest { + shouldThrow { atomically { throw IllegalArgumentException("Test") } } + } + + @Test fun throwingAnExceptionsShouldVoidAllStateChanges() = runTest { + val tv = TVar.new(10) + shouldThrow { + atomically { tv.write(30); throw IllegalArgumentException("test") } } - "throwing an exceptions should void all state changes" { - val tv = TVar.new(10) - shouldThrow { - atomically { tv.write(30); throw IllegalArgumentException("test") } + tv.unsafeRead() shouldBeExactly 10 + } + + @Test fun catchShouldWorkAsExcepted() = runTest { + val tv = TVar.new(10) + val ex = IllegalArgumentException("test") + atomically { + catch({ + tv.write(30) + throw ex + }) { e -> + e shouldBe ex } - tv.unsafeRead() shouldBeExactly 10 } - "catch should work as excepted" { - val tv = TVar.new(10) - val ex = IllegalArgumentException("test") - atomically { - catch({ - tv.write(30) - throw ex - }) { e -> - e shouldBe ex + tv.unsafeRead() shouldBeExactly 10 + } + + @Test fun concurrentExample1() = runTest { + val acc1 = TVar.new(100) + val acc2 = TVar.new(200) + parZip( + { + // transfer acc1 to acc2 + val amount = 50 + atomically { + val acc1Balance = acc1.read() + check(acc1Balance - amount >= 0) + acc1.write(acc1Balance - amount) + acc2.modify { it + 50 } } - } - tv.unsafeRead() shouldBeExactly 10 - } - "concurrent example 1" { - val acc1 = TVar.new(100) - val acc2 = TVar.new(200) - parZip( - { - // transfer acc1 to acc2 - val amount = 50 - atomically { - val acc1Balance = acc1.read() - check(acc1Balance - amount >= 0) - acc1.write(acc1Balance - amount) - acc2.modify { it + 50 } - } - }, - { - atomically { acc1.modify { it - 60 } } - delay(20.milliseconds) - atomically { acc1.modify { it + 60 } } - }, - { _, _ -> Unit } - ) - acc1.unsafeRead() shouldBeExactly 50 - acc2.unsafeRead() shouldBeExactly 250 - } + }, + { + atomically { acc1.modify { it - 60 } } + delay(20.milliseconds) + atomically { acc1.modify { it + 60 } } + }, + { _, _ -> Unit } + ) + acc1.unsafeRead() shouldBeExactly 50 + acc2.unsafeRead() shouldBeExactly 250 + } - // TypeError: Cannot read property 'toString' of undefined - // at ObjectLiteral_0.test(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:3661) - // at .invokeMatcher(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:19216) - // at .should(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:19212) - // at .shouldBeInRange(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:3652) - // at STMTransaction.f(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:261217) - // at commit.doResume(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:270552) - // at commit.CoroutineImpl.resumeWith(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:118697) - // at CancellableContinuationImpl.DispatchedTask.run(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:174593) - // at WindowMessageQueue.MessageQueue.process(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:177985) - // at .(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:177940) - "concurrent example 2".config(enabled = false) { - val tq = TQueue.new() - parZip( - { - // producers - (0..4).parMap { - for (i in (it * 20 + 1)..(it * 20 + 20)) { - atomically { tq.write(i) } - } - } - }, - { - val collected = mutableSetOf() - for (i in 1..100) { - // consumer - atomically { - tq.read().also { it shouldBeInRange (1..100) } - }.also { collected.add(it) } - } - // verify that we got 100 unique numbers - collected.size shouldBeExactly 100 + // TypeError: Cannot read property 'toString' of undefined + // at ObjectLiteral_0.test(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:3661) + // at .invokeMatcher(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:19216) + // at .should(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:19212) + // at .shouldBeInRange(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:3652) + // at STMTransaction.f(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:261217) + // at commit.doResume(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:270552) + // at commit.CoroutineImpl.resumeWith(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:118697) + // at CancellableContinuationImpl.DispatchedTask.run(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:174593) + // at WindowMessageQueue.MessageQueue.process(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:177985) + // at .(/var/folders/x5/6r18d9w52c7czy6zh5m1spvw0000gn/T/_karma_webpack_624630/commons.js:177940) + @Test @Ignore fun concurrentExample2ConfigEnabledFalse() = runTest { + val tq = TQueue.new() + parZip( + { + // producers + (0..4).parMap { + for (i in (it * 20 + 1)..(it * 20 + 20)) { + atomically { tq.write(i) } } - ) { _, _ -> Unit } - // the above only finishes if the consumer reads at least 100 values, this here is just to make sure there are no leftovers - atomically { tq.flush() } shouldBe emptyList() - } - -}) + } + }, + { + val collected = mutableSetOf() + for (i in 1..100) { + // consumer + atomically { + tq.read().also { it shouldBeInRange (1..100) } + }.also { collected.add(it) } + } + // verify that we got 100 unique numbers + collected.size shouldBeExactly 100 + } + ) { _, _ -> Unit } + // the above only finishes if the consumer reads at least 100 values, this here is just to make sure there are no leftovers + atomically { tq.flush() } shouldBe emptyList() + } +} diff --git a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TArrayTest.kt b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TArrayTest.kt index c8bf40ebad2..3bb492174ff 100644 --- a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TArrayTest.kt +++ b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TArrayTest.kt @@ -1,35 +1,39 @@ package arrow.fx.stm -import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import kotlin.test.Test -class TArrayTest : StringSpec({ - "creating an array" { - val t1 = TArray.new(10) { it } - t1.size() shouldBeExactly 10 - atomically { t1.fold(0) { acc, v -> acc + v } } shouldBeExactly (0..9).sum() - atomically { (0..9).fold(true) { acc, v -> t1.get(v) == v && acc } } shouldBe true +class TArrayTest { - val t2 = atomically { newTArray(10) { it } } - t2.size() shouldBeExactly 10 - atomically { t2.fold(0) { acc, v -> acc + v } } shouldBeExactly (0..9).sum() - atomically { (0..9).fold(true) { acc, v -> t2.get(v) == v && acc } } shouldBe true - } - "get should get the correct value" { - val t2 = atomically { newTArray(20) { it } } - atomically { (0..19).fold(true) { acc, v -> t2.get(v) == v && acc } } shouldBe true - } - "write should write to the correct value" { - val t2 = atomically { newTArray(20) { it } } - atomically { t2.get(5) } shouldBeExactly 5 - atomically { t2[5] = 2 } - atomically { t2.get(5) } shouldBeExactly 2 - } - "transform should perform an operation on each element" { - val t2 = atomically { newTArray(20) { it } } - atomically { t2.transform { it * 2 } } - atomically { t2.fold(0) { acc, v -> acc + v } } shouldBeExactly (0..19).sum() * 2 - } + @Test fun creatingAnArray() = runTest { + val t1 = TArray.new(10) { it } + t1.size() shouldBeExactly 10 + atomically { t1.fold(0) { acc, v -> acc + v } } shouldBeExactly (0..9).sum() + atomically { (0..9).fold(true) { acc, v -> t1.get(v) == v && acc } } shouldBe true + + val t2 = atomically { newTArray(10) { it } } + t2.size() shouldBeExactly 10 + atomically { t2.fold(0) { acc, v -> acc + v } } shouldBeExactly (0..9).sum() + atomically { (0..9).fold(true) { acc, v -> t2.get(v) == v && acc } } shouldBe true + } + + @Test fun getShouldGetTheCorrectValue() = runTest { + val t2 = atomically { newTArray(20) { it } } + atomically { (0..19).fold(true) { acc, v -> t2.get(v) == v && acc } } shouldBe true + } + + @Test fun writeShouldWriteToTheCorrectValue() = runTest { + val t2 = atomically { newTArray(20) { it } } + atomically { t2.get(5) } shouldBeExactly 5 + atomically { t2[5] = 2 } + atomically { t2.get(5) } shouldBeExactly 2 + } + + @Test fun transformShouldPerformAnOperationOnEachElement() = runTest { + val t2 = atomically { newTArray(20) { it } } + atomically { t2.transform { it * 2 } } + atomically { t2.fold(0) { acc, v -> acc + v } } shouldBeExactly (0..19).sum() * 2 } -) +} diff --git a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TMVarTest.kt b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TMVarTest.kt index eac2389e97b..4e23fd65780 100644 --- a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TMVarTest.kt +++ b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TMVarTest.kt @@ -1,140 +1,158 @@ package arrow.fx.stm -import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.shouldBe import io.kotest.property.Arb import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.orNull import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import kotlin.test.Test -class TMVarTest : StringSpec({ - "empty creates an empty TMVar" { - val t1 = TMVar.empty() - atomically { t1.tryTake() } shouldBe null - val t2 = atomically { newEmptyTMVar() } - atomically { t2.tryTake() } shouldBe null - } - "new creates a filled TMVar" { - val t1 = TMVar.new(100) - atomically { t1.take() } shouldBe 100 - val t2 = atomically { newTMVar(10) } - atomically { t2.take() } shouldBe 10 - } - "take leaves the TMVar empty" { - val tm = TMVar.new(500) - atomically { tm.take() } shouldBeExactly 500 - atomically { tm.tryTake() } shouldBe null - } - "isEmpty = tryRead == null" { - checkAll(Arb.int().orNull()) { i -> - val tm = when (i) { - null -> TMVar.empty() - else -> TMVar.new(i) - } - atomically { tm.isEmpty() } shouldBe - atomically { tm.tryRead() == null } - atomically { tm.isEmpty() } shouldBe - (i == null) - } - } - "isEmpty = tryRead != null" { - checkAll(Arb.int().orNull()) { i -> - val tm = when (i) { - null -> TMVar.empty() - else -> TMVar.new(i) - } - atomically { tm.isNotEmpty() } shouldBe - atomically { tm.tryRead() != null } - atomically { tm.isNotEmpty() } shouldBe - (i != null) +class TMVarTest { + + @Test fun emptyCreatesAnEmptyTMVar() = runTest { + val t1 = TMVar.empty() + atomically { t1.tryTake() } shouldBe null + val t2 = atomically { newEmptyTMVar() } + atomically { t2.tryTake() } shouldBe null + } + + @Test fun newCreatesAFilledTMVar() = runTest { + val t1 = TMVar.new(100) + atomically { t1.take() } shouldBe 100 + val t2 = atomically { newTMVar(10) } + atomically { t2.take() } shouldBe 10 + } + + @Test fun takeLeavesTheTMVarEmpty() = runTest { + val tm = TMVar.new(500) + atomically { tm.take() } shouldBeExactly 500 + atomically { tm.tryTake() } shouldBe null + } + + @Test fun isEmptyIsTryReadEqualsNull() = runTest { + checkAll(Arb.int().orNull()) { i -> + val tm = when (i) { + null -> TMVar.empty() + else -> TMVar.new(i) } + atomically { tm.isEmpty() } shouldBe + atomically { tm.tryRead() == null } + atomically { tm.isEmpty() } shouldBe + (i == null) } - "take retries on empty" { - val tm = TMVar.empty() - atomically { - stm { tm.take().let { true } } orElse { false } - } shouldBe false - atomically { tm.tryTake() } shouldBe null - } - "tryTake behaves like take if there is a value" { - val tm = TMVar.new(100) - atomically { - tm.tryTake() - } shouldBe 100 - atomically { tm.tryTake() } shouldBe null - } - "tryTake returns null on empty" { - val tm = TMVar.empty() - atomically { tm.tryTake() } shouldBe null - } - "read retries on empty" { - val tm = TMVar.empty() - atomically { - stm { tm.read().let { true } } orElse { false } - } shouldBe false - atomically { tm.tryTake() } shouldBe null - } - "read returns the value if not empty and does not remove it" { - val tm = TMVar.new(10) - atomically { - tm.read() - } shouldBe 10 - atomically { tm.tryTake() } shouldBe 10 - } - "tryRead behaves like read if there is a value" { - val tm = TMVar.new(100) - atomically { tm.tryRead() } shouldBe - atomically { tm.read() } - atomically { tm.tryTake() } shouldBe 100 - } - "tryRead returns null if there is no value" { - val tm = TMVar.empty() - atomically { tm.tryRead() } shouldBe null - atomically { tm.tryTake() } shouldBe null - } - "put retries if there is already a value" { - val tm = TMVar.new(5) - atomically { - stm { tm.put(100).let { true } } orElse { false } - } shouldBe false - atomically { tm.tryTake() } shouldBe 5 - } - "put replaces the value if it was empty" { - val tm = TMVar.empty() - atomically { - tm.put(100) - tm.tryTake() - } shouldBe 100 - } - "tryPut behaves like put if there is no value" { - val tm = TMVar.empty() - atomically { - tm.tryPut(100) - tm.tryTake() - } shouldBe atomically { - tm.put(100) - tm.tryTake() + } + + @Test fun isEmptyIsTryReadUnequalsNull() = runTest { + checkAll(Arb.int().orNull()) { i -> + val tm = when (i) { + null -> TMVar.empty() + else -> TMVar.new(i) } - atomically { tm.tryPut(30) } shouldBe true - atomically { tm.tryTake() } shouldBe 30 - } - "tryPut returns false if there is already a value" { - val tm = TMVar.new(30) - atomically { tm.tryPut(20) } shouldBe false - atomically { tm.tryTake() } shouldBe 30 - } - "swap replaces the current value only if it is not null" { - val tm = TMVar.new(30) - atomically { tm.swap(25) } shouldBeExactly 30 - atomically { tm.take() } shouldBeExactly 25 - } - "swap should retry if there is no value" { - val tm = TMVar.empty() - atomically { - stm { tm.swap(10).let { true } } orElse { false } - } shouldBe false - atomically { tm.tryTake() } shouldBe null + atomically { tm.isNotEmpty() } shouldBe + atomically { tm.tryRead() != null } + atomically { tm.isNotEmpty() } shouldBe + (i != null) } } -) + + @Test fun takeRetriesOnEmpty() = runTest { + val tm = TMVar.empty() + atomically { + stm { tm.take().let { true } } orElse { false } + } shouldBe false + atomically { tm.tryTake() } shouldBe null + } + + @Test fun tryTakeBehavesLikeTakeIfThereIsAValue() = runTest { + val tm = TMVar.new(100) + atomically { + tm.tryTake() + } shouldBe 100 + atomically { tm.tryTake() } shouldBe null + } + + @Test fun tryTakeReturnsNullOnEmpty() = runTest { + val tm = TMVar.empty() + atomically { tm.tryTake() } shouldBe null + } + + @Test fun readRetriesOnEmpty() = runTest { + val tm = TMVar.empty() + atomically { + stm { tm.read().let { true } } orElse { false } + } shouldBe false + atomically { tm.tryTake() } shouldBe null + } + + @Test fun readReturnsTheValueIfNotEmptyAndDoesNotRemoveIt() = runTest { + val tm = TMVar.new(10) + atomically { + tm.read() + } shouldBe 10 + atomically { tm.tryTake() } shouldBe 10 + } + + @Test fun tryReadBehavesLikeReadIfThereIsAValue() = runTest { + val tm = TMVar.new(100) + atomically { tm.tryRead() } shouldBe + atomically { tm.read() } + atomically { tm.tryTake() } shouldBe 100 + } + + @Test fun tryReadReturnsNullIfThereIsNoValue() = runTest { + val tm = TMVar.empty() + atomically { tm.tryRead() } shouldBe null + atomically { tm.tryTake() } shouldBe null + } + + @Test fun putRetriesIfThereIsAlreadyAValue() = runTest { + val tm = TMVar.new(5) + atomically { + stm { tm.put(100).let { true } } orElse { false } + } shouldBe false + atomically { tm.tryTake() } shouldBe 5 + } + + @Test fun putReplacesTheValueIfItWasEmpty() = runTest { + val tm = TMVar.empty() + atomically { + tm.put(100) + tm.tryTake() + } shouldBe 100 + } + + @Test fun tryPutBehavesLikePutIfThereIsNoValue() = runTest { + val tm = TMVar.empty() + atomically { + tm.tryPut(100) + tm.tryTake() + } shouldBe atomically { + tm.put(100) + tm.tryTake() + } + atomically { tm.tryPut(30) } shouldBe true + atomically { tm.tryTake() } shouldBe 30 + } + + @Test fun tryPutReturnsFalseIfThereIsAlreadyAValue() = runTest { + val tm = TMVar.new(30) + atomically { tm.tryPut(20) } shouldBe false + atomically { tm.tryTake() } shouldBe 30 + } + + @Test fun swapReplacesTheCurrentValueOnlyIfItIsNotNull() = runTest { + val tm = TMVar.new(30) + atomically { tm.swap(25) } shouldBeExactly 30 + atomically { tm.take() } shouldBeExactly 25 + } + + @Test fun swapShouldRetryIfThereIsNoValue() = runTest { + val tm = TMVar.empty() + atomically { + stm { tm.swap(10).let { true } } orElse { false } + } shouldBe false + atomically { tm.tryTake() } shouldBe null + } +} diff --git a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TMapTest.kt b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TMapTest.kt index a88c750fe9a..e7ea5776ddc 100644 --- a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TMapTest.kt +++ b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TMapTest.kt @@ -1,59 +1,64 @@ package arrow.fx.stm -import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.shouldBe import io.kotest.property.Arb import io.kotest.property.arbitrary.int import io.kotest.property.arbitrary.map import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import kotlin.test.Test -class TMapTest : StringSpec({ - "insert values" { - checkAll(Arb.int(), Arb.int()) { k, v -> - val map = TMap.new() - atomically { map.insert(k, v) } - atomically { map.lookup(k) } shouldBe v - } +class TMapTest { + + @Test fun insertValues() = runTest { + checkAll(Arb.int(), Arb.int()) { k, v -> + val map = TMap.new() + atomically { map.insert(k, v) } + atomically { map.lookup(k) } shouldBe v } - "insert multiple values" { - checkAll(Arb.map(Arb.int(), Arb.int())) { pairs -> - val map = TMap.new() - atomically { - for ((k, v) in pairs) map.insert(k, v) - } - atomically { - for ((k, v) in pairs) map.lookup(k) shouldBe v - } + } + + @Test fun insertMultipleValues() = runTest { + checkAll(Arb.map(Arb.int(), Arb.int())) { pairs -> + val map = TMap.new() + atomically { + for ((k, v) in pairs) map.insert(k, v) } - } - "insert multiple colliding values" { - checkAll(Arb.map(Arb.int(), Arb.int())) { pairs -> - val map = TMap.new { 0 } // hash function that always returns 0 - atomically { - for ((k, v) in pairs) map.insert(k, v) - } - atomically { - for ((k, v) in pairs) map.lookup(k) shouldBe v - } + atomically { + for ((k, v) in pairs) map.lookup(k) shouldBe v } } - "insert and remove" { - checkAll(Arb.int(), Arb.int()) { k, v -> - val map = TMap.new() - atomically { map.insert(k, v) } - atomically { map.lookup(k) } shouldBe v - atomically { map.remove(k) } - atomically { map.lookup(k) } shouldBe null + } + + @Test fun insertMultipleCollidingValues() = runTest { + checkAll(Arb.map(Arb.int(), Arb.int())) { pairs -> + val map = TMap.new { 0 } // hash function that always returns 0 + atomically { + for ((k, v) in pairs) map.insert(k, v) } - } - "update" { - checkAll(Arb.int(), Arb.int(), Arb.int()) { k, v, g -> - val map = TMap.new() - atomically { map.insert(k, v) } - atomically { map.lookup(k) } shouldBe v - atomically { map.update(k) { v + g } } - atomically { map.lookup(k) } shouldBe v + g + atomically { + for ((k, v) in pairs) map.lookup(k) shouldBe v } } } -) + + @Test fun insertAndRemove() = runTest { + checkAll(Arb.int(), Arb.int()) { k, v -> + val map = TMap.new() + atomically { map.insert(k, v) } + atomically { map.lookup(k) } shouldBe v + atomically { map.remove(k) } + atomically { map.lookup(k) } shouldBe null + } + } + + @Test fun update() = runTest { + checkAll(Arb.int(), Arb.int(), Arb.int()) { k, v, g -> + val map = TMap.new() + atomically { map.insert(k, v) } + atomically { map.lookup(k) } shouldBe v + atomically { map.update(k) { v + g } } + atomically { map.lookup(k) } shouldBe v + g + } + } +} diff --git a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TQueueTest.kt b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TQueueTest.kt index d55545e0329..8444f6e8518 100644 --- a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TQueueTest.kt +++ b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TQueueTest.kt @@ -1,124 +1,139 @@ package arrow.fx.stm -import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.shouldBe import io.kotest.property.Arb import io.kotest.property.arbitrary.int import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest import kotlin.random.Random +import kotlin.test.Test -class TQueueTest : StringSpec({ - "writing to a queue adds an element" { - val tq = TQueue.new() - atomically { tq.write(10) } - atomically { tq.flush() } shouldBe listOf(10) - } - "reading from a queue should retry if the queue is empty" { - val tq = TQueue.new() - atomically { - stm { tq.read().let { true } } orElse { false } - } shouldBe false - } - "reading from a queue should remove that value" { - val tq = TQueue.new() - atomically { tq.write(10); tq.write(20) } - atomically { tq.read() } shouldBe 10 - atomically { tq.flush() } shouldBe listOf(20) - } - "tryRead behaves like read if there are values to read" { - val tq = TQueue.new() - atomically { tq.write(10) } - atomically { tq.tryRead() } shouldBe 10 - atomically { tq.flush() } shouldBe emptyList() - } - "tryRead returns null if the queue is empty" { - val tq = TQueue.new() - atomically { tq.tryRead() } shouldBe null - } - "flush empties the entire queue and returns it" { - val tq = TQueue.new() - atomically { tq.write(20); tq.write(30); tq.write(40) } - atomically { tq.flush() } shouldBe listOf(20, 30, 40) - atomically { tq.flush() } shouldBe emptyList() - } - "reading/flushing should work after mixed reads/writes" { - val tq = TQueue.new() - atomically { tq.write(20); tq.write(30); tq.peek(); tq.write(40) } - atomically { tq.read() } shouldBe 20 - atomically { tq.flush() } shouldBe listOf(30, 40) +class TQueueTest { - atomically { tq.write(20); tq.write(30); tq.peek(); tq.write(40) } - atomically { tq.flush() } shouldBe listOf(20, 30, 40) - atomically { tq.flush() } shouldBe emptyList() - } - "peek should leave the queue unchanged" { - val tq = TQueue.new() - atomically { tq.write(20); tq.write(30); tq.write(40) } - atomically { tq.peek() } shouldBeExactly 20 - atomically { tq.flush() } shouldBe listOf(20, 30, 40) - } - "peek should retry if the queue is empty" { + @Test fun writingToAQueueAddsAnElement() = runTest { + val tq = TQueue.new() + atomically { tq.write(10) } + atomically { tq.flush() } shouldBe listOf(10) + } + + @Test fun readingFromAQueueShouldRetryIfTheQueueIsEmpty() = runTest { + val tq = TQueue.new() + atomically { + stm { tq.read().let { true } } orElse { false } + } shouldBe false + } + + @Test fun readingFromAQueueShouldRemoveThatValue() = runTest { + val tq = TQueue.new() + atomically { tq.write(10); tq.write(20) } + atomically { tq.read() } shouldBe 10 + atomically { tq.flush() } shouldBe listOf(20) + } + + @Test fun tryReadBehavesLikeReadIfThereAreValuesToRead() = runTest { + val tq = TQueue.new() + atomically { tq.write(10) } + atomically { tq.tryRead() } shouldBe 10 + atomically { tq.flush() } shouldBe emptyList() + } + + @Test fun tryReadReturnsNullIfTheQueueIsEmpty() = runTest { + val tq = TQueue.new() + atomically { tq.tryRead() } shouldBe null + } + + @Test fun flushEmptiesTheEntireQueueAndReturnsIt() = runTest { + val tq = TQueue.new() + atomically { tq.write(20); tq.write(30); tq.write(40) } + atomically { tq.flush() } shouldBe listOf(20, 30, 40) + atomically { tq.flush() } shouldBe emptyList() + } + + @Test fun readingFlushingShouldWorkAfterMixedReadsWrites() = runTest { + val tq = TQueue.new() + atomically { tq.write(20); tq.write(30); tq.peek(); tq.write(40) } + atomically { tq.read() } shouldBe 20 + atomically { tq.flush() } shouldBe listOf(30, 40) + + atomically { tq.write(20); tq.write(30); tq.peek(); tq.write(40) } + atomically { tq.flush() } shouldBe listOf(20, 30, 40) + atomically { tq.flush() } shouldBe emptyList() + } + + @Test fun peekShouldLeaveTheQueueUnchanged() = runTest { + val tq = TQueue.new() + atomically { tq.write(20); tq.write(30); tq.write(40) } + atomically { tq.peek() } shouldBeExactly 20 + atomically { tq.flush() } shouldBe listOf(20, 30, 40) + } + + @Test fun peekShouldRetryIfTheQueueIsEmpty() = runTest { + val tq = TQueue.new() + atomically { + stm { tq.peek().let { true } } orElse { false } + } shouldBe false + } + + @Test fun tryPeekShouldBehaveLikePeekIfThereAreElements() = runTest { + val tq = TQueue.new() + atomically { tq.write(20); tq.write(30); tq.write(40) } + atomically { tq.peek() } shouldBeExactly + atomically { tq.tryPeek()!! } + atomically { tq.flush() } shouldBe listOf(20, 30, 40) + } + + @Test fun tryPeekShouldReturnNullIfTheQueueIsEmpty() = runTest { + val tq = TQueue.new() + atomically { tq.tryPeek() } shouldBe null + } + + @Test fun isEmptyAndIsNotEmptyShouldWorkCorrectly() = runTest { + val tq = TQueue.new() + atomically { tq.isEmpty() } shouldBe true + atomically { tq.isNotEmpty() } shouldBe false + atomically { tq.write(20) } + atomically { tq.isEmpty() } shouldBe false + atomically { tq.isNotEmpty() } shouldBe true + atomically { tq.peek(); tq.write(30) } + atomically { tq.isEmpty() } shouldBe false + atomically { tq.isNotEmpty() } shouldBe true + } + + @Test fun sizeShouldReturnTheCorrectAmount() = runTest { + checkAll(Arb.int(0..50)) { i -> val tq = TQueue.new() atomically { - stm { tq.peek().let { true } } orElse { false } - } shouldBe false - } - "tryPeek should behave like peek if there are elements" { - val tq = TQueue.new() - atomically { tq.write(20); tq.write(30); tq.write(40) } - atomically { tq.peek() } shouldBeExactly - atomically { tq.tryPeek()!! } - atomically { tq.flush() } shouldBe listOf(20, 30, 40) - } - "tryPeek should return null if the queue is empty" { - val tq = TQueue.new() - atomically { tq.tryPeek() } shouldBe null - } - "isEmpty and isNotEmpty should work correctly" { - val tq = TQueue.new() - atomically { tq.isEmpty() } shouldBe true - atomically { tq.isNotEmpty() } shouldBe false - atomically { tq.write(20) } - atomically { tq.isEmpty() } shouldBe false - atomically { tq.isNotEmpty() } shouldBe true - atomically { tq.peek(); tq.write(30) } - atomically { tq.isEmpty() } shouldBe false - atomically { tq.isNotEmpty() } shouldBe true - } - "size should return the correct amount" { - checkAll(Arb.int(0..50)) { i -> - val tq = TQueue.new() - atomically { - for (j in 0..i) { - // read to swap read and write lists randomly - if (Random.nextFloat() > 0.9) tq.tryPeek() - tq.write(j) - } + for (j in 0..i) { + // read to swap read and write lists randomly + if (Random.nextFloat() > 0.9) tq.tryPeek() + tq.write(j) } - atomically { tq.size() } shouldBeExactly i + 1 } + atomically { tq.size() } shouldBeExactly i + 1 } - "writeFront should work correctly" { - val tq = TQueue.new() - atomically { tq.writeFront(203) } - atomically { tq.peek() } shouldBeExactly 203 - atomically { tq.writeFront(50) } - atomically { tq.peek() } shouldBeExactly 50 - atomically { tq.flush() } shouldBe listOf(50, 203) - } - "removeAll should work" { - val tq = TQueue.new() - atomically { tq.removeAll { true } } - atomically { tq.flush() } shouldBe emptyList() + } - atomically { - for (i in 0..100) { - tq.write(i) - } - tq.removeAll { it.rem(2) == 0 } - tq.flush() - } shouldBe (0..100).filter { it.rem(2) == 0 } - } + @Test fun writeFrontShouldWorkCorrectly() = runTest { + val tq = TQueue.new() + atomically { tq.writeFront(203) } + atomically { tq.peek() } shouldBeExactly 203 + atomically { tq.writeFront(50) } + atomically { tq.peek() } shouldBeExactly 50 + atomically { tq.flush() } shouldBe listOf(50, 203) + } + + @Test fun removeAllShouldWork() = runTest { + val tq = TQueue.new() + atomically { tq.removeAll { true } } + atomically { tq.flush() } shouldBe emptyList() + + atomically { + for (i in 0..100) { + tq.write(i) + } + tq.removeAll { it.rem(2) == 0 } + tq.flush() + } shouldBe (0..100).filter { it.rem(2) == 0 } } -) +} diff --git a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TSemaphoreTest.kt b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TSemaphoreTest.kt index f0ace0450da..60ebee46b08 100644 --- a/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TSemaphoreTest.kt +++ b/arrow-libs/fx/arrow-fx-stm/src/commonTest/kotlin/arrow/fx/stm/TSemaphoreTest.kt @@ -1,80 +1,93 @@ package arrow.fx.stm import io.kotest.assertions.throwables.shouldThrow -import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.ints.shouldBeExactly import io.kotest.matchers.shouldBe +import kotlinx.coroutines.test.runTest +import kotlin.test.Test -class TSemaphoreTest : StringSpec({ - "creating a semaphore with a negative number of permits fails" { - shouldThrow { TSemaphore.new(-1) } - shouldThrow { atomically { newTSem(-1) } } - } - "available reads the correct amount" { - val ts = TSemaphore.new(10) - atomically { ts.available() } shouldBeExactly 10 - } - "acquire removes one permit" { - val ts = TSemaphore.new(8) - atomically { ts.acquire(); ts.available() } shouldBeExactly 7 - } - "acquire retries if no permits are available" { - val ts = TSemaphore.new(0) - atomically { - stm { ts.acquire().let { true } } orElse { false } - } shouldBe false - } - "acquire(n) should take n permits" { - val ts = TSemaphore.new(10) - atomically { - ts.acquire(5); ts.available() - } shouldBeExactly 5 - } - "acquire(n) should retry if not enough permits are available" { - val ts = TSemaphore.new(10) - atomically { - stm { ts.acquire(100).let { true } } orElse { false } - } shouldBe false - } - "tryAcquire should behave like acquire if enough permits are available" { - val ts = TSemaphore.new(11) - val ts2 = TSemaphore.new(11) - atomically { ts.tryAcquire() } shouldBe - atomically { ts2.acquire().let { true } } - atomically { ts.available() } shouldBeExactly - atomically { ts2.available() } - } - "tryAcquire should not retry if not enough permits are available" { - val ts = TSemaphore.new(0) - atomically { - ts.tryAcquire() - } shouldBe false - } - "tryAcquire(n) should behave like acquire(n) if enough permits are available" { - val ts = TSemaphore.new(11) - val ts2 = TSemaphore.new(11) - atomically { ts.tryAcquire(4) } shouldBe - atomically { ts2.acquire(4).let { true } } - atomically { ts.available() } shouldBeExactly - atomically { ts2.available() } - } - "tryAcquire(n) should not retry if not enough permits are available" { - val ts = TSemaphore.new(3) - atomically { - ts.tryAcquire(10) - } shouldBe false - } - "release should add one permit" { - val ts = TSemaphore.new(0) - atomically { ts.release(); ts.available() } shouldBeExactly 1 - } - "release(n) should throw if given a negative number" { - val ts = TSemaphore.new(1) - shouldThrow { atomically { ts.release(-1) } } - } - "release(n) should add n permits" { - val ts = TSemaphore.new(3) - atomically { ts.release(6); ts.available() } shouldBe 9 - } +class TSemaphoreTest { + + @Test fun creatingASemaphoreWithANegativeNumberOfPermitsFails() = runTest { + shouldThrow { TSemaphore.new(-1) } + shouldThrow { atomically { newTSem(-1) } } + } + + @Test fun availableReadsTheCorrectAmount() = runTest { + val ts = TSemaphore.new(10) + atomically { ts.available() } shouldBeExactly 10 + } + + @Test fun acquireRemovesOnePermit() = runTest { + val ts = TSemaphore.new(8) + atomically { ts.acquire(); ts.available() } shouldBeExactly 7 + } + + @Test fun acquireRetriesIfNoPermitsAreAvailable() = runTest { + val ts = TSemaphore.new(0) + atomically { + stm { ts.acquire().let { true } } orElse { false } + } shouldBe false + } + + @Test fun acquireNShouldTakeNPermits() = runTest { + val ts = TSemaphore.new(10) + atomically { + ts.acquire(5); ts.available() + } shouldBeExactly 5 + } + + @Test fun acquireNShouldRetryIfNotEnoughPermitsAreAvailable() = runTest { + val ts = TSemaphore.new(10) + atomically { + stm { ts.acquire(100).let { true } } orElse { false } + } shouldBe false + } + + @Test fun tryAcquireShouldBehaveLikeAcquireIfEnoughPermitsAreAvailable() = runTest { + val ts = TSemaphore.new(11) + val ts2 = TSemaphore.new(11) + atomically { ts.tryAcquire() } shouldBe + atomically { ts2.acquire().let { true } } + atomically { ts.available() } shouldBeExactly + atomically { ts2.available() } + } + + @Test fun tryAcquireShouldNotRetryIfNotEnoughPermitsAreAvailable() = runTest { + val ts = TSemaphore.new(0) + atomically { + ts.tryAcquire() + } shouldBe false + } + + @Test fun tryAcquireNShouldBehaveLikeAcquireNIfEnoughPermitsAreAvailable() = runTest { + val ts = TSemaphore.new(11) + val ts2 = TSemaphore.new(11) + atomically { ts.tryAcquire(4) } shouldBe + atomically { ts2.acquire(4).let { true } } + atomically { ts.available() } shouldBeExactly + atomically { ts2.available() } + } + + @Test fun tryAcquireNShouldNotRetryIfNotEnoughPermitsAreAvailable() = runTest { + val ts = TSemaphore.new(3) + atomically { + ts.tryAcquire(10) + } shouldBe false + } + + @Test fun releaseShouldAddOnePermit() = runTest { + val ts = TSemaphore.new(0) + atomically { ts.release(); ts.available() } shouldBeExactly 1 + } + + @Test fun releaseNShouldThrowIfGivenANegativeNumber() = runTest { + val ts = TSemaphore.new(1) + shouldThrow { atomically { ts.release(-1) } } + } + + @Test fun releaseNShouldAddNPermits() = runTest { + val ts = TSemaphore.new(3) + atomically { ts.release(6); ts.available() } shouldBe 9 } -) +}