diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/KotestTestutilPrinters.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/KotestTestutilPrinters.kt index 6d767b2cc5b..7497b3a3bc8 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/KotestTestutilPrinters.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/KotestTestutilPrinters.kt @@ -16,6 +16,7 @@ package com.google.firebase.dataconnect.testutil +import com.google.firebase.dataconnect.OptionalVariable import com.google.protobuf.Duration as DurationProto import io.kotest.assertions.print.Print import io.kotest.assertions.print.Printed @@ -33,6 +34,7 @@ fun registerDataConnectKotestTestutilPrinters() { Printers.add(Triple::class, TriplePrint) Printers.add(Quadruple::class, QuadruplePrint) Printers.add(Quintuple::class, QuintuplePrint) + Printers.add(OptionalVariable::class, OptionalVariablePrint) try { Printers.add(SignificanceResult::class, SignificanceResultPrint) @@ -101,6 +103,19 @@ private object QuintuplePrint : Print> { "${third.print().value}, ${fourth.print().value}, ${fifth.print().value})" } +private object OptionalVariablePrint : Print> { + + @Suppress("OVERRIDE_DEPRECATION") + override fun print(a: OptionalVariable<*>): Printed = a.printString.printed() + + private val OptionalVariable<*>.printString: String + get() = + when (this) { + OptionalVariable.Undefined -> "OptionalVariable.Undefined" + is OptionalVariable.Value<*> -> "OptionalVariable.Value(${value.print().value})" + } +} + private object SignificanceResultPrint : Print { @Suppress("OVERRIDE_DEPRECATION") diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/PropTestConfigExts.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/PropTestConfigExts.kt index 544f18c7f4c..0fc5efc971c 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/PropTestConfigExts.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/PropTestConfigExts.kt @@ -17,8 +17,16 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary import io.kotest.common.ExperimentalKotest +import io.kotest.property.EdgeConfig import io.kotest.property.PropTestConfig fun PropTestConfig.withIterations(iterations: Int): PropTestConfig { @OptIn(ExperimentalKotest::class) return copy(iterations = iterations) } + +fun PropTestConfig.withEdgeConfig(edgeConfig: EdgeConfig): PropTestConfig { + @OptIn(ExperimentalKotest::class) return copy(edgeConfig = edgeConfig) +} + +fun PropTestConfig.withEdgeConfigEdgeCasesOnly(): PropTestConfig = + withEdgeConfig(EdgeConfig(edgecasesGenerationProbability = 1.0)) diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/SumPartitionArb.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/SumPartitionArb.kt new file mode 100644 index 00000000000..983119ce5e9 --- /dev/null +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/SumPartitionArb.kt @@ -0,0 +1,163 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.firebase.dataconnect.testutil.property.arbitrary + +import io.kotest.assertions.print.print +import io.kotest.property.Arb +import io.kotest.property.RandomSource +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.int +import io.kotest.property.asSample + +/** + * A Kotest [Arb] that generates random partitions of a non-negative integer [sum] into a fixed + * number of non-negative parts ([summandCount]). + * + * Each generated [Sample] contains a list of integers whose size is exactly [summandCount] and + * whose elements sum up to exactly [sum]. + * + * For example, with `sum = 10` and `summandCount = 3`, generated samples could include: + * - `[3, 2, 5]` + * - `[0, 10, 0]` + * - `[1, 7, 2]` + * + * @param sum The target sum that all generated summands must add up to. Must be non-negative. + * @param summandCount The number of elements in the generated list of summands. Must be + * non-negative. If [summandCount] is `0`, then [sum] must also be `0`. + */ +class SumPartitionArb(private val sum: Int, private val summandCount: Int) : + Arb() { + + init { + require(sum >= 0) { "invalid sum: $sum" } + require(summandCount >= 0) { "invalid summandCount: $summandCount" } + require(summandCount > 0 || sum == 0) { + "invalid sum/summandCount pair: sum=$sum, summandCount=$summandCount" + } + require(sum.toLong() + summandCount - 1 <= Int.MAX_VALUE) { + "sum+summandCount-1 exceeds Int.MAX_VALUE: sum=$sum, summandCount=$summandCount" + } + } + + private val edgeCaseZeroesCountArb: Arb = run { + if (sum == 0) { + arbitrary { throw IllegalStateException("internal error h5zagzq8g4: should never get here") } + } else { + Arb.int(1 until summandCount) + } + } + + override fun edgecase(rs: RandomSource): Sample? { + if (summandCount < 2 || sum == 0) { + return null + } + + val edgeCase = Sample.EdgeCase.entries.random(rs.random) + val summands: List = + when (edgeCase) { + Sample.EdgeCase.Zeroes -> + buildList(summandCount) { + val zeroesCount = edgeCaseZeroesCountArb.next(rs, edgeCaseProbability = 0.3f) + check(zeroesCount > 0) + repeat(zeroesCount) { add(0) } + addAll(generateSummands(rs, summandCount - zeroesCount)) + shuffle(rs.random) + } + Sample.EdgeCase.SortedAscending -> generateSummands(rs, summandCount).sorted() + Sample.EdgeCase.SortedDescending -> generateSummands(rs, summandCount).sortedDescending() + } + + return Sample(summands, edgeCase) + } + + override fun sample(rs: RandomSource): io.kotest.property.Sample { + val summands = generateSummands(rs, summandCount) + val sample = Sample(summands, edgeCase = null) + return sample.asSample() + } + + private fun generateSummands( + rs: RandomSource, + count: Int, + ): List { + if (count == 0) { + return emptyList() + } + if (count == 1) { + return listOf(sum) + } + + val maxPosition = sum + count - 1 + val cuts: List = + if (count - 1 < sum) { + buildSet { + while (size < count - 1) { + add(rs.random.nextInt(maxPosition)) + } + } + .sorted() + } else { + val nonCuts = + buildSet { + while (size < sum) { + add(rs.random.nextInt(maxPosition)) + } + } + .sorted() + buildList(count - 1) { + var prev = -1 + for (nonCut in nonCuts) { + for (v in (prev + 1) until nonCut) { + add(v) + } + prev = nonCut + } + for (v in (prev + 1) until maxPosition) { + add(v) + } + } + } + + return buildList(count) { + var prev = -1 + for (cut in cuts) { + add(cut - prev - 1) + prev = cut + } + add(maxPosition - prev - 1) + } + } + + class Sample( + val summands: List, + val edgeCase: EdgeCase?, + ) { + + override fun equals(other: Any?) = other is Sample && other.summands == summands + + override fun hashCode() = summands.hashCode() + + override fun toString() = + "SumPartitionArb.Sample(summands=${summands.print().value}, edgeCase=${edgeCase?.name})" + + enum class EdgeCase { + Zeroes, + SortedAscending, + SortedDescending, + } + } +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt index b6add3f0377..4e83edc5864 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbs.kt @@ -24,6 +24,7 @@ import com.google.firebase.dataconnect.CacheSettings import com.google.firebase.dataconnect.ConnectorConfig import com.google.firebase.dataconnect.DataConnectPathSegment import com.google.firebase.dataconnect.DataConnectSettings +import com.google.firebase.dataconnect.OptionalVariable import com.google.firebase.dataconnect.testutil.ImmediateDeferred import com.google.firebase.dataconnect.testutil.LoggedInInternalAuthProvider import com.google.firebase.dataconnect.testutil.LoggedInMultiTokenInternalAuthProvider @@ -329,6 +330,57 @@ object DataConnectArb { max: com.google.protobuf.Duration? = null, ): Arb = Arb.proto.duration(min = min, max = max).map { it.duration } + + fun optionalVariable( + arb: Arb, + undefinedProbability: Double, + ): Arb> { + require(undefinedProbability in 0.0..1.0) { + "invalid undefinedProbability: ${undefinedProbability.print().value}" + } + + return arbitrary { rs -> + if (rs.random.nextDouble() < undefinedProbability) { + OptionalVariable.Undefined + } else { + OptionalVariable.Value(arb.bind()) + } + } + } + + fun nullableOptionalVariable( + arb: Arb, + undefinedProbability: Double, + nullableProbability: Double, + ): Arb> { + require(undefinedProbability in 0.0..1.0) { + "invalid undefinedProbability: ${undefinedProbability.print().value}" + } + require(nullableProbability in 0.0..1.0) { + "invalid nullableProbability: ${nullableProbability.print().value}" + } + val probabilitiesSum = undefinedProbability + nullableProbability + require(probabilitiesSum <= 1.0) { + "invalid undefinedProbability/nullableProbability pair: " + + "their sum must be less than or equal to 1.0, " + + "but their sum is ${(probabilitiesSum).print().value}, " + + "which is ${(probabilitiesSum - 1.0).print().value} " + + "greater than 1.0; " + + "undefinedProbability=${undefinedProbability.print().value}, " + + "nullableProbability=${nullableProbability.print().value}" + } + + return arbitrary { rs -> + val discriminator = rs.random.nextDouble() + if (discriminator < undefinedProbability) { + OptionalVariable.Undefined + } else if (discriminator < probabilitiesSum) { + OptionalVariable.Value(null) + } else { + OptionalVariable.Value(arb.bind()) + } + } + } } private class DataConnectPathArb( diff --git a/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/SumPartitionArbUnitTest.kt b/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/SumPartitionArbUnitTest.kt new file mode 100644 index 00000000000..7bf0297832c --- /dev/null +++ b/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/SumPartitionArbUnitTest.kt @@ -0,0 +1,185 @@ +/* + * Copyright 2026 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalKotest::class) + +package com.google.firebase.dataconnect.testutil.property.arbitrary + +import com.google.firebase.dataconnect.testutil.property.arbitrary.SumPartitionArb.Sample.EdgeCase +import io.kotest.assertions.assertSoftly +import io.kotest.assertions.print.print +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.assertions.withClue +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.booleans.shouldBeFalse +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.ints.shouldBeGreaterThan +import io.kotest.matchers.ints.shouldBeGreaterThanOrEqual +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.RandomSource +import io.kotest.property.arbitrary.bind +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.nonNegativeInt +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.apache.commons.statistics.inference.ChiSquareTest +import org.junit.Test + +class SumPartitionArbUnitTest { + + @Test + fun `produces valid partitions`() = runTest { + checkAll(propTestConfig, Arb.int(0..100), Arb.int(1..20)) { sum, summandCount -> + val arb = SumPartitionArb(sum, summandCount) + val sample = arb.bind() + assertSoftly { + withClue("summandCount") { sample.summands.size shouldBe summandCount } + withClue("sum") { sample.summands.sum() shouldBe sum } + withClue("all elements non-negative") { + sample.summands.forEach { it shouldBeGreaterThanOrEqual 0 } + } + } + } + } + + @Test + fun `zero sum and summandCount produces empty list`() = runTest { + checkAll(SumPartitionArb(sum = 0, summandCount = 0)) { sample -> + assertSoftly { sample.summands shouldBe emptyList() } + } + } + + @Test + fun `summandCount of one produces single element list containing sum`() = runTest { + checkAll(propTestConfig, Arb.int(0..100)) { sum -> + val arb = SumPartitionArb(sum, summandCount = 1) + val sample = arb.bind() + assertSoftly { sample.summands shouldBe listOf(sum) } + } + } + + @Test + fun `invalid parameters throw IllegalArgumentException`() { + shouldThrow { SumPartitionArb(sum = -1, summandCount = 5) } + shouldThrow { SumPartitionArb(sum = 10, summandCount = -1) } + shouldThrow { SumPartitionArb(sum = 10, summandCount = 0) } + } + + @Test + fun `edgecase(summandCount=0) returns null`() = runTest { + val arb = SumPartitionArb(sum = 0, summandCount = 0) + val rs = RandomSource.default() + + arb.edgecase(rs) shouldBe null + } + + @Test + fun `edgecase(summandCount=1) returns null`() = runTest { + checkAll(propTestConfig, Arb.nonNegativeInt()) { sum -> + val arb = SumPartitionArb(sum = sum, summandCount = 1) + + arb.edgecase(randomSource()) shouldBe null + } + } + + @Test + fun `edgecase(sum=0) returns null`() = runTest { + checkAll(propTestConfig, Arb.nonNegativeInt()) { summandCount -> + val arb = SumPartitionArb(sum = 0, summandCount = summandCount) + + arb.edgecase(randomSource()) shouldBe null + } + } + + @Test + fun `edgecase returns valid edge case samples`() = runTest { + checkAll(propTestConfig.withEdgeConfigEdgeCasesOnly(), nonNullEdgeCaseParametersArb()) { + (sum, summandCount) -> + val arb = SumPartitionArb(sum = sum, summandCount = summandCount) + + val sample = arb.edgecase(randomSource()) + + withClue("sample=$sample") { + sample.shouldNotBeNull() + sample.edgeCase.shouldNotBeNull() + when (sample.edgeCase) { + EdgeCase.Zeroes -> sample.summands.count { it == 0 } shouldBeGreaterThan 0 + EdgeCase.SortedAscending -> sample.summands shouldContainExactly sample.summands.sorted() + EdgeCase.SortedDescending -> + sample.summands shouldContainExactly sample.summands.sortedDescending() + } + } + } + } + + @Test + fun `edgecase returns even distribution of edge case types`() = runTest { + val edgeCases = EdgeCase.entries.associateWith { 0 }.toMutableMap() + + checkAll(propTestConfig.withEdgeConfigEdgeCasesOnly(), nonNullEdgeCaseParametersArb()) { + (sum, summandCount) -> + val arb = SumPartitionArb(sum = sum, summandCount = summandCount) + + val sample = arb.edgecase(randomSource()) + + checkNotNull(sample?.edgeCase) + edgeCases[sample.edgeCase] = edgeCases[sample.edgeCase]!! + 1 + } + + withClue("edgeCases=${edgeCases.print().value}") { + val iterations = edgeCases.values.sum() + val observedCounts = edgeCases.values.map { it.toLong() }.toLongArray() + val expectedObservedCount = iterations.toDouble() / observedCounts.size + val expectedCounts = DoubleArray(observedCounts.size) { expectedObservedCount } + val significanceResult = ChiSquareTest.withDefaults().test(expectedCounts, observedCounts) + withClue("significanceResult=${significanceResult.print().value}") { + significanceResult.reject(0.00001).shouldBeFalse() + } + } + } + + @Test + fun `generateSummands branch count minus one is greater than or equal to sum`() = runTest { + checkAll(propTestConfig, Arb.int(0..10), Arb.int(12..100)) { sum, summandCount -> + val arb = SumPartitionArb(sum, summandCount) + + val sample = arb.sample(randomSource()).value + + withClue("sample=$sample") { + withClue("summands.size") { sample.summands.size shouldBe summandCount } + withClue("summands.sum()") { sample.summands.sum() shouldBe sum } + withClue("all elements non-negative") { + sample.summands.forEach { it shouldBeGreaterThanOrEqual 0 } + } + } + } + } +} + +private val propTestConfig = + PropTestConfig(iterations = 200, edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2)) + +private data class NonNullEdgeCaseParameters(val sum: Int, val summandCount: Int) + +private fun nonNullEdgeCaseParametersArb(): Arb { + val sumArb = Arb.intWithEvenNumDigitsDistribution(1..999_999_999) + val summandCountArb = Arb.int(2..100) + return Arb.bind(sumArb, summandCountArb, ::NonNullEdgeCaseParameters) +} diff --git a/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbsUnitTest.kt b/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbsUnitTest.kt index 67de380bdb4..b0bb5944f25 100644 --- a/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbsUnitTest.kt +++ b/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/arbsUnitTest.kt @@ -18,16 +18,24 @@ package com.google.firebase.dataconnect.testutil.property.arbitrary +import com.google.firebase.dataconnect.OptionalVariable import io.kotest.assertions.assertSoftly +import io.kotest.assertions.print.print import io.kotest.assertions.withClue import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.ints.shouldBeInRange import io.kotest.matchers.longs.shouldBeGreaterThanOrEqual +import io.kotest.matchers.nulls.shouldBeNull +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf import io.kotest.property.Arb import io.kotest.property.PropTestConfig +import io.kotest.property.arbitrary.int import io.kotest.property.checkAll -import kotlin.ranges.rangeTo import kotlinx.coroutines.test.runTest +import org.apache.commons.statistics.inference.ChiSquareTest import org.junit.Test class ArbsUnitTest { @@ -58,6 +66,181 @@ class ArbsUnitTest { } } } + + @Test + fun `optionalVariable(undefinedProbability=1)`() = runTest { + val arb = Arb.dataConnect.optionalVariable(Arb.any(), undefinedProbability = 1.0) + checkAll(propTestConfig, arb) { optionalVariable -> + optionalVariable shouldBe OptionalVariable.Undefined + } + } + + @Test + fun `optionalVariable(undefinedProbability=0)`() = runTest { + val arb = Arb.dataConnect.optionalVariable(Arb.any(), undefinedProbability = 0.0) + checkAll(propTestConfig, arb) { optionalVariable -> + optionalVariable.shouldBeInstanceOf>() + } + } + + @Test + fun `optionalVariable(undefinedProbability=0point5)`() = runTest { + val arb = Arb.dataConnect.optionalVariable(Arb.any(), undefinedProbability = 0.5) + var undefinedCount = 0 + var valueCount = 0 + checkAll(propTestConfig, arb) { optionalVariable -> + when (optionalVariable) { + OptionalVariable.Undefined -> undefinedCount++ + is OptionalVariable.Value<*> -> valueCount++ + } + } + + withClue("undefinedCount=$undefinedCount, valueCount=$valueCount") { + val iterations = undefinedCount + valueCount + val observedCounts = longArrayOf(undefinedCount.toLong(), valueCount.toLong()) + val expectedObservedCount = iterations.toDouble() / observedCounts.size + val expectedCounts = DoubleArray(observedCounts.size) { expectedObservedCount } + val significanceResult = ChiSquareTest.withDefaults().test(expectedCounts, observedCounts) + withClue("significanceResult=${significanceResult.print().value}") { + significanceResult.reject(0.00001).shouldBeFalse() + } + } + } + + @Test + fun `optionalVariable(arb) produces values from the given Arb`() = runTest { + val arb = Arb.dataConnect.optionalVariable(Arb.int(-1000..1000), undefinedProbability = 0.0) + checkAll(propTestConfig, arb) { optionalVariable -> + check(optionalVariable is OptionalVariable.Value) + optionalVariable.value shouldBeInRange -1000..1000 + } + } + + @Test + fun `nullableOptionalVariable(undefinedProbability=1)`() = runTest { + val arb = + Arb.dataConnect.nullableOptionalVariable( + Arb.any(), + undefinedProbability = 1.0, + nullableProbability = 0.0, + ) + checkAll(propTestConfig, arb) { nullableOptionalVariable -> + nullableOptionalVariable shouldBe OptionalVariable.Undefined + } + } + + @Test + fun `nullableOptionalVariable(nullableProbability=1)`() = runTest { + val arb = + Arb.dataConnect.nullableOptionalVariable( + Arb.any(), + undefinedProbability = 0.0, + nullableProbability = 1.0, + ) + checkAll(propTestConfig, arb) { nullableOptionalVariable -> + nullableOptionalVariable.shouldBeInstanceOf>() + nullableOptionalVariable.value.shouldBeNull() + } + } + + @Test + fun `nullableOptionalVariable(undefinedProbability and nullableProbability = 0)`() = runTest { + val arb = + Arb.dataConnect.nullableOptionalVariable( + Arb.any(), + undefinedProbability = 0.0, + nullableProbability = 0.0, + ) + checkAll(propTestConfig, arb) { nullableOptionalVariable -> + nullableOptionalVariable.shouldBeInstanceOf>() + } + } + + @Test + fun `nullableOptionalVariable(undefinedProbability and nullableProbability = 0point5)`() = + runTest { + val arb = + Arb.dataConnect.nullableOptionalVariable( + Arb.any(), + undefinedProbability = 0.5, + nullableProbability = 0.5, + ) + var undefinedCount = 0 + var nullCount = 0 + checkAll(propTestConfig, arb) { nullableOptionalVariable -> + when (nullableOptionalVariable) { + OptionalVariable.Undefined -> undefinedCount++ + is OptionalVariable.Value -> { + check(nullableOptionalVariable.value == null) + nullCount++ + } + } + } + + withClue("undefinedCount=$undefinedCount, nullCount=$nullCount") { + val iterations = undefinedCount + nullCount + val observedCounts = longArrayOf(undefinedCount.toLong(), nullCount.toLong()) + val expectedObservedCount = iterations.toDouble() / observedCounts.size + val expectedCounts = DoubleArray(observedCounts.size) { expectedObservedCount } + val significanceResult = ChiSquareTest.withDefaults().test(expectedCounts, observedCounts) + withClue("significanceResult=${significanceResult.print().value}") { + significanceResult.reject(0.00001).shouldBeFalse() + } + } + } + + @Test + fun `nullableOptionalVariable(undefinedProbability and nullableProbability = 0point33333)`() = + runTest { + val oneThird = 1.0 / 3.0 + val arb = + Arb.dataConnect.nullableOptionalVariable( + Arb.any(), + undefinedProbability = oneThird, + nullableProbability = oneThird, + ) + var undefinedCount = 0 + var nullCount = 0 + var nonNullCount = 0 + checkAll(propTestConfig, arb) { nullableOptionalVariable -> + when (nullableOptionalVariable) { + OptionalVariable.Undefined -> undefinedCount++ + is OptionalVariable.Value -> { + if (nullableOptionalVariable.value == null) { + nullCount++ + } else { + nonNullCount++ + } + } + } + } + + withClue("undefinedCount=$undefinedCount, nullCount=$nullCount nonNullCount=$nonNullCount") { + val iterations = undefinedCount + nullCount + nonNullCount + val observedCounts = + longArrayOf(undefinedCount.toLong(), nullCount.toLong(), nonNullCount.toLong()) + val expectedObservedCount = iterations.toDouble() / observedCounts.size + val expectedCounts = DoubleArray(observedCounts.size) { expectedObservedCount } + val significanceResult = ChiSquareTest.withDefaults().test(expectedCounts, observedCounts) + withClue("significanceResult=${significanceResult.print().value}") { + significanceResult.reject(0.00001).shouldBeFalse() + } + } + } + + @Test + fun `nullableOptionalVariable(arb) produces values from the given Arb`() = runTest { + val arb = + Arb.dataConnect.nullableOptionalVariable( + Arb.int(-1000..1000), + undefinedProbability = 0.0, + nullableProbability = 0.0, + ) + checkAll(propTestConfig, arb) { nullableOptionalVariable -> + check(nullableOptionalVariable is OptionalVariable.Value) + nullableOptionalVariable.value.shouldNotBeNull() shouldBeInRange -1000..1000 + } + } } private val propTestConfig = PropTestConfig(iterations = 1000)