From a340023c65242ee272fab05acd91b3d51c1c0d8a Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 10 Jul 2026 16:12:02 -0400 Subject: [PATCH 1/7] MutablePrimitives.kt added --- .../dataconnect/testutil/MutablePrimitives.kt | 94 ++ .../testutil/property/arbitrary/any.kt | 134 ++ .../property/arbitrary/mutableprimitives.kt | 65 + .../testutil/MutablePrimitivesUnitTest.kt | 1493 +++++++++++++++++ 4 files changed, 1786 insertions(+) create mode 100644 firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/MutablePrimitives.kt create mode 100644 firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/any.kt create mode 100644 firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/mutableprimitives.kt create mode 100644 firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/MutablePrimitivesUnitTest.kt diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/MutablePrimitives.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/MutablePrimitives.kt new file mode 100644 index 00000000000..b33543fa0cb --- /dev/null +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/MutablePrimitives.kt @@ -0,0 +1,94 @@ +/* + * 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 + +class MutableBoolean(var value: Boolean) { + override fun toString() = value.toString() + + override fun hashCode() = value.hashCode() + + override fun equals(other: Any?) = other is MutableBoolean && other.value == value +} + +class MutableByte(var value: Byte) { + override fun toString() = value.toString() + + override fun hashCode() = value.hashCode() + + override fun equals(other: Any?) = other is MutableByte && other.value == value +} + +class MutableShort(var value: Short) { + override fun toString() = value.toString() + + override fun hashCode() = value.hashCode() + + override fun equals(other: Any?) = other is MutableShort && other.value == value +} + +class MutableInt(var value: Int) { + override fun toString() = value.toString() + + override fun hashCode() = value.hashCode() + + override fun equals(other: Any?) = other is MutableInt && other.value == value +} + +class MutableLong(var value: Long) { + override fun toString() = value.toString() + + override fun hashCode() = value.hashCode() + + override fun equals(other: Any?) = other is MutableLong && other.value == value +} + +class MutableChar(var value: Char) { + override fun toString() = value.toString() + + override fun hashCode() = value.hashCode() + + override fun equals(other: Any?) = other is MutableChar && other.value == value +} + +class MutableFloat(var value: Float) { + override fun toString() = value.toString() + + override fun hashCode() = value.hashCode() + + // Use compareTo instead of == because == is false for NaN == NaN (violating reflexivity) + // and true for -0.0f == 0.0f (violating equals/hashCode contract since they have different hash + // codes). + override fun equals(other: Any?) = other is MutableFloat && value.compareTo(other.value) == 0 +} + +class MutableDouble(var value: Double) { + override fun toString() = value.toString() + + override fun hashCode() = value.hashCode() + + // Use compareTo instead of == because == is false for NaN == NaN (violating reflexivity) + // and true for -0.0 == 0.0 (violating equals/hashCode contract since they have different hash + // codes). + override fun equals(other: Any?) = other is MutableDouble && value.compareTo(other.value) == 0 +} + +class MutableReference(var value: T) { + override fun toString() = value.toString() + + override fun hashCode() = value.hashCode() + + override fun equals(other: Any?) = other is MutableReference<*> && other.value == value +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/any.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/any.kt new file mode 100644 index 00000000000..18f52672d41 --- /dev/null +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/any.kt @@ -0,0 +1,134 @@ +/* + * 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 com.google.firebase.dataconnect.testutil.MutableBoolean +import com.google.firebase.dataconnect.testutil.MutableByte +import com.google.firebase.dataconnect.testutil.MutableChar +import com.google.firebase.dataconnect.testutil.MutableDouble +import com.google.firebase.dataconnect.testutil.MutableFloat +import com.google.firebase.dataconnect.testutil.MutableInt +import com.google.firebase.dataconnect.testutil.MutableLong +import com.google.firebase.dataconnect.testutil.MutableReference +import com.google.firebase.dataconnect.testutil.MutableShort +import io.kotest.property.Arb +import io.kotest.property.arbitrary.Codepoint +import io.kotest.property.arbitrary.array +import io.kotest.property.arbitrary.az +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.byte +import io.kotest.property.arbitrary.char +import io.kotest.property.arbitrary.choice +import io.kotest.property.arbitrary.double +import io.kotest.property.arbitrary.float +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.javaDate +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.long +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.pair +import io.kotest.property.arbitrary.set +import io.kotest.property.arbitrary.short +import io.kotest.property.arbitrary.string +import io.kotest.property.arbitrary.triple +import java.util.Date +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger +import java.util.concurrent.atomic.AtomicLong +import java.util.concurrent.atomic.AtomicReference +import kotlin.reflect.KClass + +fun Arb.Companion.any( + excludes: List> = emptyList(), + extras: List> = emptyList(), +): Arb { + val arbs = arbs(excludes, extras) + return Arb.choice(arbs.toList()) +} + +fun Arb.Companion.any(exclude: KClass<*>): Arb = any(excludes = listOf(exclude)) + +fun Arb.Companion.any(exclude: KClass<*>, extra: Arb): Arb = + any(excludes = listOf(exclude), extras = listOf(extra)) + +private class ScalarArbFactory(val type: KClass, val factory: () -> Arb) + +private val allScalarArbFactories = + listOf( + ScalarArbFactory(String::class) { Arb.string(minSize = 0, maxSize = 10, Codepoint.az()) }, + ScalarArbFactory(Boolean::class, Arb.Companion::boolean), + ScalarArbFactory(Byte::class, Arb.Companion::byte), + ScalarArbFactory(Short::class, Arb.Companion::short), + ScalarArbFactory(Int::class, Arb.Companion::int), + ScalarArbFactory(Long::class, Arb.Companion::long), + ScalarArbFactory(Char::class, Arb.Companion::char), + ScalarArbFactory(Float::class, Arb.Companion::float), + ScalarArbFactory(Double::class, Arb.Companion::double), + ScalarArbFactory(MutableBoolean::class, Arb.Companion::mutableBoolean), + ScalarArbFactory(MutableByte::class, Arb.Companion::mutableByte), + ScalarArbFactory(MutableShort::class, Arb.Companion::mutableShort), + ScalarArbFactory(MutableInt::class, Arb.Companion::mutableInt), + ScalarArbFactory(MutableLong::class, Arb.Companion::mutableLong), + ScalarArbFactory(MutableChar::class, Arb.Companion::mutableChar), + ScalarArbFactory(MutableFloat::class, Arb.Companion::mutableFloat), + ScalarArbFactory(MutableDouble::class, Arb.Companion::mutableDouble), + ScalarArbFactory(AtomicBoolean::class) { Arb.boolean().map(::AtomicBoolean) }, + ScalarArbFactory(AtomicInteger::class) { Arb.int().map(::AtomicInteger) }, + ScalarArbFactory(AtomicLong::class) { Arb.long().map(::AtomicLong) }, + ScalarArbFactory(Date::class, Arb.Companion::javaDate), + ) + +private fun arbs( + excludes: List> = emptyList(), + extras: List> = emptyList(), +): Sequence> = sequence { + val scalarArbs = buildList { + allScalarArbFactories.forEach { + if (it.type !in excludes) { + add(it.factory()) + } + addAll(extras) + } + } + + scalarArbs.forEach { yield(it) } + val scalarArb = Arb.choice(scalarArbs) + + if (Map::class !in excludes) { + yield(Arb.map(scalarArb, scalarArb, minSize = 0, maxSize = 5)) + } + if (List::class !in excludes) { + yield(Arb.list(scalarArb, 0..5)) + } + if (Set::class !in excludes) { + yield(Arb.set(scalarArb, 0..5)) + } + if (Array::class !in excludes) { + yield(Arb.array(scalarArb, 0..5)) + } + if (MutableReference::class !in excludes) { + yield(scalarArb.map(::MutableReference)) + } + if (AtomicReference::class !in excludes) { + yield(scalarArb.map(::AtomicReference)) + } + if (Pair::class !in excludes) { + yield(Arb.pair(scalarArb, scalarArb)) + } + if (Triple::class !in excludes) { + yield(Arb.triple(scalarArb, scalarArb, scalarArb)) + } +} diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/mutableprimitives.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/mutableprimitives.kt new file mode 100644 index 00000000000..2f8e82cb89e --- /dev/null +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/mutableprimitives.kt @@ -0,0 +1,65 @@ +/* + * 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:Suppress("UnusedReceiverParameter") + +package com.google.firebase.dataconnect.testutil.property.arbitrary + +import com.google.firebase.dataconnect.testutil.MutableBoolean +import com.google.firebase.dataconnect.testutil.MutableByte +import com.google.firebase.dataconnect.testutil.MutableChar +import com.google.firebase.dataconnect.testutil.MutableDouble +import com.google.firebase.dataconnect.testutil.MutableFloat +import com.google.firebase.dataconnect.testutil.MutableInt +import com.google.firebase.dataconnect.testutil.MutableLong +import com.google.firebase.dataconnect.testutil.MutableReference +import com.google.firebase.dataconnect.testutil.MutableShort +import io.kotest.property.Arb +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.byte +import io.kotest.property.arbitrary.char +import io.kotest.property.arbitrary.double +import io.kotest.property.arbitrary.float +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.long +import io.kotest.property.arbitrary.map +import io.kotest.property.arbitrary.short + +fun Arb.Companion.mutableBoolean(boolean: Arb = Arb.boolean()): Arb = + boolean.map(::MutableBoolean) + +fun Arb.Companion.mutableByte(byte: Arb = Arb.byte()): Arb = + byte.map(::MutableByte) + +fun Arb.Companion.mutableShort(short: Arb = Arb.short()): Arb = + short.map(::MutableShort) + +fun Arb.Companion.mutableInt(int: Arb = Arb.int()): Arb = int.map(::MutableInt) + +fun Arb.Companion.mutableLong(long: Arb = Arb.long()): Arb = + long.map(::MutableLong) + +fun Arb.Companion.mutableChar(char: Arb = Arb.char()): Arb = + char.map(::MutableChar) + +fun Arb.Companion.mutableFloat(float: Arb = Arb.float()): Arb = + float.map(::MutableFloat) + +fun Arb.Companion.mutableDouble(double: Arb = Arb.double()): Arb = + double.map(::MutableDouble) + +fun Arb.Companion.mutableReference(any: Arb = Arb.any()): Arb> = + any.map(::MutableReference) diff --git a/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/MutablePrimitivesUnitTest.kt b/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/MutablePrimitivesUnitTest.kt new file mode 100644 index 00000000000..4a6bd917f59 --- /dev/null +++ b/firebase-dataconnect/testutil/src/test/kotlin/com/google/firebase/dataconnect/testutil/MutablePrimitivesUnitTest.kt @@ -0,0 +1,1493 @@ +/* + * 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 + +import com.google.firebase.dataconnect.testutil.property.arbitrary.any +import com.google.firebase.dataconnect.testutil.property.arbitrary.distinctPair +import com.google.firebase.dataconnect.testutil.property.arbitrary.pair +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.kotest.property.Arb +import io.kotest.property.EdgeConfig +import io.kotest.property.PropTestConfig +import io.kotest.property.ShrinkingMode +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.byte +import io.kotest.property.arbitrary.char +import io.kotest.property.arbitrary.double +import io.kotest.property.arbitrary.float +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.list +import io.kotest.property.arbitrary.long +import io.kotest.property.arbitrary.of +import io.kotest.property.arbitrary.short +import io.kotest.property.checkAll +import kotlinx.coroutines.test.runTest +import org.junit.Test + +class MutablePrimitivesUnitTest { + + @Test + fun `MutableBoolean value initialization from constructor`() = runTest { + checkAll(propTestConfig, Arb.boolean()) { booleanValue -> + val mutableBoolean = MutableBoolean(booleanValue) + mutableBoolean.value shouldBe booleanValue + } + } + + @Test + fun `MutableBoolean toString() initial value`() = runTest { + checkAll(propTestConfig, Arb.boolean()) { booleanValue -> + val mutableBoolean = MutableBoolean(booleanValue) + mutableBoolean.toString() shouldBe booleanValue.toString() + } + } + + @Test + fun `MutableBoolean toString() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.boolean(), Arb.list(Arb.boolean(), 10..10)) { + initialBooleanValue, + booleanValues -> + val mutableBoolean = MutableBoolean(initialBooleanValue) + booleanValues.forEach { booleanValue -> + mutableBoolean.value = booleanValue + mutableBoolean.toString() shouldBe booleanValue.toString() + } + } + } + + @Test + fun `MutableBoolean hashCode() initial value`() = runTest { + checkAll(propTestConfig, Arb.boolean()) { booleanValue -> + val mutableBoolean = MutableBoolean(booleanValue) + mutableBoolean.hashCode() shouldBe booleanValue.hashCode() + } + } + + @Test + fun `MutableBoolean hashCode() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.boolean(), Arb.list(Arb.boolean(), 10..10)) { + initialBooleanValue, + booleanValues -> + val mutableBoolean = MutableBoolean(initialBooleanValue) + booleanValues.forEach { booleanValue -> + mutableBoolean.value = booleanValue + mutableBoolean.hashCode() shouldBe booleanValue.hashCode() + } + } + } + + @Test + fun `MutableBoolean hashCode() and equals() consistency`() = runTest { + checkAll(propTestConfig, Arb.boolean()) { booleanValue -> + val mutableBoolean1 = MutableBoolean(booleanValue) + val mutableBoolean2 = MutableBoolean(booleanValue) + check(mutableBoolean1 == mutableBoolean2) + mutableBoolean1.hashCode() shouldBe mutableBoolean2.hashCode() + } + } + + @Test + fun `MutableBoolean equals(self)`() = runTest { + checkAll(propTestConfig, Arb.boolean()) { booleanValue -> + val mutableBoolean = MutableBoolean(booleanValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableBoolean.equals(mutableBoolean) shouldBe true + } + } + + @Test + fun `MutableBoolean equals(equal, but distinct, instance)`() = runTest { + checkAll(propTestConfig, Arb.boolean()) { booleanValue -> + val mutableBoolean1 = MutableBoolean(booleanValue) + val mutableBoolean2 = MutableBoolean(booleanValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableBoolean1.equals(mutableBoolean2) shouldBe true + } + } + + @Test + fun `MutableBoolean equals(equal, but distinct, instance, after mutations)`() = runTest { + checkAll(propTestConfig, Arb.list(Arb.boolean(), 10..10).pair(), Arb.boolean()) { + (booleanValues1, booleanValues2), + finalBooleanValue -> + val mutableBoolean1 = MutableBoolean(booleanValues1[0]) + val mutableBoolean2 = MutableBoolean(booleanValues2[0]) + booleanValues1.drop(1).forEach { mutableBoolean1.value = it } + booleanValues2.drop(1).forEach { mutableBoolean2.value = it } + mutableBoolean1.value = finalBooleanValue + mutableBoolean2.value = finalBooleanValue + @Suppress("ReplaceCallWithBinaryOperator") + mutableBoolean1.equals(mutableBoolean2) shouldBe true + } + } + + @Test + fun `MutableBoolean equals(unequal instance)`() = runTest { + checkAll(propTestConfig, Arb.boolean()) { booleanValue -> + val mutableBoolean1 = MutableBoolean(booleanValue) + val mutableBoolean2 = MutableBoolean(!booleanValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableBoolean1.equals(mutableBoolean2) shouldBe false + } + } + + @Test + fun `MutableBoolean equals(unequal instance, after mutations)`() = runTest { + checkAll( + propTestConfig, + Arb.boolean(), + Arb.list(Arb.boolean(), 10..10).pair(), + Arb.boolean() + ) { initialBooleanValue, (booleanValues1, booleanValues2), finalMutation -> + val mutableBoolean1 = MutableBoolean(initialBooleanValue) + val mutableBoolean2 = MutableBoolean(initialBooleanValue) + booleanValues1.forEach { mutableBoolean1.value = it } + booleanValues2.forEach { mutableBoolean2.value = it } + if (finalMutation) { + mutableBoolean1.value = !booleanValues2.last() + } else { + mutableBoolean2.value = !booleanValues1.last() + } + @Suppress("ReplaceCallWithBinaryOperator") + mutableBoolean1.equals(mutableBoolean2) shouldBe false + } + } + + @Test + fun `MutableBoolean equals(value)`() = runTest { + checkAll(propTestConfig, Arb.boolean()) { booleanValue -> + val mutableBoolean = MutableBoolean(booleanValue) + mutableBoolean.equals(mutableBoolean.value) shouldBe false + } + } + + @Test + fun `MutableBoolean equals(null)`() = runTest { + checkAll(propTestConfig, Arb.boolean()) { booleanValue -> + val mutableBoolean = MutableBoolean(booleanValue) + mutableBoolean.equals(null) shouldBe false + } + } + + @Test + fun `MutableBoolean equals(different type)`() = runTest { + checkAll( + propTestConfig, + Arb.boolean(), + Arb.any(exclude = MutableBoolean::class, extra = Arb.boolean()) + ) { booleanValue, other -> + val mutableBoolean = MutableBoolean(booleanValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableBoolean.equals(other) shouldBe false + } + } + + @Test + fun `MutableByte value initialization from constructor`() = runTest { + checkAll(propTestConfig, Arb.byte()) { byteValue -> + val mutableByte = MutableByte(byteValue) + mutableByte.value shouldBe byteValue + } + } + + @Test + fun `MutableByte toString() initial value`() = runTest { + checkAll(propTestConfig, Arb.byte()) { byteValue -> + val mutableByte = MutableByte(byteValue) + mutableByte.toString() shouldBe byteValue.toString() + } + } + + @Test + fun `MutableByte toString() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.byte(), Arb.list(Arb.byte(), 10..10)) { + initialByteValue, + byteValues -> + val mutableByte = MutableByte(initialByteValue) + byteValues.forEach { byteValue -> + mutableByte.value = byteValue + mutableByte.toString() shouldBe byteValue.toString() + } + } + } + + @Test + fun `MutableByte hashCode() initial value`() = runTest { + checkAll(propTestConfig, Arb.byte()) { byteValue -> + val mutableByte = MutableByte(byteValue) + mutableByte.hashCode() shouldBe byteValue.hashCode() + } + } + + @Test + fun `MutableByte hashCode() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.byte(), Arb.list(Arb.byte(), 10..10)) { + initialByteValue, + byteValues -> + val mutableByte = MutableByte(initialByteValue) + byteValues.forEach { byteValue -> + mutableByte.value = byteValue + mutableByte.hashCode() shouldBe byteValue.hashCode() + } + } + } + + @Test + fun `MutableByte hashCode() and equals() consistency`() = runTest { + checkAll(propTestConfig, Arb.byte()) { byteValue -> + val mutableByte1 = MutableByte(byteValue) + val mutableByte2 = MutableByte(byteValue) + check(mutableByte1 == mutableByte2) + mutableByte1.hashCode() shouldBe mutableByte2.hashCode() + } + } + + @Test + fun `MutableByte equals(self)`() = runTest { + checkAll(propTestConfig, Arb.byte()) { byteValue -> + val mutableByte = MutableByte(byteValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableByte.equals(mutableByte) shouldBe true + } + } + + @Test + fun `MutableByte equals(equal, but distinct, instance)`() = runTest { + checkAll(propTestConfig, Arb.byte()) { byteValue -> + val mutableByte1 = MutableByte(byteValue) + val mutableByte2 = MutableByte(byteValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableByte1.equals(mutableByte2) shouldBe true + } + } + + @Test + fun `MutableByte equals(equal, but distinct, instance, after mutations)`() = runTest { + checkAll(propTestConfig, Arb.list(Arb.byte(), 10..10).pair(), Arb.byte()) { + (byteValues1, byteValues2), + finalByteValue -> + val mutableByte1 = MutableByte(byteValues1[0]) + val mutableByte2 = MutableByte(byteValues2[0]) + byteValues1.drop(1).forEach { mutableByte1.value = it } + byteValues2.drop(1).forEach { mutableByte2.value = it } + mutableByte1.value = finalByteValue + mutableByte2.value = finalByteValue + @Suppress("ReplaceCallWithBinaryOperator") + mutableByte1.equals(mutableByte2) shouldBe true + } + } + + @Test + fun `MutableByte equals(unequal instance)`() = runTest { + checkAll(propTestConfig, Arb.byte().distinctPair()) { (byteValue1, byteValue2) -> + val mutableByte1 = MutableByte(byteValue1) + val mutableByte2 = MutableByte(byteValue2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableByte1.equals(mutableByte2) shouldBe false + } + } + + @Test + fun `MutableByte equals(unequal instance, after mutations)`() = runTest { + checkAll( + propTestConfig, + Arb.byte(), + Arb.list(Arb.byte(), 10..10).pair(), + Arb.byte().distinctPair(), + ) { initialByteValue, (byteValues1, byteValues2), (finalByteValue1, finalByteValue2) -> + val mutableByte1 = MutableByte(initialByteValue) + val mutableByte2 = MutableByte(initialByteValue) + byteValues1.forEach { mutableByte1.value = it } + byteValues2.forEach { mutableByte2.value = it } + mutableByte1.value = finalByteValue1 + mutableByte2.value = finalByteValue2 + @Suppress("ReplaceCallWithBinaryOperator") + mutableByte1.equals(mutableByte2) shouldBe false + } + } + + @Test + fun `MutableByte equals(value)`() = runTest { + checkAll(propTestConfig, Arb.byte()) { byteValue -> + val mutableByte = MutableByte(byteValue) + mutableByte.equals(mutableByte.value) shouldBe false + } + } + + @Test + fun `MutableByte equals(null)`() = runTest { + checkAll(propTestConfig, Arb.byte()) { byteValue -> + val mutableByte = MutableByte(byteValue) + mutableByte.equals(null) shouldBe false + } + } + + @Test + fun `MutableByte equals(different type)`() = runTest { + checkAll( + propTestConfig, + Arb.byte(), + Arb.any(exclude = MutableByte::class, extra = Arb.byte()) + ) { byteValue, other -> + val mutableByte = MutableByte(byteValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableByte.equals(other) shouldBe false + } + } + + @Test + fun `MutableShort value initialization from constructor`() = runTest { + checkAll(propTestConfig, Arb.short()) { shortValue -> + val mutableShort = MutableShort(shortValue) + mutableShort.value shouldBe shortValue + } + } + + @Test + fun `MutableShort toString() initial value`() = runTest { + checkAll(propTestConfig, Arb.short()) { shortValue -> + val mutableShort = MutableShort(shortValue) + mutableShort.toString() shouldBe shortValue.toString() + } + } + + @Test + fun `MutableShort toString() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.short(), Arb.list(Arb.short(), 10..10)) { + initialShortValue, + shortValues -> + val mutableShort = MutableShort(initialShortValue) + shortValues.forEach { shortValue -> + mutableShort.value = shortValue + mutableShort.toString() shouldBe shortValue.toString() + } + } + } + + @Test + fun `MutableShort hashCode() initial value`() = runTest { + checkAll(propTestConfig, Arb.short()) { shortValue -> + val mutableShort = MutableShort(shortValue) + mutableShort.hashCode() shouldBe shortValue.hashCode() + } + } + + @Test + fun `MutableShort hashCode() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.short(), Arb.list(Arb.short(), 10..10)) { + initialShortValue, + shortValues -> + val mutableShort = MutableShort(initialShortValue) + shortValues.forEach { shortValue -> + mutableShort.value = shortValue + mutableShort.hashCode() shouldBe shortValue.hashCode() + } + } + } + + @Test + fun `MutableShort hashCode() and equals() consistency`() = runTest { + checkAll(propTestConfig, Arb.short()) { shortValue -> + val mutableShort1 = MutableShort(shortValue) + val mutableShort2 = MutableShort(shortValue) + check(mutableShort1 == mutableShort2) + mutableShort1.hashCode() shouldBe mutableShort2.hashCode() + } + } + + @Test + fun `MutableShort equals(self)`() = runTest { + checkAll(propTestConfig, Arb.short()) { shortValue -> + val mutableShort = MutableShort(shortValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableShort.equals(mutableShort) shouldBe true + } + } + + @Test + fun `MutableShort equals(equal, but distinct, instance)`() = runTest { + checkAll(propTestConfig, Arb.short()) { shortValue -> + val mutableShort1 = MutableShort(shortValue) + val mutableShort2 = MutableShort(shortValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableShort1.equals(mutableShort2) shouldBe true + } + } + + @Test + fun `MutableShort equals(equal, but distinct, instance, after mutations)`() = runTest { + checkAll(propTestConfig, Arb.list(Arb.short(), 10..10).pair(), Arb.short()) { + (shortValues1, shortValues2), + finalShortValue -> + val mutableShort1 = MutableShort(shortValues1[0]) + val mutableShort2 = MutableShort(shortValues2[0]) + shortValues1.drop(1).forEach { mutableShort1.value = it } + shortValues2.drop(1).forEach { mutableShort2.value = it } + mutableShort1.value = finalShortValue + mutableShort2.value = finalShortValue + @Suppress("ReplaceCallWithBinaryOperator") + mutableShort1.equals(mutableShort2) shouldBe true + } + } + + @Test + fun `MutableShort equals(unequal instance)`() = runTest { + checkAll(propTestConfig, Arb.short().distinctPair()) { (shortValue1, shortValue2) -> + val mutableShort1 = MutableShort(shortValue1) + val mutableShort2 = MutableShort(shortValue2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableShort1.equals(mutableShort2) shouldBe false + } + } + + @Test + fun `MutableShort equals(unequal instance, after mutations)`() = runTest { + checkAll( + propTestConfig, + Arb.short(), + Arb.list(Arb.short(), 10..10).pair(), + Arb.short().distinctPair(), + ) { initialShortValue, (shortValues1, shortValues2), (finalShortValue1, finalShortValue2) -> + val mutableShort1 = MutableShort(initialShortValue) + val mutableShort2 = MutableShort(initialShortValue) + shortValues1.forEach { mutableShort1.value = it } + shortValues2.forEach { mutableShort2.value = it } + mutableShort1.value = finalShortValue1 + mutableShort2.value = finalShortValue2 + @Suppress("ReplaceCallWithBinaryOperator") + mutableShort1.equals(mutableShort2) shouldBe false + } + } + + @Test + fun `MutableShort equals(value)`() = runTest { + checkAll(propTestConfig, Arb.short()) { shortValue -> + val mutableShort = MutableShort(shortValue) + mutableShort.equals(mutableShort.value) shouldBe false + } + } + + @Test + fun `MutableShort equals(null)`() = runTest { + checkAll(propTestConfig, Arb.short()) { shortValue -> + val mutableShort = MutableShort(shortValue) + mutableShort.equals(null) shouldBe false + } + } + + @Test + fun `MutableShort equals(different type)`() = runTest { + checkAll( + propTestConfig, + Arb.short(), + Arb.any(exclude = MutableShort::class, extra = Arb.short()) + ) { shortValue, other -> + val mutableShort = MutableShort(shortValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableShort.equals(other) shouldBe false + } + } + @Test + fun `MutableInt value initialization from constructor`() = runTest { + checkAll(propTestConfig, Arb.int()) { intValue -> + val mutableInt = MutableInt(intValue) + mutableInt.value shouldBe intValue + } + } + + @Test + fun `MutableInt toString() initial value`() = runTest { + checkAll(propTestConfig, Arb.int()) { intValue -> + val mutableInt = MutableInt(intValue) + mutableInt.toString() shouldBe intValue.toString() + } + } + + @Test + fun `MutableInt toString() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.int(), Arb.list(Arb.int(), 10..10)) { initialIntValue, intValues -> + val mutableInt = MutableInt(initialIntValue) + intValues.forEach { intValue -> + mutableInt.value = intValue + mutableInt.toString() shouldBe intValue.toString() + } + } + } + + @Test + fun `MutableInt hashCode() initial value`() = runTest { + checkAll(propTestConfig, Arb.int()) { intValue -> + val mutableInt = MutableInt(intValue) + mutableInt.hashCode() shouldBe intValue.hashCode() + } + } + + @Test + fun `MutableInt hashCode() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.int(), Arb.list(Arb.int(), 10..10)) { initialIntValue, intValues -> + val mutableInt = MutableInt(initialIntValue) + intValues.forEach { intValue -> + mutableInt.value = intValue + mutableInt.hashCode() shouldBe intValue.hashCode() + } + } + } + + @Test + fun `MutableInt hashCode() and equals() consistency`() = runTest { + checkAll(propTestConfig, Arb.int()) { intValue -> + val mutableInt1 = MutableInt(intValue) + val mutableInt2 = MutableInt(intValue) + check(mutableInt1 == mutableInt2) + mutableInt1.hashCode() shouldBe mutableInt2.hashCode() + } + } + + @Test + fun `MutableInt equals(self)`() = runTest { + checkAll(propTestConfig, Arb.int()) { intValue -> + val mutableInt = MutableInt(intValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableInt.equals(mutableInt) shouldBe true + } + } + + @Test + fun `MutableInt equals(equal, but distinct, instance)`() = runTest { + checkAll(propTestConfig, Arb.int()) { intValue -> + val mutableInt1 = MutableInt(intValue) + val mutableInt2 = MutableInt(intValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableInt1.equals(mutableInt2) shouldBe true + } + } + + @Test + fun `MutableInt equals(equal, but distinct, instance, after mutations)`() = runTest { + checkAll(propTestConfig, Arb.list(Arb.int(), 10..10).pair(), Arb.int()) { + (intValues1, intValues2), + finalIntValue -> + val mutableInt1 = MutableInt(intValues1[0]) + val mutableInt2 = MutableInt(intValues2[0]) + intValues1.drop(1).forEach { mutableInt1.value = it } + intValues2.drop(1).forEach { mutableInt2.value = it } + mutableInt1.value = finalIntValue + mutableInt2.value = finalIntValue + @Suppress("ReplaceCallWithBinaryOperator") + mutableInt1.equals(mutableInt2) shouldBe true + } + } + + @Test + fun `MutableInt equals(unequal instance)`() = runTest { + checkAll(propTestConfig, Arb.int().distinctPair()) { (intValue1, intValue2) -> + val mutableInt1 = MutableInt(intValue1) + val mutableInt2 = MutableInt(intValue2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableInt1.equals(mutableInt2) shouldBe false + } + } + + @Test + fun `MutableInt equals(unequal instance, after mutations)`() = runTest { + checkAll( + propTestConfig, + Arb.int(), + Arb.list(Arb.int(), 10..10).pair(), + Arb.int().distinctPair(), + ) { initialIntValue, (intValues1, intValues2), (finalIntValue1, finalIntValue2) -> + val mutableInt1 = MutableInt(initialIntValue) + val mutableInt2 = MutableInt(initialIntValue) + intValues1.forEach { mutableInt1.value = it } + intValues2.forEach { mutableInt2.value = it } + mutableInt1.value = finalIntValue1 + mutableInt2.value = finalIntValue2 + @Suppress("ReplaceCallWithBinaryOperator") + mutableInt1.equals(mutableInt2) shouldBe false + } + } + + @Test + fun `MutableInt equals(value)`() = runTest { + checkAll(propTestConfig, Arb.int()) { intValue -> + val mutableInt = MutableInt(intValue) + mutableInt.equals(mutableInt.value) shouldBe false + } + } + + @Test + fun `MutableInt equals(null)`() = runTest { + checkAll(propTestConfig, Arb.int()) { intValue -> + val mutableInt = MutableInt(intValue) + mutableInt.equals(null) shouldBe false + } + } + + @Test + fun `MutableInt equals(different type)`() = runTest { + checkAll(propTestConfig, Arb.int(), Arb.any(exclude = MutableInt::class, extra = Arb.int())) { + intValue, + other -> + val mutableInt = MutableInt(intValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableInt.equals(other) shouldBe false + } + } + + @Test + fun `MutableLong value initialization from constructor`() = runTest { + checkAll(propTestConfig, Arb.long()) { longValue -> + val mutableLong = MutableLong(longValue) + mutableLong.value shouldBe longValue + } + } + + @Test + fun `MutableLong toString() initial value`() = runTest { + checkAll(propTestConfig, Arb.long()) { longValue -> + val mutableLong = MutableLong(longValue) + mutableLong.toString() shouldBe longValue.toString() + } + } + + @Test + fun `MutableLong toString() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.long(), Arb.list(Arb.long(), 10..10)) { + initialLongValue, + longValues -> + val mutableLong = MutableLong(initialLongValue) + longValues.forEach { longValue -> + mutableLong.value = longValue + mutableLong.toString() shouldBe longValue.toString() + } + } + } + + @Test + fun `MutableLong hashCode() initial value`() = runTest { + checkAll(propTestConfig, Arb.long()) { longValue -> + val mutableLong = MutableLong(longValue) + mutableLong.hashCode() shouldBe longValue.hashCode() + } + } + + @Test + fun `MutableLong hashCode() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.long(), Arb.list(Arb.long(), 10..10)) { + initialLongValue, + longValues -> + val mutableLong = MutableLong(initialLongValue) + longValues.forEach { longValue -> + mutableLong.value = longValue + mutableLong.hashCode() shouldBe longValue.hashCode() + } + } + } + + @Test + fun `MutableLong hashCode() and equals() consistency`() = runTest { + checkAll(propTestConfig, Arb.long()) { longValue -> + val mutableLong1 = MutableLong(longValue) + val mutableLong2 = MutableLong(longValue) + check(mutableLong1 == mutableLong2) + mutableLong1.hashCode() shouldBe mutableLong2.hashCode() + } + } + + @Test + fun `MutableLong equals(self)`() = runTest { + checkAll(propTestConfig, Arb.long()) { longValue -> + val mutableLong = MutableLong(longValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableLong.equals(mutableLong) shouldBe true + } + } + + @Test + fun `MutableLong equals(equal, but distinct, instance)`() = runTest { + checkAll(propTestConfig, Arb.long()) { longValue -> + val mutableLong1 = MutableLong(longValue) + val mutableLong2 = MutableLong(longValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableLong1.equals(mutableLong2) shouldBe true + } + } + + @Test + fun `MutableLong equals(equal, but distinct, instance, after mutations)`() = runTest { + checkAll(propTestConfig, Arb.list(Arb.long(), 10..10).pair(), Arb.long()) { + (longValues1, longValues2), + finalLongValue -> + val mutableLong1 = MutableLong(longValues1[0]) + val mutableLong2 = MutableLong(longValues2[0]) + longValues1.drop(1).forEach { mutableLong1.value = it } + longValues2.drop(1).forEach { mutableLong2.value = it } + mutableLong1.value = finalLongValue + mutableLong2.value = finalLongValue + @Suppress("ReplaceCallWithBinaryOperator") + mutableLong1.equals(mutableLong2) shouldBe true + } + } + + @Test + fun `MutableLong equals(unequal instance)`() = runTest { + checkAll(propTestConfig, Arb.long().distinctPair()) { (longValue1, longValue2) -> + val mutableLong1 = MutableLong(longValue1) + val mutableLong2 = MutableLong(longValue2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableLong1.equals(mutableLong2) shouldBe false + } + } + + @Test + fun `MutableLong equals(unequal instance, after mutations)`() = runTest { + checkAll( + propTestConfig, + Arb.long(), + Arb.list(Arb.long(), 10..10).pair(), + Arb.long().distinctPair(), + ) { initialLongValue, (longValues1, longValues2), (finalLongValue1, finalLongValue2) -> + val mutableLong1 = MutableLong(initialLongValue) + val mutableLong2 = MutableLong(initialLongValue) + longValues1.forEach { mutableLong1.value = it } + longValues2.forEach { mutableLong2.value = it } + mutableLong1.value = finalLongValue1 + mutableLong2.value = finalLongValue2 + @Suppress("ReplaceCallWithBinaryOperator") + mutableLong1.equals(mutableLong2) shouldBe false + } + } + + @Test + fun `MutableLong equals(value)`() = runTest { + checkAll(propTestConfig, Arb.long()) { longValue -> + val mutableLong = MutableLong(longValue) + mutableLong.equals(mutableLong.value) shouldBe false + } + } + + @Test + fun `MutableLong equals(null)`() = runTest { + checkAll(propTestConfig, Arb.long()) { longValue -> + val mutableLong = MutableLong(longValue) + mutableLong.equals(null) shouldBe false + } + } + + @Test + fun `MutableLong equals(different type)`() = runTest { + checkAll( + propTestConfig, + Arb.long(), + Arb.any(exclude = MutableLong::class, extra = Arb.long()) + ) { longValue, other -> + val mutableLong = MutableLong(longValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableLong.equals(other) shouldBe false + } + } + + @Test + fun `MutableChar value initialization from constructor`() = runTest { + checkAll(propTestConfig, Arb.char()) { charValue -> + val mutableChar = MutableChar(charValue) + mutableChar.value shouldBe charValue + } + } + + @Test + fun `MutableChar toString() initial value`() = runTest { + checkAll(propTestConfig, Arb.char()) { charValue -> + val mutableChar = MutableChar(charValue) + mutableChar.toString() shouldBe charValue.toString() + } + } + + @Test + fun `MutableChar toString() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.char(), Arb.list(Arb.char(), 10..10)) { + initialCharValue, + charValues -> + val mutableChar = MutableChar(initialCharValue) + charValues.forEach { charValue -> + mutableChar.value = charValue + mutableChar.toString() shouldBe charValue.toString() + } + } + } + + @Test + fun `MutableChar hashCode() initial value`() = runTest { + checkAll(propTestConfig, Arb.char()) { charValue -> + val mutableChar = MutableChar(charValue) + mutableChar.hashCode() shouldBe charValue.hashCode() + } + } + + @Test + fun `MutableChar hashCode() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.char(), Arb.list(Arb.char(), 10..10)) { + initialCharValue, + charValues -> + val mutableChar = MutableChar(initialCharValue) + charValues.forEach { charValue -> + mutableChar.value = charValue + mutableChar.hashCode() shouldBe charValue.hashCode() + } + } + } + + @Test + fun `MutableChar hashCode() and equals() consistency`() = runTest { + checkAll(propTestConfig, Arb.char()) { charValue -> + val mutableChar1 = MutableChar(charValue) + val mutableChar2 = MutableChar(charValue) + check(mutableChar1 == mutableChar2) + mutableChar1.hashCode() shouldBe mutableChar2.hashCode() + } + } + + @Test + fun `MutableChar equals(self)`() = runTest { + checkAll(propTestConfig, Arb.char()) { charValue -> + val mutableChar = MutableChar(charValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableChar.equals(mutableChar) shouldBe true + } + } + + @Test + fun `MutableChar equals(equal, but distinct, instance)`() = runTest { + checkAll(propTestConfig, Arb.char()) { charValue -> + val mutableChar1 = MutableChar(charValue) + val mutableChar2 = MutableChar(charValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableChar1.equals(mutableChar2) shouldBe true + } + } + + @Test + fun `MutableChar equals(equal, but distinct, instance, after mutations)`() = runTest { + checkAll(propTestConfig, Arb.list(Arb.char(), 10..10).pair(), Arb.char()) { + (charValues1, charValues2), + finalCharValue -> + val mutableChar1 = MutableChar(charValues1[0]) + val mutableChar2 = MutableChar(charValues2[0]) + charValues1.drop(1).forEach { mutableChar1.value = it } + charValues2.drop(1).forEach { mutableChar2.value = it } + mutableChar1.value = finalCharValue + mutableChar2.value = finalCharValue + @Suppress("ReplaceCallWithBinaryOperator") + mutableChar1.equals(mutableChar2) shouldBe true + } + } + + @Test + fun `MutableChar equals(unequal instance)`() = runTest { + checkAll(propTestConfig, Arb.char().distinctPair()) { (charValue1, charValue2) -> + val mutableChar1 = MutableChar(charValue1) + val mutableChar2 = MutableChar(charValue2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableChar1.equals(mutableChar2) shouldBe false + } + } + + @Test + fun `MutableChar equals(unequal instance, after mutations)`() = runTest { + checkAll( + propTestConfig, + Arb.char(), + Arb.list(Arb.char(), 10..10).pair(), + Arb.char().distinctPair(), + ) { initialCharValue, (charValues1, charValues2), (finalCharValue1, finalCharValue2) -> + val mutableChar1 = MutableChar(initialCharValue) + val mutableChar2 = MutableChar(initialCharValue) + charValues1.forEach { mutableChar1.value = it } + charValues2.forEach { mutableChar2.value = it } + mutableChar1.value = finalCharValue1 + mutableChar2.value = finalCharValue2 + @Suppress("ReplaceCallWithBinaryOperator") + mutableChar1.equals(mutableChar2) shouldBe false + } + } + + @Test + fun `MutableChar equals(value)`() = runTest { + checkAll(propTestConfig, Arb.char()) { charValue -> + val mutableChar = MutableChar(charValue) + mutableChar.equals(mutableChar.value) shouldBe false + } + } + + @Test + fun `MutableChar equals(null)`() = runTest { + checkAll(propTestConfig, Arb.char()) { charValue -> + val mutableChar = MutableChar(charValue) + mutableChar.equals(null) shouldBe false + } + } + + @Test + fun `MutableChar equals(different type)`() = runTest { + checkAll( + propTestConfig, + Arb.char(), + Arb.any(exclude = MutableChar::class, extra = Arb.char()) + ) { charValue, other -> + val mutableChar = MutableChar(charValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableChar.equals(other) shouldBe false + } + } + + @Test + fun `MutableFloat value initialization from constructor`() = runTest { + checkAll(propTestConfig, Arb.float()) { floatValue -> + val mutableFloat = MutableFloat(floatValue) + mutableFloat.value shouldBe floatValue + } + } + + @Test + fun `MutableFloat toString() initial value`() = runTest { + checkAll(propTestConfig, Arb.float()) { floatValue -> + val mutableFloat = MutableFloat(floatValue) + mutableFloat.toString() shouldBe floatValue.toString() + } + } + + @Test + fun `MutableFloat toString() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.float(), Arb.list(Arb.float(), 10..10)) { + initialFloatValue, + floatValues -> + val mutableFloat = MutableFloat(initialFloatValue) + floatValues.forEach { floatValue -> + mutableFloat.value = floatValue + mutableFloat.toString() shouldBe floatValue.toString() + } + } + } + + @Test + fun `MutableFloat hashCode() initial value`() = runTest { + checkAll(propTestConfig, Arb.float()) { floatValue -> + val mutableFloat = MutableFloat(floatValue) + mutableFloat.hashCode() shouldBe floatValue.hashCode() + } + } + + @Test + fun `MutableFloat hashCode() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.float(), Arb.list(Arb.float(), 10..10)) { + initialFloatValue, + floatValues -> + val mutableFloat = MutableFloat(initialFloatValue) + floatValues.forEach { floatValue -> + mutableFloat.value = floatValue + mutableFloat.hashCode() shouldBe floatValue.hashCode() + } + } + } + + @Test + fun `MutableFloat hashCode() and equals() consistency`() = runTest { + checkAll(propTestConfig, Arb.float()) { floatValue -> + val mutableFloat1 = MutableFloat(floatValue) + val mutableFloat2 = MutableFloat(floatValue) + check(mutableFloat1 == mutableFloat2) + mutableFloat1.hashCode() shouldBe mutableFloat2.hashCode() + } + } + + @Test + fun `MutableFloat equals(self)`() = runTest { + checkAll(propTestConfig, Arb.float()) { floatValue -> + val mutableFloat = MutableFloat(floatValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableFloat.equals(mutableFloat) shouldBe true + } + } + + @Test + fun `MutableFloat equals(equal, but distinct, instance)`() = runTest { + checkAll(propTestConfig, Arb.float()) { floatValue -> + val mutableFloat1 = MutableFloat(floatValue) + val mutableFloat2 = MutableFloat(floatValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableFloat1.equals(mutableFloat2) shouldBe true + } + } + + @Test + fun `MutableFloat equals(equal, but distinct, instance, after mutations)`() = runTest { + checkAll(propTestConfig, Arb.list(Arb.float(), 10..10).pair(), Arb.float()) { + (floatValues1, floatValues2), + finalFloatValue -> + val mutableFloat1 = MutableFloat(floatValues1[0]) + val mutableFloat2 = MutableFloat(floatValues2[0]) + floatValues1.drop(1).forEach { mutableFloat1.value = it } + floatValues2.drop(1).forEach { mutableFloat2.value = it } + mutableFloat1.value = finalFloatValue + mutableFloat2.value = finalFloatValue + @Suppress("ReplaceCallWithBinaryOperator") + mutableFloat1.equals(mutableFloat2) shouldBe true + } + } + + @Test + fun `MutableFloat equals(unequal instance)`() = runTest { + checkAll(propTestConfig, Arb.float().distinctPair()) { (floatValue1, floatValue2) -> + val mutableFloat1 = MutableFloat(floatValue1) + val mutableFloat2 = MutableFloat(floatValue2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableFloat1.equals(mutableFloat2) shouldBe false + } + } + + @Test + fun `MutableFloat equals(unequal instance, after mutations)`() = runTest { + checkAll( + propTestConfig, + Arb.float(), + Arb.list(Arb.float(), 10..10).pair(), + Arb.float().distinctPair(), + ) { initialFloatValue, (floatValues1, floatValues2), (finalFloatValue1, finalFloatValue2) -> + val mutableFloat1 = MutableFloat(initialFloatValue) + val mutableFloat2 = MutableFloat(initialFloatValue) + floatValues1.forEach { mutableFloat1.value = it } + floatValues2.forEach { mutableFloat2.value = it } + mutableFloat1.value = finalFloatValue1 + mutableFloat2.value = finalFloatValue2 + @Suppress("ReplaceCallWithBinaryOperator") + mutableFloat1.equals(mutableFloat2) shouldBe false + } + } + + @Test + fun `MutableFloat equals(value)`() = runTest { + checkAll(propTestConfig, Arb.float()) { floatValue -> + val mutableFloat = MutableFloat(floatValue) + mutableFloat.equals(mutableFloat.value) shouldBe false + } + } + + @Test + fun `MutableFloat equals(null)`() = runTest { + checkAll(propTestConfig, Arb.float()) { floatValue -> + val mutableFloat = MutableFloat(floatValue) + mutableFloat.equals(null) shouldBe false + } + } + + @Test + fun `MutableFloat equals(different type)`() = runTest { + checkAll( + propTestConfig, + Arb.float(), + Arb.any(exclude = MutableFloat::class, extra = Arb.float()) + ) { floatValue, other -> + val mutableFloat = MutableFloat(floatValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableFloat.equals(other) shouldBe false + } + } + + @Test + fun `MutableFloat equals(-0 and +0)`() = runTest { + val arb = Arb.of(Pair(-0.0f, 0.0f), Pair(0.0f, -0.0f)) + checkAll(propTestConfig, arb) { (floatValue1, floatValue2) -> + val mutableFloat1 = MutableFloat(floatValue1) + val mutableFloat2 = MutableFloat(floatValue2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableFloat1.equals(mutableFloat2) shouldBe false + } + } + + @Test + fun `MutableFloat hashCode(-0 and +0)`() = runTest { + val arb = Arb.of(Pair(-0.0f, 0.0f), Pair(0.0f, -0.0f)) + checkAll(propTestConfig, arb) { (floatValue1, floatValue2) -> + val mutableFloat1 = MutableFloat(floatValue1) + val mutableFloat2 = MutableFloat(floatValue2) + mutableFloat1.hashCode() shouldNotBe mutableFloat2.hashCode() + } + } + + @Test + fun `MutableFloat equals(NaN)`() = runTest { + val mutableFloat1 = MutableFloat(Float.NaN) + val mutableFloat2 = MutableFloat(Float.NaN) + @Suppress("ReplaceCallWithBinaryOperator") + mutableFloat1.equals(mutableFloat2) shouldBe true + } + + @Test + fun `MutableFloat hashCode(NaN)`() = runTest { + val mutableFloat1 = MutableFloat(Float.NaN) + val mutableFloat2 = MutableFloat(Float.NaN) + mutableFloat1.hashCode() shouldBe mutableFloat2.hashCode() + } + + @Test + fun `MutableDouble value initialization from constructor`() = runTest { + checkAll(propTestConfig, Arb.double()) { doubleValue -> + val mutableDouble = MutableDouble(doubleValue) + mutableDouble.value shouldBe doubleValue + } + } + + @Test + fun `MutableDouble toString() initial value`() = runTest { + checkAll(propTestConfig, Arb.double()) { doubleValue -> + val mutableDouble = MutableDouble(doubleValue) + mutableDouble.toString() shouldBe doubleValue.toString() + } + } + + @Test + fun `MutableDouble toString() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.double(), Arb.list(Arb.double(), 10..10)) { + initialDoubleValue, + doubleValues -> + val mutableDouble = MutableDouble(initialDoubleValue) + doubleValues.forEach { doubleValue -> + mutableDouble.value = doubleValue + mutableDouble.toString() shouldBe doubleValue.toString() + } + } + } + + @Test + fun `MutableDouble hashCode() initial value`() = runTest { + checkAll(propTestConfig, Arb.double()) { doubleValue -> + val mutableDouble = MutableDouble(doubleValue) + mutableDouble.hashCode() shouldBe doubleValue.hashCode() + } + } + + @Test + fun `MutableDouble hashCode() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.double(), Arb.list(Arb.double(), 10..10)) { + initialDoubleValue, + doubleValues -> + val mutableDouble = MutableDouble(initialDoubleValue) + doubleValues.forEach { doubleValue -> + mutableDouble.value = doubleValue + mutableDouble.hashCode() shouldBe doubleValue.hashCode() + } + } + } + + @Test + fun `MutableDouble hashCode() and equals() consistency`() = runTest { + checkAll(propTestConfig, Arb.double()) { doubleValue -> + val mutableDouble1 = MutableDouble(doubleValue) + val mutableDouble2 = MutableDouble(doubleValue) + check(mutableDouble1 == mutableDouble2) + mutableDouble1.hashCode() shouldBe mutableDouble2.hashCode() + } + } + + @Test + fun `MutableDouble equals(self)`() = runTest { + checkAll(propTestConfig, Arb.double()) { doubleValue -> + val mutableDouble = MutableDouble(doubleValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableDouble.equals(mutableDouble) shouldBe true + } + } + + @Test + fun `MutableDouble equals(equal, but distinct, instance)`() = runTest { + checkAll(propTestConfig, Arb.double()) { doubleValue -> + val mutableDouble1 = MutableDouble(doubleValue) + val mutableDouble2 = MutableDouble(doubleValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableDouble1.equals(mutableDouble2) shouldBe true + } + } + + @Test + fun `MutableDouble equals(equal, but distinct, instance, after mutations)`() = runTest { + checkAll(propTestConfig, Arb.list(Arb.double(), 10..10).pair(), Arb.double()) { + (doubleValues1, doubleValues2), + finalDoubleValue -> + val mutableDouble1 = MutableDouble(doubleValues1[0]) + val mutableDouble2 = MutableDouble(doubleValues2[0]) + doubleValues1.drop(1).forEach { mutableDouble1.value = it } + doubleValues2.drop(1).forEach { mutableDouble2.value = it } + mutableDouble1.value = finalDoubleValue + mutableDouble2.value = finalDoubleValue + @Suppress("ReplaceCallWithBinaryOperator") + mutableDouble1.equals(mutableDouble2) shouldBe true + } + } + + @Test + fun `MutableDouble equals(unequal instance)`() = runTest { + checkAll(propTestConfig, Arb.double().distinctPair()) { (doubleValue1, doubleValue2) -> + val mutableDouble1 = MutableDouble(doubleValue1) + val mutableDouble2 = MutableDouble(doubleValue2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableDouble1.equals(mutableDouble2) shouldBe false + } + } + + @Test + fun `MutableDouble equals(unequal instance, after mutations)`() = runTest { + checkAll( + propTestConfig, + Arb.double(), + Arb.list(Arb.double(), 10..10).pair(), + Arb.double().distinctPair(), + ) { initialDoubleValue, (doubleValues1, doubleValues2), (finalDoubleValue1, finalDoubleValue2) + -> + val mutableDouble1 = MutableDouble(initialDoubleValue) + val mutableDouble2 = MutableDouble(initialDoubleValue) + doubleValues1.forEach { mutableDouble1.value = it } + doubleValues2.forEach { mutableDouble2.value = it } + mutableDouble1.value = finalDoubleValue1 + mutableDouble2.value = finalDoubleValue2 + @Suppress("ReplaceCallWithBinaryOperator") + mutableDouble1.equals(mutableDouble2) shouldBe false + } + } + + @Test + fun `MutableDouble equals(value)`() = runTest { + checkAll(propTestConfig, Arb.double()) { doubleValue -> + val mutableDouble = MutableDouble(doubleValue) + mutableDouble.equals(mutableDouble.value) shouldBe false + } + } + + @Test + fun `MutableDouble equals(null)`() = runTest { + checkAll(propTestConfig, Arb.double()) { doubleValue -> + val mutableDouble = MutableDouble(doubleValue) + mutableDouble.equals(null) shouldBe false + } + } + + @Test + fun `MutableDouble equals(different type)`() = runTest { + checkAll( + propTestConfig, + Arb.double(), + Arb.any(exclude = MutableDouble::class, extra = Arb.double()) + ) { doubleValue, other -> + val mutableDouble = MutableDouble(doubleValue) + @Suppress("ReplaceCallWithBinaryOperator") + mutableDouble.equals(other) shouldBe false + } + } + + @Test + fun `MutableDouble equals(-0 and +0)`() = runTest { + val arb = Arb.of(Pair(-0.0, 0.0), Pair(0.0, -0.0)) + checkAll(propTestConfig, arb) { (doubleValue1, doubleValue2) -> + val mutableDouble1 = MutableDouble(doubleValue1) + val mutableDouble2 = MutableDouble(doubleValue2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableDouble1.equals(mutableDouble2) shouldBe false + } + } + + @Test + fun `MutableDouble hashCode(-0 and +0)`() = runTest { + val arb = Arb.of(Pair(-0.0, 0.0), Pair(0.0, -0.0)) + checkAll(propTestConfig, arb) { (doubleValue1, doubleValue2) -> + val mutableDouble1 = MutableDouble(doubleValue1) + val mutableDouble2 = MutableDouble(doubleValue2) + mutableDouble1.hashCode() shouldNotBe mutableDouble2.hashCode() + } + } + + @Test + fun `MutableDouble equals(NaN)`() = runTest { + val mutableDouble1 = MutableDouble(Double.NaN) + val mutableDouble2 = MutableDouble(Double.NaN) + @Suppress("ReplaceCallWithBinaryOperator") + mutableDouble1.equals(mutableDouble2) shouldBe true + } + + @Test + fun `MutableDouble hashCode(NaN)`() = runTest { + val mutableDouble1 = MutableDouble(Double.NaN) + val mutableDouble2 = MutableDouble(Double.NaN) + mutableDouble1.hashCode() shouldBe mutableDouble2.hashCode() + } + + @Test + fun `MutableReference value initialization from constructor`() = runTest { + checkAll(propTestConfig, Arb.any()) { value -> + val mutableReference = MutableReference(value) + mutableReference.value shouldBe value + } + } + + @Test + fun `MutableReference toString() initial value`() = runTest { + checkAll(propTestConfig, Arb.any()) { value -> + val mutableReference = MutableReference(value) + mutableReference.toString() shouldBe value.toString() + } + } + + @Test + fun `MutableReference toString() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.any(), Arb.list(Arb.any(), 10..10)) { initialReferenceValue, values + -> + val mutableReference = MutableReference(initialReferenceValue) + values.forEach { value -> + mutableReference.value = value + mutableReference.toString() shouldBe value.toString() + } + } + } + + @Test + fun `MutableReference hashCode() initial value`() = runTest { + checkAll(propTestConfig, Arb.any()) { value -> + val mutableReference = MutableReference(value) + mutableReference.hashCode() shouldBe value.hashCode() + } + } + + @Test + fun `MutableReference hashCode() subsequent values`() = runTest { + checkAll(propTestConfig, Arb.any(), Arb.list(Arb.any(), 10..10)) { initialReferenceValue, values + -> + val mutableReference = MutableReference(initialReferenceValue) + values.forEach { value -> + mutableReference.value = value + mutableReference.hashCode() shouldBe value.hashCode() + } + } + } + + @Test + fun `MutableReference hashCode() and equals() consistency`() = runTest { + checkAll(propTestConfig, Arb.any()) { value -> + val mutableReference1 = MutableReference(value) + val mutableReference2 = MutableReference(value) + check(mutableReference1 == mutableReference2) + mutableReference1.hashCode() shouldBe mutableReference2.hashCode() + } + } + + @Test + fun `MutableReference equals(self)`() = runTest { + checkAll(propTestConfig, Arb.any()) { value -> + val mutableReference = MutableReference(value) + @Suppress("ReplaceCallWithBinaryOperator") + mutableReference.equals(mutableReference) shouldBe true + } + } + + @Test + fun `MutableReference equals(equal, but distinct, instance)`() = runTest { + checkAll(propTestConfig, Arb.any()) { value -> + val mutableReference1 = MutableReference(value) + val mutableReference2 = MutableReference(value) + @Suppress("ReplaceCallWithBinaryOperator") + mutableReference1.equals(mutableReference2) shouldBe true + } + } + + @Test + fun `MutableReference equals(equal, but distinct, instance, after mutations)`() = runTest { + checkAll(propTestConfig, Arb.list(Arb.any(), 10..10).pair(), Arb.any()) { + (values1, values2), + finalReferenceValue -> + val mutableReference1 = MutableReference(values1[0]) + val mutableReference2 = MutableReference(values2[0]) + values1.drop(1).forEach { mutableReference1.value = it } + values2.drop(1).forEach { mutableReference2.value = it } + mutableReference1.value = finalReferenceValue + mutableReference2.value = finalReferenceValue + @Suppress("ReplaceCallWithBinaryOperator") + mutableReference1.equals(mutableReference2) shouldBe true + } + } + + @Test + fun `MutableReference equals(unequal instance)`() = runTest { + checkAll(propTestConfig, Arb.any().distinctPair()) { (value1, value2) -> + val mutableReference1 = MutableReference(value1) + val mutableReference2 = MutableReference(value2) + @Suppress("ReplaceCallWithBinaryOperator") + mutableReference1.equals(mutableReference2) shouldBe false + } + } + + @Test + fun `MutableReference equals(unequal instance, after mutations)`() = runTest { + checkAll( + propTestConfig, + Arb.any(), + Arb.list(Arb.any(), 10..10).pair(), + Arb.any().distinctPair(), + ) { initialReferenceValue, (values1, values2), (finalReferenceValue1, finalReferenceValue2) -> + val mutableReference1 = MutableReference(initialReferenceValue) + val mutableReference2 = MutableReference(initialReferenceValue) + values1.forEach { mutableReference1.value = it } + values2.forEach { mutableReference2.value = it } + mutableReference1.value = finalReferenceValue1 + mutableReference2.value = finalReferenceValue2 + @Suppress("ReplaceCallWithBinaryOperator") + mutableReference1.equals(mutableReference2) shouldBe false + } + } + + @Test + fun `MutableReference equals(value)`() = runTest { + checkAll(propTestConfig, Arb.any()) { value -> + val mutableReference = MutableReference(value) + mutableReference.equals(mutableReference.value) shouldBe false + } + } + + @Test + fun `MutableReference equals(null)`() = runTest { + checkAll(propTestConfig, Arb.any()) { value -> + val mutableReference = MutableReference(value) + mutableReference.equals(null) shouldBe false + } + } + + @Test + fun `MutableReference equals(different type)`() = runTest { + checkAll(propTestConfig, Arb.any(), Arb.any(exclude = MutableReference::class)) { value, other + -> + val mutableReference = MutableReference(value) + @Suppress("ReplaceCallWithBinaryOperator") + mutableReference.equals(other) shouldBe false + } + } +} + +@OptIn(ExperimentalKotest::class) +private val propTestConfig = + PropTestConfig( + iterations = 200, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2), + shrinkingMode = ShrinkingMode.Off, + ) From 5d277ebe0ffbae5fc74939028bc2548244784ce2 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 14 Jul 2026 16:40:22 -0400 Subject: [PATCH 2/7] ConflatedSignal.kt: added `signalIfNotNull(signal: T?)` extension function --- .../util/coroutines/ConflatedSignal.kt | 7 +++++++ .../coroutines/ConflatedSignalUnitTest.kt | 19 +++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/coroutines/ConflatedSignal.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/coroutines/ConflatedSignal.kt index bef4aa1900b..812a662d570 100644 --- a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/coroutines/ConflatedSignal.kt +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/util/coroutines/ConflatedSignal.kt @@ -179,3 +179,10 @@ internal class ConflatedSignal { internal fun ConflatedSignal.signal() { signal(Unit) } + +/** Convenience extension function to signal a [ConflatedSignal] if the given signal is not null. */ +internal fun ConflatedSignal.signalIfNotNull(signal: T?) { + if (signal != null) { + signal(signal) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/util/coroutines/ConflatedSignalUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/util/coroutines/ConflatedSignalUnitTest.kt index 24f6fec70e5..5bb6f2bef43 100644 --- a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/util/coroutines/ConflatedSignalUnitTest.kt +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/util/coroutines/ConflatedSignalUnitTest.kt @@ -356,4 +356,23 @@ class ConflatedSignalUnitTest { backgroundScope.launch(start = CoroutineStart.UNDISPATCHED) { signal.signals.collect {} } job.isCompleted shouldBe false } + + @Test + fun `signalIfNotNull with non-null value triggers signal`() = runTest { + val signal = ConflatedSignal() + signal.signalIfNotNull("hello") + + signal.hasPendingSignal shouldBe true + signal.await() shouldBe "hello" + } + + @Test + fun `signalIfNotNull with null value does not trigger signal`() = runTest { + val signal = ConflatedSignal() + signal.signalIfNotNull(null) + + signal.hasPendingSignal shouldBe false + val job = backgroundScope.launch(start = CoroutineStart.UNDISPATCHED) { signal.await() } + job.isCompleted shouldBe false + } } From c998f21881274fce24878cfbe64828899e36c603 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Fri, 10 Jul 2026 16:12:21 -0400 Subject: [PATCH 3/7] NetworkConnectivityRestoredFlow.kt added --- .../core/NetworkConnectivityRestoredFlow.kt | 174 +++++ ...NetworkConnectivityRestoredFlowUnitTest.kt | 643 ++++++++++++++++++ 2 files changed, 817 insertions(+) create mode 100644 firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/NetworkConnectivityRestoredFlow.kt create mode 100644 firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/NetworkConnectivityRestoredFlowUnitTest.kt diff --git a/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/NetworkConnectivityRestoredFlow.kt b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/NetworkConnectivityRestoredFlow.kt new file mode 100644 index 00000000000..966a888326d --- /dev/null +++ b/firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/NetworkConnectivityRestoredFlow.kt @@ -0,0 +1,174 @@ +/* + * 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.core + +import android.content.Context +import android.content.Context.CONNECTIVITY_SERVICE +import android.net.ConnectivityManager +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkRequest +import android.os.Build +import androidx.annotation.RequiresApi +import com.google.firebase.dataconnect.util.coroutines.ConflatedSignal +import com.google.firebase.dataconnect.util.coroutines.signalIfNotNull +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.flow + +internal data object NetworkConnectivityRestored + +/** + * Creates a cold [Flow] of [NetworkConnectivityRestored] events that monitors network connectivity + * changes that potentially result in a restoration of network connectivity. + * + * Emissions from this flow signify that the network connectivity status has changed in a way that + * suggests a network connection might now be successfully established. + * + * A downstream connection retry backoff mechanism can collect this flow to interrupt or abort a + * retry suspension (backoff delay). Instead of waiting for the full backoff timeout to expire, the + * mechanism can immediately re-attempt the connection upon receiving a + * [NetworkConnectivityRestored] event. + * + * If the [Flow] is collected while a network appears to be available then an event will be emitted + * immediately. Namely, it will not wait for some other network to become available to emit the + * first event if a network is _already_ available. + * + * @param context The [Context] used to retrieve the [ConnectivityManager] system service. + * @return A cold [Flow] emitting [NetworkConnectivityRestored] on network state transitions that + * suggest that network connectivity is now (or continues to be) available. + */ +internal fun networkConnectivityRestoredFlow(context: Context): Flow { + val connectivityManager = context.getSystemService(CONNECTIVITY_SERVICE) + check(connectivityManager is ConnectivityManager) { + "internal error rfq632nm3m: getSystemService(CONNECTIVITY_SERVICE) should have returned " + + "an instance of ${ConnectivityManager::class.qualifiedName}, but got $connectivityManager" + } + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + networkConnectivityRestoredFlowAPI24(connectivityManager) + } else { + networkConnectivityRestoredFlowAPI23(connectivityManager) + } +} + +private fun networkConnectivityRestoredFlowAPI23( + connectivityManager: ConnectivityManager +): Flow = + networkConnectivityRestoredFlow(connectivityManager) { + val request = NetworkRequest.Builder().addCapability(NET_CAPABILITY_INTERNET).build() + connectivityManager.registerNetworkCallback(request, it) + } + +@RequiresApi(Build.VERSION_CODES.N) +private fun networkConnectivityRestoredFlowAPI24( + connectivityManager: ConnectivityManager +): Flow = + networkConnectivityRestoredFlow(connectivityManager) { + connectivityManager.registerDefaultNetworkCallback(it) + } + +private fun networkConnectivityRestoredFlow( + connectivityManager: ConnectivityManager, + registerCallback: (ConnectivityManager.NetworkCallback) -> Unit +): Flow = flow { + val callback = NetworkCallbackImpl() + registerCallback(callback) + + try { + emitAll(callback.signal.signals) + } finally { + connectivityManager.unregisterNetworkCallback(callback) + } +} + +private class NetworkCallbackImpl : ConnectivityManager.NetworkCallback() { + + val signal = ConflatedSignal() + + private val lock = ReentrantLock() + private val availableNetworks = mutableSetOf() + private val validatedNetworks = mutableSetOf() + + override fun onAvailable(network: Network) { + val signalOrNull = + lock.withLock { + val networkAdded = availableNetworks.add(network) + if (networkAdded) NetworkConnectivityRestored else null + } + + signal.signalIfNotNull(signalOrNull) + } + + override fun onLost(network: Network) { + val signalOrNull = + lock.withLock { + availableNetworks.remove(network) + val wasValidated = validatedNetworks.remove(network) + // Only signal if we have other validated networks that can take over (API 23 failover) + if (wasValidated && validatedNetworks.isNotEmpty()) NetworkConnectivityRestored else null + } + + signal.signalIfNotNull(signalOrNull) + } + + /** + * Notifies the callback when the network block status changes (e.g. background data saver rules + * are toggled). We signal when the network becomes unblocked (`blocked == false`) so consumers + * can attempt to reconnect. + */ + @RequiresApi(Build.VERSION_CODES.Q) + override fun onBlockedStatusChanged(network: Network, blocked: Boolean) { + lock.withLock { availableNetworks.add(network) } + if (!blocked) { + signal.signal(NetworkConnectivityRestored) + } + } + + /** + * Handles capability changes for a network. + * + * On Android 9 (API 28) and above, this callback is invoked frequently for minor capability + * updates such as signal strength (RSSI) fluctuations or bandwidth estimate changes, even if the + * connection remains active and stable. + * + * To prevent excessive signals and unnecessary reconnection attempts by downstream consumers, + * this method filters out those redundant updates and only signals when the network's validation + * status (presence of [NET_CAPABILITY_VALIDATED]) actually transitions (either gained or lost). + */ + override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { + val isValidated = networkCapabilities.hasCapability(NET_CAPABILITY_VALIDATED) + + val signalOrNull = + lock.withLock { + availableNetworks.add(network) + if (isValidated) { + val networkAdded = validatedNetworks.add(network) + if (networkAdded) NetworkConnectivityRestored else null + } else { + val networkRemoved = validatedNetworks.remove(network) + if (networkRemoved && validatedNetworks.isNotEmpty()) NetworkConnectivityRestored + else null + } + } + + signal.signalIfNotNull(signalOrNull) + } +} diff --git a/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/NetworkConnectivityRestoredFlowUnitTest.kt b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/NetworkConnectivityRestoredFlowUnitTest.kt new file mode 100644 index 00000000000..5898e88128b --- /dev/null +++ b/firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/NetworkConnectivityRestoredFlowUnitTest.kt @@ -0,0 +1,643 @@ +/* + * 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.core + +import android.content.Context +import android.content.Context.CONNECTIVITY_SERVICE +import android.net.ConnectivityManager +import android.net.ConnectivityManager.NetworkCallback +import android.net.Network +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET +import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED +import android.net.NetworkRequest +import android.os.Build +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.turbine.turbineScope +import com.google.firebase.dataconnect.testutil.MutableReference +import com.google.firebase.dataconnect.testutil.RandomSeedTestRule +import com.google.firebase.dataconnect.testutil.SuspendingCountDownLatch +import com.google.firebase.dataconnect.testutil.shouldContainWithNonAbuttingText +import com.google.firebase.dataconnect.util.coroutines.ConflatedSignal +import com.google.firebase.dataconnect.util.coroutines.signal +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.common.ExperimentalKotest +import io.kotest.matchers.collections.shouldBeUnique +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +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.ShrinkingMode +import io.kotest.property.arbitrary.arbitrary +import io.kotest.property.arbitrary.boolean +import io.kotest.property.arbitrary.enum +import io.kotest.property.arbitrary.int +import io.kotest.property.arbitrary.of +import io.kotest.property.checkAll +import io.mockk.CapturingSlot +import io.mockk.MockKVerificationScope +import io.mockk.clearAllMocks +import io.mockk.clearMocks +import io.mockk.every +import io.mockk.mockk +import io.mockk.slot +import io.mockk.verify +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@RunWith(AndroidJUnit4::class) +class NetworkConnectivityRestoredFlowUnitTest { + + @get:Rule(order = Int.MIN_VALUE) val randomSeedTestRule = RandomSeedTestRule() + + val rs: RandomSource by randomSeedTestRule.rs + + @After + fun clearAllMocksAfterwards() { + // Workaround OOM errors where mockk keeps references into roboelectric for method arguments. + // These strong references prevent the entire classloader from being GC'd. + clearAllMocks() + } + + @Test + fun `networkConnectivityRestoredFlow() when ConnectivityManager is null`() = runTest { + val context: Context = mockk { every { getSystemService(CONNECTIVITY_SERVICE) } returns null } + + val exception = shouldThrow { networkConnectivityRestoredFlow(context) } + exception.message shouldContainWithNonAbuttingText "rfq632nm3m" + exception.message shouldContainWithNonAbuttingText ConnectivityManager::class.qualifiedName!! + exception.message shouldContainWithNonAbuttingText "null" + } + + @Test + fun `networkConnectivityRestoredFlow() when ConnectivityManager is wrong type`() = runTest { + class NotAConnectivityManager + + val connectivityManagerOfWrongType = NotAConnectivityManager() + val context: Context = mockk { + every { getSystemService(CONNECTIVITY_SERVICE) } returns connectivityManagerOfWrongType + } + + val exception = shouldThrow { networkConnectivityRestoredFlow(context) } + exception.message shouldContainWithNonAbuttingText "rfq632nm3m" + exception.message shouldContainWithNonAbuttingText ConnectivityManager::class.qualifiedName!! + exception.message shouldContainWithNonAbuttingText connectivityManagerOfWrongType.toString() + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `networkConnectivityRestoredFlow() collection registers and unregisters callback API 23`() = + runTest { + val connectivityManager: ConnectivityManager = mockk(relaxed = true) + val context: Context = mockk { + every { getSystemService(CONNECTIVITY_SERVICE) } returns connectivityManager + } + + turbineScope { + val flow = networkConnectivityRestoredFlow(context) + val collector = flow.testIn(backgroundScope) + + val networkRequestSlot = slot() + val callbackSlot = slot() + verify(exactly = 1) { + connectivityManager.registerNetworkCallback( + capture(networkRequestSlot), + capture(callbackSlot) + ) + } + + val networkRequest = networkRequestSlot.captured + val expectedNetworkRequest = + NetworkRequest.Builder().addCapability(NET_CAPABILITY_INTERNET).build() + networkRequest shouldBe expectedNetworkRequest + + collector.cancelAndIgnoreRemainingEvents() + verify(exactly = 1) { connectivityManager.unregisterNetworkCallback(callbackSlot.captured) } + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.N]) + fun `networkConnectivityRestoredFlow() collection registers and unregisters callback API 24`() = + runTest { + val connectivityManager: ConnectivityManager = mockk(relaxed = true) + val context: Context = mockk { + every { getSystemService(CONNECTIVITY_SERVICE) } returns connectivityManager + } + + turbineScope { + val flow = networkConnectivityRestoredFlow(context) + val collector = flow.testIn(backgroundScope) + + val callbackSlot = slot() + verify(exactly = 1) { + connectivityManager.registerDefaultNetworkCallback(capture(callbackSlot)) + } + + collector.cancelAndIgnoreRemainingEvents() + verify(exactly = 1) { connectivityManager.unregisterNetworkCallback(callbackSlot.captured) } + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `networkConnectivityRestoredFlow() collection unregisters callback on exception API 23`() = + `networkConnectivityRestoredFlow() collection unregisters callback on exception` { callback -> + every { registerNetworkCallback(any(), any()) } answers + { + callback(secondArg()) + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.N]) + fun `networkConnectivityRestoredFlow() collection unregisters callback on exception API 24`() = + `networkConnectivityRestoredFlow() collection unregisters callback on exception` { callback -> + every { registerDefaultNetworkCallback(any()) } answers + { + callback(firstArg()) + } + } + + private fun `networkConnectivityRestoredFlow() collection unregisters callback on exception`( + registerNetworkCallback: ConnectivityManager.((NetworkCallback) -> Unit) -> Unit, + ) = runTest { + val callbackRegisteredSignal = ConflatedSignal() + val callbackUnregisteredSignal = ConflatedSignal() + val connectivityManager: ConnectivityManager = + mockk(relaxed = true) { + registerNetworkCallback { networkCallback: NetworkCallback -> + callbackRegisteredSignal.signal(networkCallback) + } + every { unregisterNetworkCallback(any()) } answers + { + callbackUnregisteredSignal.signal(firstArg()) + } + } + + val context: Context = mockk { + every { getSystemService(CONNECTIVITY_SERVICE) } returns connectivityManager + } + + class TestException : Exception() + val flow = networkConnectivityRestoredFlow(context) + backgroundScope.launch { runCatching { flow.collect { throw TestException() } } } + + val registeredCallback = callbackRegisteredSignal.await() + registeredCallback.onAvailable(mockk()) // cause a value to be emitted by the flow + callbackUnregisteredSignal.await() shouldBe registeredCallback + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `networkConnectivityRestoredFlow() sequential collection registers and unregisters callback API 23`() = + `networkConnectivityRestoredFlow() sequential collection registers and unregisters callback` { + networkRequest, + networkCallback -> + registerNetworkCallback(networkRequest(), networkCallback) + } + + @Test + @Config(sdk = [Build.VERSION_CODES.N]) + fun `networkConnectivityRestoredFlow() sequential collection registers and unregisters callback API 24`() = + `networkConnectivityRestoredFlow() sequential collection registers and unregisters callback` { + _, + networkCallback -> + registerDefaultNetworkCallback(networkCallback) + } + + private fun `networkConnectivityRestoredFlow() sequential collection registers and unregisters callback`( + registerNetworkCallback: ConnectivityManager.(() -> NetworkRequest, NetworkCallback) -> Unit, + ) = runTest { + val connectivityManager: ConnectivityManager = mockk(relaxed = true) + val context: Context = mockk { + every { getSystemService(CONNECTIVITY_SERVICE) } returns connectivityManager + } + + val flow = networkConnectivityRestoredFlow(context) + val callbacks = mutableListOf() + + repeat(5) { + turbineScope { + clearMocks(connectivityManager) + + val collector = flow.testIn(backgroundScope) + val callbackSlot = slot() + verify(exactly = 1) { + registerNetworkCallback(connectivityManager, ::any, capture(callbackSlot)) + } + val callback = callbackSlot.captured + callbacks.add(callback) + + collector.cancelAndIgnoreRemainingEvents() + verify(exactly = 1) { connectivityManager.unregisterNetworkCallback(callbackSlot.captured) } + } + } + + callbacks.shouldBeUnique() + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `networkConnectivityRestoredFlow() parallel collection registers and unregisters callback API 23`() = + `networkConnectivityRestoredFlow() parallel collection registers and unregisters callback` { + onRegisterCallback -> + every { registerNetworkCallback(any(), any()) } coAnswers + { + onRegisterCallback(secondArg()) + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.N]) + fun `networkConnectivityRestoredFlow() parallel collection registers and unregisters callback API 24`() = + `networkConnectivityRestoredFlow() parallel collection registers and unregisters callback` { + onRegisterCallback -> + every { registerDefaultNetworkCallback(any()) } coAnswers + { + onRegisterCallback(firstArg()) + } + } + + private fun `networkConnectivityRestoredFlow() parallel collection registers and unregisters callback`( + registerNetworkCallback: ConnectivityManager.(suspend (NetworkCallback) -> Unit) -> Unit, + ) = runTest { + val latch = SuspendingCountDownLatch(5) + val callbackRegisteredSignal = ConflatedSignal() + val callbacksMutex = Mutex() + val registeredCallbacks = mutableListOf() + val unregisteredCallbacks = mutableListOf() + + val connectivityManager: ConnectivityManager = + mockk(relaxed = true) { + registerNetworkCallback { networkCallback: NetworkCallback -> + callbacksMutex.withLock { registeredCallbacks.add(networkCallback) } + callbackRegisteredSignal.signal() + } + every { unregisterNetworkCallback(any()) } coAnswers + { + callbacksMutex.withLock { unregisteredCallbacks.add(firstArg()) } + } + } + + val context: Context = mockk { + every { getSystemService(CONNECTIVITY_SERVICE) } returns connectivityManager + } + val flow = networkConnectivityRestoredFlow(context) + + val jobs = + List(latch.count) { + backgroundScope.launch(Dispatchers.Default) { + latch.countDown().await() + flow.collect() + } + } + + callbackRegisteredSignal.signals.first { + callbacksMutex.withLock { registeredCallbacks.size == jobs.size } + } + + jobs.forEach { it.cancel() } + jobs.joinAll() + + callbacksMutex.withLock { + check(registeredCallbacks.size == jobs.size) + registeredCallbacks.shouldBeUnique() + unregisteredCallbacks shouldContainExactlyInAnyOrder registeredCallbacks + } + } + + @Test + @Config(sdk = [Build.VERSION_CODES.M]) + fun `networkConnectivityRestoredFlow() emits expected events API 23`() = + testNetworkCallbackSequences(includeBlockedStatusChanged = false, api23CaptureCallback) + + @Test + @Config(sdk = [Build.VERSION_CODES.N]) + fun `networkConnectivityRestoredFlow() emits expected events API 24`() = + testNetworkCallbackSequences(includeBlockedStatusChanged = false, api24CaptureCallback) + + @Test + @Config(sdk = [Build.VERSION_CODES.Q]) + fun `networkConnectivityRestoredFlow() emits expected events API 29`() = + testNetworkCallbackSequences(includeBlockedStatusChanged = true, api29CaptureCallback) + + private fun testNetworkCallbackSequences( + // Specify includeBlockedStatusChanged=true if, and only if, the API level of the test + // environment is 29 (Build.VERSION_CODES.Q) or later. This is because the + // onBlockedStatusChanged() method did not exist in API versions prior to 29 and attempting to + // call in when it does not exist results in a NoSuchMethodError being thrown. + includeBlockedStatusChanged: Boolean, + captureCallback: + MockKVerificationScope.(ConnectivityManager, CapturingSlot) -> Unit + ) = runTest { + val arb = networkCallbackSequenceArb(includeBlockedStatusChanged) + checkAll(propTestConfig, arb) { networkCallbackSequence -> + // Work around potential OOM due to memory leaks left behind by previous iteration; + // see clearAllMocksAfterwards() above for details. + clearAllMocks() + + val connectivityManager: ConnectivityManager = mockk(relaxed = true) + val context: Context = mockk { + every { getSystemService(CONNECTIVITY_SERVICE) } returns connectivityManager + } + + turbineScope { + val flow = networkConnectivityRestoredFlow(context) + val collector = flow.testIn(backgroundScope) + val callback = run { + val callbackSlot = slot() + verify(exactly = 1) { captureCallback(connectivityManager, callbackSlot) } + callbackSlot.captured + } + + networkCallbackSequence.events.forEach { (event, shouldEmit) -> + event.callOn(callback) + + if (shouldEmit) { + collector.awaitItem() shouldBe NetworkConnectivityRestored + } else { + testScheduler.advanceUntilIdle() + collector.expectNoEvents() + } + } + + collector.expectNoEvents() + collector.cancelAndIgnoreRemainingEvents() + } + } + } +} + +@OptIn(ExperimentalKotest::class) +private val propTestConfig = + PropTestConfig( + iterations = 100, + edgeConfig = EdgeConfig(edgecasesGenerationProbability = 0.2), + shrinkingMode = ShrinkingMode.Off, + ) + +private data class NetworkCallbackSequence(val events: List) { + + data class EventShouldEmitPair(val event: Event, val shouldEmit: Boolean) { + override fun toString() = "{$event, shouldEmit=$shouldEmit}" + } + + enum class EventType { + Available, + Lost, + BlockedStatusChanged, + CapabilitiesChanged, + } + + sealed class Event(@Suppress("unused") val type: EventType, val network: Network) { + + abstract fun callOn(callback: NetworkCallback) + + class Available(network: Network) : Event(EventType.Available, network) { + override fun toString() = "Available(network=$network)" + + override fun callOn(callback: NetworkCallback) { + callback.onAvailable(network) + } + } + + class Lost(network: Network) : Event(EventType.Lost, network) { + override fun toString() = "Lost(network=$network)" + + override fun callOn(callback: NetworkCallback) { + callback.onLost(network) + } + } + + class BlockedStatusChanged(network: Network, val blocked: Boolean) : + Event(EventType.BlockedStatusChanged, network) { + override fun toString() = "BlockedStatusChanged(network=$network, blocked=$blocked)" + + override fun callOn(callback: NetworkCallback) { + callback.onBlockedStatusChanged(network, blocked) + } + } + + class CapabilitiesChanged(network: Network, val capabilities: Set) : + Event(EventType.CapabilitiesChanged, network) { + override fun toString(): String { + val capabilitiesStr = capabilities.sortedBy { it.name }.joinToString("|") + return "CapabilitiesChanged(network=$network, capabilities=$capabilitiesStr)" + } + + override fun callOn(callback: NetworkCallback) { + val capabilities: NetworkCapabilities = mockk { + every { hasCapability(any()) } answers + { + val givenCapability = firstArg() + this@CapabilitiesChanged.capabilities.any { it.intValue == givenCapability } + } + } + callback.onCapabilitiesChanged(network, capabilities) + } + } + + @Suppress("unused") + enum class NetworkCapability(val intValue: Int) { + MMS(NetworkCapabilities.NET_CAPABILITY_MMS), + SUPL(NetworkCapabilities.NET_CAPABILITY_SUPL), + DUN(NetworkCapabilities.NET_CAPABILITY_DUN), + FOTA(NetworkCapabilities.NET_CAPABILITY_FOTA), + IMS(NetworkCapabilities.NET_CAPABILITY_IMS), + CBS(NetworkCapabilities.NET_CAPABILITY_CBS), + WIFI_P2P(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P), + IA(NetworkCapabilities.NET_CAPABILITY_IA), + RCS(NetworkCapabilities.NET_CAPABILITY_RCS), + XCAP(NetworkCapabilities.NET_CAPABILITY_XCAP), + EIMS(NetworkCapabilities.NET_CAPABILITY_EIMS), + NOT_METERED(NetworkCapabilities.NET_CAPABILITY_NOT_METERED), + INTERNET(NET_CAPABILITY_INTERNET), + NOT_RESTRICTED(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED), + TRUSTED(NetworkCapabilities.NET_CAPABILITY_TRUSTED), + NOT_VPN(NetworkCapabilities.NET_CAPABILITY_NOT_VPN), + VALIDATED(NET_CAPABILITY_VALIDATED), + CAPTIVE_PORTAL(NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL), + NOT_ROAMING(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING), + FOREGROUND(NetworkCapabilities.NET_CAPABILITY_FOREGROUND), + NOT_CONGESTED(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED), + NOT_SUSPENDED(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED), + MCX(NetworkCapabilities.NET_CAPABILITY_MCX), + TEMPORARILY_NOT_METERED(NetworkCapabilities.NET_CAPABILITY_TEMPORARILY_NOT_METERED), + ENTERPRISE(NetworkCapabilities.NET_CAPABILITY_ENTERPRISE), + HEAD_UNIT(NetworkCapabilities.NET_CAPABILITY_HEAD_UNIT), + MMTEL(NetworkCapabilities.NET_CAPABILITY_MMTEL), + PRIORITIZE_LATENCY(NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_LATENCY), + PRIORITIZE_BANDWIDTH(NetworkCapabilities.NET_CAPABILITY_PRIORITIZE_BANDWIDTH), + } + } +} + +private enum class NetworkState { + Unavailable, + Available, + Validated, +} + +private fun networkCallbackSequenceArb( + includeBlockedStatusChanged: Boolean +): Arb { + val networkCountArb = Arb.int(1..5) + val eventCountArb = Arb.int(1..50) + val eventTypeArb = + if (includeBlockedStatusChanged) { + Arb.enum() + } else { + Arb.of( + NetworkCallbackSequence.EventType.entries.filterNot { + it == NetworkCallbackSequence.EventType.BlockedStatusChanged + } + ) + } + val blockedArb = Arb.boolean() + val capabilityArb = Arb.enum() + val capabilityCountArb = Arb.int(1..5) + val capabilitiesIncludesValidatedArb = Arb.boolean() + + return arbitrary { + val networkCount = networkCountArb.bind() + val networks: List = List(networkCount) { mockk(name = "Network$it") } + val networkIndexArb = Arb.of(networks.indices.toList()) + val networkStateByIndex = + networks.indices.associateWith { MutableReference(NetworkState.Unavailable) } + val eventCount = eventCountArb.bind() + + val events = + List(eventCount) { + val eventType = eventTypeArb.bind() + val networkIndex = networkIndexArb.bind() + val network = networks[networkIndex] + val networkState = networkStateByIndex[networkIndex]!! + + when (eventType) { + NetworkCallbackSequence.EventType.Available -> { + val shouldEmit = + when (networkState.value) { + NetworkState.Available -> false + NetworkState.Validated -> false + NetworkState.Unavailable -> { + networkState.value = NetworkState.Available + true + } + } + NetworkCallbackSequence.EventShouldEmitPair( + NetworkCallbackSequence.Event.Available(network), + shouldEmit + ) + } + NetworkCallbackSequence.EventType.Lost -> { + val shouldEmit = + when (networkState.value) { + NetworkState.Unavailable -> false + NetworkState.Available -> { + networkState.value = NetworkState.Unavailable + false + } + NetworkState.Validated -> { + networkState.value = NetworkState.Unavailable + networkStateByIndex.values.count { it.value == NetworkState.Validated } > 0 + } + } + NetworkCallbackSequence.EventShouldEmitPair( + NetworkCallbackSequence.Event.Lost(network), + shouldEmit + ) + } + NetworkCallbackSequence.EventType.BlockedStatusChanged -> { + if (networkState.value == NetworkState.Unavailable) { + networkState.value = NetworkState.Available + } + val blocked = blockedArb.bind() + val shouldEmit = !blocked + NetworkCallbackSequence.EventShouldEmitPair( + NetworkCallbackSequence.Event.BlockedStatusChanged(network, blocked), + shouldEmit + ) + } + NetworkCallbackSequence.EventType.CapabilitiesChanged -> { + if (networkState.value == NetworkState.Unavailable) { + networkState.value = NetworkState.Available + } + + val capabilityCount = capabilityCountArb.bind() + val capabilities = buildSet { + if (capabilitiesIncludesValidatedArb.bind()) { + add(NetworkCallbackSequence.Event.NetworkCapability.VALIDATED) + } + while (size < capabilityCount) { + add(capabilityArb.bind()) + } + } + + val shouldEmit = + if (NetworkCallbackSequence.Event.NetworkCapability.VALIDATED in capabilities) { + if (networkState.value == NetworkState.Validated) { + false + } else { + networkState.value = NetworkState.Validated + true + } + } else if (networkState.value != NetworkState.Validated) { + false + } else { + networkState.value = NetworkState.Available + networkStateByIndex.values.count { it.value == NetworkState.Validated } > 0 + } + NetworkCallbackSequence.EventShouldEmitPair( + NetworkCallbackSequence.Event.CapabilitiesChanged(network, capabilities), + shouldEmit + ) + } + } + } + + NetworkCallbackSequence(events) + } +} + +private val api23CaptureCallback: + MockKVerificationScope.(ConnectivityManager, CapturingSlot) -> Unit = + { connectivityManager, slot -> + connectivityManager.registerNetworkCallback(any(), capture(slot)) + } + +private val api24CaptureCallback: + MockKVerificationScope.(ConnectivityManager, CapturingSlot) -> Unit = + { connectivityManager, slot -> + connectivityManager.registerDefaultNetworkCallback(capture(slot)) + } + +// API 29 uses the same API as 24; however, create a distinct variable for it to avoid confusion +// at the usage sites. +private val api29CaptureCallback = api24CaptureCallback From 75ae902d656b6f197e79fed14fbbd21456688447 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 14 Jul 2026 21:57:46 -0400 Subject: [PATCH 4/7] Empty commit to suppress github actions [skip actions] From 5098b436cae8b654bb0235d19eff098905286e86 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 14 Jul 2026 21:58:56 -0400 Subject: [PATCH 5/7] CHANGELOG.md: add entry [no ci] --- firebase-dataconnect/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index 5a2f634d655..5efecd74ad3 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -2,7 +2,8 @@ - [changed] Realtime query subscriptions now retry connecting using an exponential backoff strategy. - ([#8381](https://github.com/firebase/firebase-android-sdk/pull/8381)) + ([#8381](https://github.com/firebase/firebase-android-sdk/pull/8381), + ([#8421](https://github.com/firebase/firebase-android-sdk/pull/8421)) # 17.3.2 From 828d6bafdf7a23cf11ddb46bfee308b6ef28a5a6 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 14 Jul 2026 22:02:57 -0400 Subject: [PATCH 6/7] any.kt: fix bug where extras were added 21 times https://github.com/firebase/firebase-android-sdk/pull/8421#discussion_r3583971256 The `addAll(extras)` call is inside the `forEach` loop of `allScalarArbFactories`. This causes the `extras` list to be duplicated and added to `scalarArbs` multiple times (once for each scalar factory), which skews the random distribution of `Arb.any` heavily towards the extras. Moving it outside the loop resolves this issue. ```suggestion val scalarArbs = buildList { allScalarArbFactories.forEach { if (it.type !in excludes) { add(it.factory()) } } addAll(extras) } ``` --- .../firebase/dataconnect/testutil/property/arbitrary/any.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/any.kt b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/any.kt index 18f52672d41..bef5bac49b6 100644 --- a/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/any.kt +++ b/firebase-dataconnect/testutil/src/main/kotlin/com/google/firebase/dataconnect/testutil/property/arbitrary/any.kt @@ -100,8 +100,8 @@ private fun arbs( if (it.type !in excludes) { add(it.factory()) } - addAll(extras) } + addAll(extras) } scalarArbs.forEach { yield(it) } From b5041f16fc8c46d300905d8e5b8c963976271070 Mon Sep 17 00:00:00 2001 From: Denver Coneybeare Date: Tue, 14 Jul 2026 22:04:07 -0400 Subject: [PATCH 7/7] CHANGELOG.md: remove superfluous opening parenthesis https://github.com/firebase/firebase-android-sdk/pull/8421#discussion_r3583971275 There is a typo in the parentheses formatting. The second line has an extra opening parenthesis `(` before the pull request link. --- firebase-dataconnect/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firebase-dataconnect/CHANGELOG.md b/firebase-dataconnect/CHANGELOG.md index 5efecd74ad3..28ae1719068 100644 --- a/firebase-dataconnect/CHANGELOG.md +++ b/firebase-dataconnect/CHANGELOG.md @@ -3,7 +3,7 @@ - [changed] Realtime query subscriptions now retry connecting using an exponential backoff strategy. ([#8381](https://github.com/firebase/firebase-android-sdk/pull/8381), - ([#8421](https://github.com/firebase/firebase-android-sdk/pull/8421)) + [#8421](https://github.com/firebase/firebase-android-sdk/pull/8421)) # 17.3.2