From 616d97ca039434461adff1f5cc2b0725500b13c0 Mon Sep 17 00:00:00 2001 From: mxhsnd <2049295946@qq.com> Date: Mon, 24 Aug 2026 02:43:19 +0800 Subject: [PATCH] fix: snapshot infrared fake calls --- .../testing/fakes/FakeDeviceGateways.kt | 16 +++-- .../testing/fakes/FakeDeviceGatewaysTest.kt | 69 +++++++++++++++++++ 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/testing/fakes/src/main/kotlin/com/flagship/abox/manager/testing/fakes/FakeDeviceGateways.kt b/testing/fakes/src/main/kotlin/com/flagship/abox/manager/testing/fakes/FakeDeviceGateways.kt index 89323d3..12584ff 100644 --- a/testing/fakes/src/main/kotlin/com/flagship/abox/manager/testing/fakes/FakeDeviceGateways.kt +++ b/testing/fakes/src/main/kotlin/com/flagship/abox/manager/testing/fakes/FakeDeviceGateways.kt @@ -12,8 +12,8 @@ import com.flagship.abox.manager.domain.SocketPowerState import com.flagship.abox.manager.domain.SocketStatus import com.flagship.abox.manager.domain.TemperatureHumidityGateway import com.flagship.abox.manager.domain.TemperatureHumidityReading -import java.util.Collections import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.CopyOnWriteArrayList class FakeSocketGateway : SocketGateway { private val states = ConcurrentHashMap() @@ -78,10 +78,9 @@ class FakeTemperatureHumidityGateway : TemperatureHumidityGateway { } class FakeInfraredGateway : InfraredGateway { - val sentKeys: MutableList = Collections.synchronizedList(mutableListOf()) - val learnedKeys: MutableList = Collections.synchronizedList(mutableListOf()) - val downloadCalls: MutableList = - Collections.synchronizedList(mutableListOf()) + val sentKeys = CopyOnWriteArrayList() + val learnedKeys = CopyOnWriteArrayList() + val downloadCalls = CopyOnWriteArrayList() private val sendResults = ConcurrentHashMap>() private val learnResults = ConcurrentHashMap>() @@ -134,9 +133,12 @@ class FakeInfraredGateway : InfraredGateway { profileId: ProviderProfileId, codes: List, ): DomainResult> { + val codesSnapshot = codes.toList() downloadError?.let { return DomainResult.Failure(it) } - downloadCalls += InfraredDownloadCall(profileId, codes) - return DomainResult.Success(codes.map { InfraredCodeDownloadResult(it, successful = true) }) + downloadCalls += InfraredDownloadCall(profileId, codesSnapshot) + return DomainResult.Success( + codesSnapshot.map { InfraredCodeDownloadResult(it, successful = true) }, + ) } } diff --git a/testing/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeDeviceGatewaysTest.kt b/testing/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeDeviceGatewaysTest.kt index b162310..432ea50 100644 --- a/testing/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeDeviceGatewaysTest.kt +++ b/testing/fakes/src/test/kotlin/com/flagship/abox/manager/testing/fakes/FakeDeviceGatewaysTest.kt @@ -4,8 +4,14 @@ import com.flagship.abox.manager.domain.DeviceName import com.flagship.abox.manager.domain.DomainError import com.flagship.abox.manager.domain.DomainResult import com.flagship.abox.manager.domain.InfraredCode +import com.flagship.abox.manager.domain.InfraredCodeDownloadResult import com.flagship.abox.manager.domain.ProviderProfileId import com.flagship.abox.manager.domain.SocketPowerState +import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.CountDownLatch +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.joinAll +import kotlinx.coroutines.launch import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -55,6 +61,69 @@ class FakeDeviceGatewaysTest { assertTrue(download is DomainResult.Success) } + @Test + fun infraredDownloadRecordsAndReturnsAnInputSnapshot() = runTest { + val gateway = FakeInfraredGateway() + val code = InfraredCode(DeviceName("ir-1"), "power", "code-value") + val mutableCodes = mutableListOf(code) + + val result = gateway.downloadCodes(profileOne, mutableCodes) + mutableCodes.clear() + + assertEquals( + InfraredDownloadCall(profileOne, listOf(code)), + gateway.downloadCalls.single(), + ) + assertEquals( + DomainResult.Success(listOf(InfraredCodeDownloadResult(code, successful = true))), + result, + ) + } + + @Test + fun infraredCallLogCanBeIteratedSafelyAroundConcurrentWrites() = runTest { + val gateway = FakeInfraredGateway() + val deviceName = DeviceName("ir-1") + gateway.sendKey(profileOne, deviceName, "initial") + val staleIterators = List(10) { gateway.sentKeys.iterator() } + val start = CountDownLatch(1) + val failures = ConcurrentLinkedQueue() + val writerCount = 4 + val callsPerWriter = 100 + + val writers = List(writerCount) { writerIndex -> + launch(Dispatchers.Default) { + start.await() + repeat(callsPerWriter) { callIndex -> + gateway.sendKey(profileOne, deviceName, "$writerIndex-$callIndex") + } + } + } + val reader = launch(Dispatchers.Default) { + start.await() + repeat(100) { + try { + gateway.sentKeys.forEach { call -> call.key.length } + } catch (failure: Throwable) { + failures += failure + } + } + } + + start.countDown() + (writers + reader).joinAll() + staleIterators.forEach { iterator -> + try { + iterator.forEachRemaining { call -> call.key.length } + } catch (failure: Throwable) { + failures += failure + } + } + + assertTrue(failures.toList().joinToString(), failures.isEmpty()) + assertEquals(1 + writerCount * callsPerWriter, gateway.sentKeys.size) + } + @Test fun infraredLearnResultsCanDifferByProfileDeviceAndKey() = runTest { val gateway = FakeInfraredGateway()