diff --git a/suite/src/commonTest/kotlin/RandomTest.kt b/suite/src/commonTest/kotlin/RandomTest.kt new file mode 100644 index 0000000..4e11700 --- /dev/null +++ b/suite/src/commonTest/kotlin/RandomTest.kt @@ -0,0 +1,20 @@ +package opensavvy.prepared.suite + +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.shouldBe +import opensavvy.prepared.runner.kotest.PreparedSpec + +class RandomTest : PreparedSpec({ + + test("Setting the seed guarantees that the same sequence of numbers is generated") { + random.setSeed(42) + + assertSoftly { + random.nextInt() shouldBe 972016666 + random.nextInt() shouldBe 1740578880 + random.nextDouble() shouldBe 0.9049568172356872 + random.nextBoolean() shouldBe false + } + } + +}) -- 2.51.2 From 1a324a04529b52b1988fa019a9335a167f54fbad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 11 May 2024 19:49:40 +0200 Subject: [PATCH 2/5] fix(suite): Fixed randomBoolean which previously returned an Int --- suite/src/commonMain/kotlin/Random.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/suite/src/commonMain/kotlin/Random.kt b/suite/src/commonMain/kotlin/Random.kt index debebe3..2481108 100644 --- a/suite/src/commonMain/kotlin/Random.kt +++ b/suite/src/commonMain/kotlin/Random.kt @@ -4,6 +4,7 @@ import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlin.jvm.JvmName import kotlin.random.Random.Default.nextBits +import kotlin.random.Random.Default.nextBoolean import kotlin.random.Random.Default.nextDouble import kotlin.random.Random.Default.nextFloat import kotlin.random.Random.Default.nextInt @@ -284,7 +285,7 @@ fun randomLong(from: Long, until: Long) = prepared { nextLong(from, until) } * @see KotlinRandom.nextBoolean Standard library. * @see nextBoolean Direct value equivalent. */ -fun randomBoolean() = prepared { nextInt() } +fun randomBoolean() = prepared { nextBoolean() } /** * Provider for a random double. -- 2.51.2 From bd9348d06aa564fdefa3e10332f2ac18ddbcdaa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 11 May 2024 19:55:01 +0200 Subject: [PATCH 3/5] fix(suite): Random prepared generators ignored the seed --- suite/src/commonMain/kotlin/Random.kt | 18 +++++++++--------- suite/src/commonTest/kotlin/RandomTest.kt | 16 ++++++++++++++++ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/suite/src/commonMain/kotlin/Random.kt b/suite/src/commonMain/kotlin/Random.kt index 2481108..f68408e 100644 --- a/suite/src/commonMain/kotlin/Random.kt +++ b/suite/src/commonMain/kotlin/Random.kt @@ -245,7 +245,7 @@ suspend fun Random.nextFloat() = * @see KotlinRandom.nextBits Standard library. * @see nextBits Direct value equivalent. */ -fun randomBits(bitCount: Int) = prepared { nextBits(bitCount) } +fun randomBits(bitCount: Int) = prepared { random.nextBits(bitCount) } /** * Provider for a random integer. @@ -253,7 +253,7 @@ fun randomBits(bitCount: Int) = prepared { nextBits(bitCount) } * @see KotlinRandom.nextInt Standard library. * @see nextInt Direct value equivalent. */ -fun randomInt() = prepared { nextInt() } +fun randomInt() = prepared { random.nextInt() } /** * Provider for a random integer. @@ -261,7 +261,7 @@ fun randomInt() = prepared { nextInt() } * @see KotlinRandom.nextInt Standard library. * @see nextInt Direct value equivalent. */ -fun randomInt(from: Int, until: Int) = prepared { nextInt(from, until) } +fun randomInt(from: Int, until: Int) = prepared { random.nextInt(from, until) } /** * Provider for a random integer. @@ -269,7 +269,7 @@ fun randomInt(from: Int, until: Int) = prepared { nextInt(from, until) } * @see KotlinRandom.nextLong Standard library. * @see nextLong Direct value equivalent. */ -fun randomLong() = prepared { nextLong() } +fun randomLong() = prepared { random.nextLong() } /** * Provider for a random integer. @@ -277,7 +277,7 @@ fun randomLong() = prepared { nextLong() } * @see KotlinRandom.nextLong Standard library. * @see nextLong Direct value equivalent. */ -fun randomLong(from: Long, until: Long) = prepared { nextLong(from, until) } +fun randomLong(from: Long, until: Long) = prepared { random.nextLong(from, until) } /** * Provider for a random boolean. @@ -285,7 +285,7 @@ fun randomLong(from: Long, until: Long) = prepared { nextLong(from, until) } * @see KotlinRandom.nextBoolean Standard library. * @see nextBoolean Direct value equivalent. */ -fun randomBoolean() = prepared { nextBoolean() } +fun randomBoolean() = prepared { random.nextBoolean() } /** * Provider for a random double. @@ -293,7 +293,7 @@ fun randomBoolean() = prepared { nextBoolean() } * @see KotlinRandom.nextDouble Standard library. * @see nextDouble Direct value equivalent. */ -fun randomDouble() = prepared { nextDouble() } +fun randomDouble() = prepared { random.nextDouble() } /** * Provider for a random double. @@ -301,7 +301,7 @@ fun randomDouble() = prepared { nextDouble() } * @see KotlinRandom.nextDouble Standard library. * @see nextDouble Direct value equivalent. */ -fun randomDouble(from: Double, until: Double) = prepared { nextDouble(from, until) } +fun randomDouble(from: Double, until: Double) = prepared { random.nextDouble(from, until) } /** * Provider for a random float. @@ -309,7 +309,7 @@ fun randomDouble(from: Double, until: Double) = prepared { nextDouble(from, unti * @see KotlinRandom.nextFloat Standard library. * @see nextFloat Direct value equivalent. */ -fun randomFloat() = prepared { nextFloat() } +fun randomFloat() = prepared { random.nextFloat() } // endregion // region Warn on usages of kotlin.random.Random in tests diff --git a/suite/src/commonTest/kotlin/RandomTest.kt b/suite/src/commonTest/kotlin/RandomTest.kt index 4e11700..784e544 100644 --- a/suite/src/commonTest/kotlin/RandomTest.kt +++ b/suite/src/commonTest/kotlin/RandomTest.kt @@ -17,4 +17,20 @@ class RandomTest : PreparedSpec({ } } + val int1 by randomInt() + val int2 by randomInt() + val double by randomDouble() + val boolean by randomBoolean() + + test("Setting the seed before accessing prepared values guarantees the sequence of numbers") { + random.setSeed(43) + + assertSoftly { + int1() shouldBe -1828752340 + int2() shouldBe -1728936224 + double() shouldBe 0.9383535655576841 + boolean() shouldBe false + } + } + }) -- 2.51.2 From 21004559c1ebc023192f2944e059421c6fed3546 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 11 May 2024 20:05:43 +0200 Subject: [PATCH 4/5] fix(suite): Fix risk of double-generation of prepared values --- suite/src/commonMain/kotlin/Cache.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/suite/src/commonMain/kotlin/Cache.kt b/suite/src/commonMain/kotlin/Cache.kt index b6a716c..92acfab 100644 --- a/suite/src/commonMain/kotlin/Cache.kt +++ b/suite/src/commonMain/kotlin/Cache.kt @@ -11,7 +11,10 @@ internal class Cache { lock.withLock(key) { cache[key] } ?: run { val result = compute() - lock.withLock(key) { cache[key] = result } + lock.withLock(key) { + if (!cache.containsKey(key)) + cache[key] = result + } result } -- 2.51.2 From c1bc163d570db00b15d8f7e5660b23aad22647a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20=E2=80=9CCLOVIS=E2=80=9D=20Canet?= Date: Sat, 11 May 2024 20:34:35 +0200 Subject: [PATCH 5/5] fix(suite): Fixes the random source printing the seed 'null' to the standard output no matter what the actual seed was Also, greatly simplifies the initialization code for the generator. --- suite/src/commonMain/kotlin/Random.kt | 43 +++++++++-------------- suite/src/commonTest/kotlin/RandomTest.kt | 9 +++++ 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/suite/src/commonMain/kotlin/Random.kt b/suite/src/commonMain/kotlin/Random.kt index f68408e..273ec91 100644 --- a/suite/src/commonMain/kotlin/Random.kt +++ b/suite/src/commonMain/kotlin/Random.kt @@ -11,44 +11,33 @@ import kotlin.random.Random.Default.nextInt import kotlin.random.Random.Default.nextLong import kotlin.random.Random as KotlinRandom -private class ConfigurableRandom { +private class ConfiguredRandom( + private val explicitlyChosen: Boolean, + private val seed: Long, +) { - private lateinit var source: KotlinRandom - private val lock = Mutex() - - private var seed: Long? = null - private var explicitlyChosen: Boolean = false + val source = KotlinRandom(seed) - suspend fun setSeed(seed: Long) = lock.withLock("setSeed($seed)") { - check(!this::source.isInitialized) { "The seed has already been configured, giving the random source $source. It is not allowed to configure the seed multiple times in a single test." } - this.seed = seed - this.explicitlyChosen = true - source = KotlinRandom(seed) - } + private val lock = Mutex() suspend fun use(block: (KotlinRandom) -> T): T = lock.withLock("use") { - if (!this::source.isInitialized) { - val seed = nextLong() - this.seed = seed - source = KotlinRandom(seed) - } - block(source) } - suspend fun getUnsafe(): KotlinRandom { - use { /* force initialization */ } - return source - } - override fun toString() = "Random generator" + when (explicitlyChosen) { true -> " with the explicitly selected seed $seed" false -> " with seed $seed. To reproduce this execution, add 'random.setSeed($seed)' at the start of the test, before any random generation." } } +private val seedCacheKey = Any() + // Ensure there is exactly one instance per test -private val randomSource by prepared { ConfigurableRandom() } +@Suppress("UNCHECKED_CAST") +private val randomSource by prepared { + val (seed, chosenExplicitly) = environment.cache.cache(seedCacheKey) { nextLong() to false } as Pair + ConfiguredRandom(chosenExplicitly, seed) +} /** * Random control helper. See [random][TestDsl.random]. @@ -78,8 +67,10 @@ class Random internal constructor(private val dsl: TestDsl) { * This function can only be called before the first random value is generated for the current test, * otherwise it throws [IllegalStateException]. */ + @Suppress("UNCHECKED_CAST") suspend fun setSeed(seed: Long) = with(dsl) { - randomSource().setSeed(seed) + val (storedSeed, chosenExplicitly) = environment.cache.cache(seedCacheKey) { seed to true } as Pair + check(storedSeed == seed) { "The random generator has already been configured to use the seed $storedSeed (${if (chosenExplicitly) "explicitly chosen" else "generated randomly on first use"}), impossible to override its seed with $seed" } } /** @@ -95,7 +86,7 @@ class Random internal constructor(private val dsl: TestDsl) { * In most cases, [use] is probably sufficient. */ suspend fun accessUnsafe(): KotlinRandom = with(dsl) { - return randomSource().getUnsafe() + return randomSource().source } /** diff --git a/suite/src/commonTest/kotlin/RandomTest.kt b/suite/src/commonTest/kotlin/RandomTest.kt index 784e544..1da357a 100644 --- a/suite/src/commonTest/kotlin/RandomTest.kt +++ b/suite/src/commonTest/kotlin/RandomTest.kt @@ -2,10 +2,19 @@ package opensavvy.prepared.suite import io.kotest.assertions.assertSoftly import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import opensavvy.prepared.runner.kotest.PreparedSpec class RandomTest : PreparedSpec({ + test("Generate random values without setting a seed") { + assertSoftly { + random.nextInt() shouldNotBe 972016666 + random.nextInt() shouldNotBe 1740578880 + random.nextDouble() shouldNotBe 0.9049568172356872 + } + } + test("Setting the seed guarantees that the same sequence of numbers is generated") { random.setSeed(42)