|
|
|
@ -3,7 +3,11 @@ package com.dct.app.tests |
|
|
|
import com.dct.app.core.common.DctResult |
|
|
|
import com.dct.app.data.communication.WifiSocketManager |
|
|
|
import com.dct.app.data.protocol.DctProtocolRequests |
|
|
|
import com.dct.app.data.parser.A2lParser |
|
|
|
import com.dct.app.data.repository.PacketRepositoryImpl |
|
|
|
import com.dct.app.domain.model.a2l.A2lDataType |
|
|
|
import com.dct.app.domain.model.mdb.MdbAttrType |
|
|
|
import com.dct.app.domain.model.mdb.SignalDefinition |
|
|
|
import com.dct.app.domain.model.protocol.* |
|
|
|
import com.dct.app.domain.model.project.createDefaultSensorModules |
|
|
|
import com.dct.app.domain.repository.DeviceConnectionState |
|
|
|
@ -14,8 +18,90 @@ import kotlinx.coroutines.flow.MutableStateFlow |
|
|
|
import kotlinx.coroutines.test.* |
|
|
|
import org.junit.jupiter.api.Assertions.* |
|
|
|
import org.junit.jupiter.api.Test |
|
|
|
import org.junit.jupiter.params.ParameterizedTest |
|
|
|
import org.junit.jupiter.params.provider.CsvSource |
|
|
|
import org.junit.jupiter.params.provider.ValueSource |
|
|
|
|
|
|
|
class PacketRepositoryResponseTest { |
|
|
|
@ParameterizedTest |
|
|
|
@CsvSource("true,false", "false,true") |
|
|
|
fun xcpCalibrationFollowsConfiguredEndianAcrossProjectChanges(firstMsb: Boolean, secondMsb: Boolean) = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
for (msb in listOf(firstMsb, secondMsb)) { |
|
|
|
assertTrue(f.repository.configureMeasurement(xcpConfiguration(msb)) is DctResult.Success) |
|
|
|
assertXcpCalibrationPackets(f, msb) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ParameterizedTest |
|
|
|
@ValueSource(strings = ["rejected", "timeout", "malformed"]) |
|
|
|
fun xcpFailedIfDataDoesNotReplaceAppliedEndian(failure: String) = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
if (failure == "timeout") f.sessionId = 7L |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
assertTrue(f.repository.sendXcpIfDatas(A2lParser.XcpIfData(isMsbFirst = true)) is DctResult.Success) |
|
|
|
when (failure) { |
|
|
|
"rejected" -> f.reject = 0x41 |
|
|
|
"timeout" -> f.silent = 0x41 |
|
|
|
"malformed" -> coEvery { f.wifi.sendCommand(match { it.command == 0x41.toByte() }) } coAnswers { |
|
|
|
f.packets.emit(DctPacket(0xC1.toByte(), byteArrayOf(1, 99))) |
|
|
|
true |
|
|
|
} |
|
|
|
} |
|
|
|
assertTrue(f.repository.configureMeasurement(xcpConfiguration(false)) is DctResult.Failure) |
|
|
|
if (failure == "timeout") { |
|
|
|
assertFalse(f.connected.value) |
|
|
|
f.sessionId = 8L |
|
|
|
f.connected.value = true |
|
|
|
f.silent = null |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
} |
|
|
|
assertXcpCalibrationPackets(f, true) |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun xcpIfDataAckUpdatesEndianEvenWhenLaterSetupFails() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
f.reject = 0x42 |
|
|
|
assertTrue(f.repository.configureMeasurement(xcpConfiguration(true)) is DctResult.Failure) |
|
|
|
assertXcpCalibrationPackets(f, true) |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun xcpMissingIfDataUsesDefaultLittleEndianAfterMsbProject() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
assertTrue(f.repository.sendXcpIfDatas(A2lParser.XcpIfData(isMsbFirst = true)) is DctResult.Success) |
|
|
|
assertTrue(f.repository.configureMeasurement(xcpConfiguration(true).copy(xcpIfData = null)) is DctResult.Success) |
|
|
|
assertXcpCalibrationPackets(f, false) |
|
|
|
} |
|
|
|
|
|
|
|
private fun xcpConfiguration(msb: Boolean) = DctMeasurementConfiguration( |
|
|
|
a2lSignals = listOf(SignalDefinition(1, 1, "Calibration", MdbAttrType.ATTR_2_UINT16, |
|
|
|
1.0, 0.0, "", 0, 0.0, 65535.0, adl = 0x12345678, a2lDataType = A2lDataType.UWORD)), |
|
|
|
xcpIfData = A2lParser.XcpIfData(isMsbFirst = msb) |
|
|
|
) |
|
|
|
|
|
|
|
private suspend fun assertXcpCalibrationPackets(f: Fixture, msb: Boolean) { |
|
|
|
val valueBytes = if (msb) byteArrayOf(0x10, 0x20) else byteArrayOf(0x20, 0x10) |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 0x52.toByte() }) } coAnswers { |
|
|
|
f.sentPackets += firstArg<DctPacket>() |
|
|
|
f.packets.emit(DctPacket(0xD2.toByte(), byteArrayOf(1) + valueBytes)) |
|
|
|
true |
|
|
|
} |
|
|
|
assertTrue(f.repository.writeXcpCharacteristic(0x12345678, A2lDataType.UWORD, 0x1020.toDouble()) is DctResult.Success) |
|
|
|
val read = f.repository.readXcpCharacteristic(0x12345678, A2lDataType.UWORD) |
|
|
|
assertTrue(read is DctResult.Success) |
|
|
|
assertArrayEquals(valueBytes, (read as DctResult.Success).data) |
|
|
|
// PC Characteristic payloads: [type][value][address] / [type][address]. |
|
|
|
val expectedWrite = if (msb) byteArrayOf(2, 0x10, 0x20, 0x12, 0x34, 0x56, 0x78) |
|
|
|
else byteArrayOf(2, 0x20, 0x10, 0x78, 0x56, 0x34, 0x12) |
|
|
|
val expectedRead = if (msb) byteArrayOf(2, 0x12, 0x34, 0x56, 0x78) |
|
|
|
else byteArrayOf(2, 0x78, 0x56, 0x34, 0x12) |
|
|
|
assertArrayEquals(expectedWrite, f.sentPackets.last { it.command == 0x51.toByte() }.payload) |
|
|
|
assertArrayEquals(expectedRead, f.sentPackets.last { it.command == 0x52.toByte() }.payload) |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun responseBudgetIncludesSlowSend() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
coEvery { f.wifi.sendCommand(any()) } coAnswers { kotlinx.coroutines.delay(1000); true } |
|
|
|
@ -43,16 +129,27 @@ class PacketRepositoryResponseTest { |
|
|
|
val wifi = mockk<WifiSocketManager>(relaxed = true) |
|
|
|
val packets = MutableSharedFlow<DctPacket>(extraBufferCapacity = 8) |
|
|
|
val connected = MutableStateFlow(true) |
|
|
|
var sessionId = 0L |
|
|
|
var reject: Byte? = null |
|
|
|
var silent: Byte? = null |
|
|
|
var versionPayload = byteArrayOf(1, 65) |
|
|
|
val sentPackets = mutableListOf<DctPacket>() |
|
|
|
init { |
|
|
|
every { wifi.isConnected } returns connected |
|
|
|
every { wifi.packetStream } returns packets |
|
|
|
every { wifi.currentSessionId } answers { sessionId } |
|
|
|
every { wifi.invalidateConnectionIfSession(any()) } answers { |
|
|
|
if (firstArg<Long>() != 0L && firstArg<Long>() == wifi.currentSessionId && connected.value) { |
|
|
|
sessionId = 0L |
|
|
|
connected.value = false |
|
|
|
true |
|
|
|
} else false |
|
|
|
} |
|
|
|
coEvery { wifi.disconnectSuspend() } coAnswers { connected.value = false } |
|
|
|
coEvery { wifi.sendCommand(any()) } coAnswers { |
|
|
|
val request = firstArg<DctPacket>() |
|
|
|
sentPackets += request |
|
|
|
if (request.command == silent) return@coAnswers true |
|
|
|
val subCmd = request.payload.firstOrNull() ?: 1 |
|
|
|
val payload = when (request.command.toInt()) { |
|
|
|
1 -> versionPayload |
|
|
|
@ -69,6 +166,73 @@ class PacketRepositoryResponseTest { |
|
|
|
val repository = PacketRepositoryImpl(wifiSocketManager = wifi, ioDispatcher = dispatcher, responseTimeoutMs = 50) |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun firstEnvironmentQueryWaits200msAfterVersionWriteCompletes() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
var versionWrittenAt = -1L |
|
|
|
var firstEnvAt = -1L |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 1.toByte() }) } coAnswers { |
|
|
|
kotlinx.coroutines.delay(75) |
|
|
|
f.packets.emit(DctPacket(0x81.toByte(), byteArrayOf(1, 65))) |
|
|
|
versionWrittenAt = testScheduler.currentTime |
|
|
|
true |
|
|
|
} |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 9.toByte() }) } coAnswers { |
|
|
|
if (firstEnvAt < 0) firstEnvAt = testScheduler.currentTime |
|
|
|
f.packets.emit(DctPacket(0x89.toByte(), byteArrayOf(firstArg<DctPacket>().payload[0], 1))) |
|
|
|
true |
|
|
|
} |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
assertTrue(versionWrittenAt >= 0) |
|
|
|
assertEquals(200L, firstEnvAt - versionWrittenAt) |
|
|
|
} |
|
|
|
|
|
|
|
@ParameterizedTest |
|
|
|
@CsvSource("0,false", "0,true", "9,false", "9,true", "18,false", "18,true", "3,false", "3,true", "4,false", "4,true") |
|
|
|
fun failedHandshakeClosesOnlyFailedSessionAndCanReconnect(command: Int, nack: Boolean) = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
every { f.wifi.currentSessionId } returns 7L |
|
|
|
every { f.wifi.invalidateConnectionIfSession(7L) } answers { f.connected.value = false; true } |
|
|
|
if (nack) f.reject = command.toByte() else f.silent = command.toByte() |
|
|
|
if (nack && command == 3) { |
|
|
|
// HWID has no status byte: its failure path is a truncated address. |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 3.toByte() }) } coAnswers { |
|
|
|
f.packets.emit(DctPacket(0x83.toByte(), ByteArray(if (f.reject == 3.toByte()) 5 else 6))) |
|
|
|
true |
|
|
|
} |
|
|
|
} |
|
|
|
f.repository.setInitialMeasurementConfiguration(DctMeasurementConfiguration(sensorModules = createDefaultSensorModules())) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Failure) |
|
|
|
assertEquals(DeviceConnectionState.ERROR, f.repository.connectionState.value) |
|
|
|
assertFalse(f.repository.isConnected.value) |
|
|
|
assertFalse(f.connected.value) |
|
|
|
verify(exactly = 1) { f.wifi.invalidateConnectionIfSession(7L) } |
|
|
|
assertFalse(f.sentPackets.any { it.command == 0x0F.toByte() || it.command == 0x20.toByte() }) |
|
|
|
|
|
|
|
// A new session must not inherit the failed session's response quarantine. |
|
|
|
every { f.wifi.currentSessionId } returns 8L |
|
|
|
f.connected.value = true |
|
|
|
f.reject = null |
|
|
|
f.silent = null |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
assertEquals(DeviceConnectionState.READY, f.repository.connectionState.value) |
|
|
|
verify(exactly = 0) { f.wifi.invalidateConnectionIfSession(8L) } |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun cancelledHandshakeDuringPcGapClosesItsSocket() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
every { f.wifi.currentSessionId } returns 7L |
|
|
|
every { f.wifi.invalidateConnectionIfSession(7L) } answers { f.connected.value = false; true } |
|
|
|
val connect = async { f.repository.connect("") } |
|
|
|
advanceTimeBy(100) |
|
|
|
connect.cancel() |
|
|
|
runCurrent() |
|
|
|
assertTrue(connect.isCancelled) |
|
|
|
assertFalse(f.connected.value) |
|
|
|
assertEquals(DeviceConnectionState.ERROR, f.repository.connectionState.value) |
|
|
|
verify(exactly = 1) { f.wifi.invalidateConnectionIfSession(7L) } |
|
|
|
coVerify(exactly = 0) { f.wifi.sendCommand(match { it.command == 9.toByte() }) } |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun negativeEnableNeverReachesReady() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
f.reject = 0 |
|
|
|
@ -106,7 +270,7 @@ class PacketRepositoryResponseTest { |
|
|
|
val env = f.sentPackets.filter { it.command == 0x09.toByte() } |
|
|
|
assertTrue(env.any { it.payload.contentEquals(byteArrayOf(0x06, 0x11)) }) |
|
|
|
val sm = f.sentPackets.first { it.command == 0x12.toByte() } |
|
|
|
assertTrue(sm.payload.contentEquals(byteArrayOf(0x01, 0x01, 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte(), 0xFF.toByte()))) |
|
|
|
assertTrue(sm.payload.contentEquals(byteArrayOf(0x01, 0x01, 0xFF.toByte(), 0x00, 0x00, 0x00))) |
|
|
|
} |
|
|
|
@Test fun truncatedVersionStillStopsHandshakeBeforeHardwareId() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
@ -126,6 +290,75 @@ class PacketRepositoryResponseTest { |
|
|
|
) |
|
|
|
assertTrue(f.repository.configureMeasurement(config) is DctResult.Failure) |
|
|
|
coVerify(exactly = 0) { f.wifi.sendCommand(match { it.command == 0x10.toByte() }) } |
|
|
|
assertTrue(f.connected.value) |
|
|
|
assertEquals(DeviceConnectionState.READY, f.repository.connectionState.value) |
|
|
|
} |
|
|
|
|
|
|
|
@ParameterizedTest |
|
|
|
@ValueSource(ints = [8, 9, 18, 65]) |
|
|
|
fun configurationTimeoutClosesTcpEvenWithHeartbeatAndIgnoresLateAck(command: Int) = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
f.sessionId = 7L |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
f.sentPackets.clear() |
|
|
|
f.silent = command.toByte() |
|
|
|
val configuration = if (command == 65) xcpConfiguration(false) |
|
|
|
else DctMeasurementConfiguration(projectName = "TEST") |
|
|
|
val setup = async { f.repository.configureMeasurement(configuration) } |
|
|
|
runCurrent() |
|
|
|
if (command == 9 || command == 18) { |
|
|
|
advanceTimeBy(120) // CAN-to-DID 100ms plus DID-to-environment 20ms. |
|
|
|
runCurrent() |
|
|
|
} |
|
|
|
assertEquals(command, f.sentPackets.last().command.toInt() and 255) |
|
|
|
f.packets.emit(DctPacket(0x85.toByte(), byteArrayOf(1), transportSessionId = 7L)) |
|
|
|
advanceTimeBy(49) |
|
|
|
assertFalse(setup.isCompleted) |
|
|
|
assertTrue(f.connected.value) |
|
|
|
advanceTimeBy(1) |
|
|
|
runCurrent() |
|
|
|
|
|
|
|
val result = setup.await() |
|
|
|
assertTrue(result is DctResult.Failure && result.error is com.dct.app.core.error.DctError.ProtocolError.ResponseTimeout) |
|
|
|
assertFalse(f.connected.value) |
|
|
|
assertFalse(f.repository.isConnected.value) |
|
|
|
assertFalse(f.repository.isStreaming.value) |
|
|
|
assertEquals(DeviceConnectionState.ERROR, f.repository.connectionState.value) |
|
|
|
verify(exactly = 1) { f.wifi.invalidateConnectionIfSession(7L) } |
|
|
|
val sentAtTimeout = f.sentPackets.size |
|
|
|
f.packets.emit(DctPacket((command + 0x80).toByte(), byteArrayOf(0, 1), transportSessionId = 7L)) |
|
|
|
assertEquals(sentAtTimeout, f.sentPackets.size) |
|
|
|
assertEquals(DeviceConnectionState.ERROR, f.repository.connectionState.value) |
|
|
|
assertTrue(f.repository.startStreaming() is DctResult.Failure) |
|
|
|
assertFalse(f.sentPackets.any { it.command == 0x20.toByte() }) |
|
|
|
|
|
|
|
f.sessionId = 8L |
|
|
|
f.connected.value = true |
|
|
|
f.silent = null |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
assertTrue(f.repository.configureMeasurement(configuration) is DctResult.Success) |
|
|
|
f.packets.emit(DctPacket((command + 0x80).toByte(), byteArrayOf(0, 1), transportSessionId = 7L)) |
|
|
|
assertTrue(f.connected.value) |
|
|
|
assertEquals(DeviceConnectionState.READY, f.repository.connectionState.value) |
|
|
|
verify(exactly = 0) { f.wifi.invalidateConnectionIfSession(8L) } |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun oldConfigurationTimeoutDoesNotCloseNewerTcpSession() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
f.sessionId = 7L |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
f.silent = 8 |
|
|
|
val setup = async { f.repository.configureMeasurement(DctMeasurementConfiguration(projectName = "TEST")) } |
|
|
|
runCurrent() |
|
|
|
f.sessionId = 8L |
|
|
|
advanceTimeBy(50) |
|
|
|
runCurrent() |
|
|
|
assertTrue(setup.await() is DctResult.Failure) |
|
|
|
verify(exactly = 1) { f.wifi.invalidateConnectionIfSession(7L) } |
|
|
|
assertTrue(f.connected.value) |
|
|
|
assertTrue(f.repository.isConnected.value) |
|
|
|
assertEquals(DeviceConnectionState.READY, f.repository.connectionState.value) |
|
|
|
verify(exactly = 0) { f.wifi.invalidateConnectionIfSession(8L) } |
|
|
|
} |
|
|
|
@Test fun timeoutKeepsTransportAndBlocksAmbiguousRetryUntilLateAck() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
@ -143,15 +376,195 @@ class PacketRepositoryResponseTest { |
|
|
|
} |
|
|
|
assertTrue(f.repository.sendCommand(command) is DctResult.Success) |
|
|
|
} |
|
|
|
@Test fun startTimeoutKeepsTransportAliveForRetry() = runTest { |
|
|
|
@ParameterizedTest |
|
|
|
@ValueSource(booleans = [false, true]) |
|
|
|
fun ambiguousStartTimeoutKeepsTcpUntilExplicitDisconnect(stopAckLost: Boolean) = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == DctCommand.CMD_START_TRANSMIT_DATA.code }) } returns true |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == DctCommand.CMD_START_TRANSMIT_DATA.code }) } coAnswers { |
|
|
|
f.sentPackets += firstArg<DctPacket>() |
|
|
|
true |
|
|
|
} |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 0x21.toByte() }) } coAnswers { |
|
|
|
f.sentPackets += firstArg<DctPacket>() |
|
|
|
// A late successful START must not revive the run during rollback. |
|
|
|
f.packets.emit(DctPacket(0xA0.toByte(), byteArrayOf(1, 1))) |
|
|
|
if (!stopAckLost) f.packets.emit(DctPacket(0xA1.toByte(), byteArrayOf(1))) |
|
|
|
true |
|
|
|
} |
|
|
|
|
|
|
|
assertTrue(f.repository.startStreaming(30) is DctResult.Failure) |
|
|
|
assertTrue(f.connected.value) |
|
|
|
assertFalse(f.repository.isStreaming.value) |
|
|
|
assertEquals(DeviceConnectionState.READY, f.repository.connectionState.value) |
|
|
|
assertFalse(f.sentPackets.any { it.command == 0x21.toByte() || it.command == 0x0F.toByte() }) |
|
|
|
coVerify(exactly = 0) { f.wifi.disconnectSuspend() } |
|
|
|
f.packets.emit(DctPacket(0xA0.toByte(), byteArrayOf(1, 1))) |
|
|
|
assertFalse(f.repository.isStreaming.value) |
|
|
|
assertEquals(DeviceConnectionState.READY, f.repository.connectionState.value) |
|
|
|
|
|
|
|
// Only explicit Disconnect rolls back the uncertain START and closes TCP. |
|
|
|
assertTrue(f.repository.disconnect() is DctResult.Success) |
|
|
|
assertEquals(listOf(0x20, 0x21, 0x0F), f.sentPackets.takeLast(3).map { it.command.toInt() and 255 }) |
|
|
|
coVerify(exactly = 1) { f.wifi.disconnectSuspend() } |
|
|
|
assertFalse(f.connected.value) |
|
|
|
assertFalse(f.repository.isStreaming.value) |
|
|
|
assertEquals(DeviceConnectionState.DISCONNECTED, f.repository.connectionState.value) |
|
|
|
f.packets.emit(DctPacket(0xA0.toByte(), byteArrayOf(1, 1))) |
|
|
|
assertFalse(f.repository.isStreaming.value) |
|
|
|
assertEquals(DeviceConnectionState.DISCONNECTED, f.repository.connectionState.value) |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun cancelledStartStillSendsStopBeforeDisableOnDisconnect() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 0x20.toByte() }) } coAnswers { |
|
|
|
f.sentPackets += firstArg<DctPacket>() |
|
|
|
true |
|
|
|
} |
|
|
|
val start = async { f.repository.startStreaming(30) } |
|
|
|
runCurrent() |
|
|
|
start.cancel() |
|
|
|
start.join() |
|
|
|
assertTrue(f.repository.disconnect() is DctResult.Success) |
|
|
|
assertEquals(listOf(0x20, 0x21, 0x0F), f.sentPackets.takeLast(3).map { it.command.toInt() and 255 }) |
|
|
|
assertFalse(f.connected.value) |
|
|
|
} |
|
|
|
|
|
|
|
@ParameterizedTest |
|
|
|
@ValueSource(longs = [0L, 75L]) |
|
|
|
fun pcPostCanSetupSendsDidWithoutWaitingForAckThenRefreshesEnvironment(writeDelayMs: Long) = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
f.sentPackets.clear() |
|
|
|
var didWrittenAt = -1L |
|
|
|
var didStartedAt = -1L |
|
|
|
var firstEnvAt = -1L |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 0x17.toByte() }) } coAnswers { |
|
|
|
f.sentPackets += firstArg<DctPacket>() |
|
|
|
didStartedAt = testScheduler.currentTime |
|
|
|
kotlinx.coroutines.delay(writeDelayMs) |
|
|
|
didWrittenAt = testScheduler.currentTime |
|
|
|
true // PC does not gate setup completion on a 0x97 response. |
|
|
|
} |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 9.toByte() && it.payload.firstOrNull() == 0.toByte() }) } coAnswers { |
|
|
|
f.sentPackets += firstArg<DctPacket>() |
|
|
|
firstEnvAt = testScheduler.currentTime |
|
|
|
f.packets.emit(DctPacket(0x89.toByte(), byteArrayOf(0, 1))) |
|
|
|
true |
|
|
|
} |
|
|
|
val before = testScheduler.currentTime |
|
|
|
assertTrue(f.repository.configureMeasurement(DctMeasurementConfiguration( |
|
|
|
didPreSetting = 0x31, isHighSpeedCan = false, sensorModules = createDefaultSensorModules() |
|
|
|
)) is DctResult.Success) |
|
|
|
assertEquals(listOf(0x40, 0x11, 0x17, 0x09, 0x09, 0x09, 0x12), |
|
|
|
f.sentPackets.map { it.command.toInt() and 255 }) |
|
|
|
assertEquals(100L, didStartedAt - before) |
|
|
|
assertEquals(writeDelayMs, didWrittenAt - didStartedAt) |
|
|
|
assertEquals(20L, firstEnvAt - didWrittenAt) |
|
|
|
assertArrayEquals(byteArrayOf(0x31), f.sentPackets[2].payload) |
|
|
|
assertEquals(listOf(0, 3, 6), f.sentPackets.filter { it.command == 9.toByte() } |
|
|
|
.map { it.payload.first().toInt() }) |
|
|
|
assertArrayEquals(byteArrayOf(6, 0), f.sentPackets.last { it.command == 9.toByte() }.payload) |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun cancellingDidEnvironmentGapDoesNotSendTheNextRequest() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
f.sentPackets.clear() |
|
|
|
val setup = async { f.repository.configureMeasurement(DctMeasurementConfiguration()) } |
|
|
|
advanceTimeBy(119) |
|
|
|
runCurrent() |
|
|
|
assertEquals(listOf(0x40, 0x11, 0x17), f.sentPackets.map { it.command.toInt() and 255 }) |
|
|
|
setup.cancel() |
|
|
|
runCurrent() |
|
|
|
advanceTimeBy(50) |
|
|
|
runCurrent() |
|
|
|
assertTrue(setup.isCancelled) |
|
|
|
assertFalse(f.sentPackets.any { it.command == 9.toByte() || it.command == 0x20.toByte() }) |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun rejectedCanRegistrationDoesNotSendDidOrPostSetup() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
f.sentPackets.clear() |
|
|
|
f.reject = 0x11 |
|
|
|
assertTrue(f.repository.configureMeasurement(DctMeasurementConfiguration()) is DctResult.Failure) |
|
|
|
assertEquals(listOf(0x40, 0x11), f.sentPackets.map { it.command.toInt() and 255 }) |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun didWriteFailureStopsTheSetupBeforeEnvironmentRefresh() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
f.sentPackets.clear() |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 0x17.toByte() }) } returns false |
|
|
|
assertTrue(f.repository.configureMeasurement(DctMeasurementConfiguration()) is DctResult.Failure) |
|
|
|
assertEquals(listOf(0x40, 0x11), f.sentPackets.map { it.command.toInt() and 255 }) |
|
|
|
} |
|
|
|
|
|
|
|
@ParameterizedTest |
|
|
|
@ValueSource(ints = [0, 49, 255]) |
|
|
|
fun didPresetMaskIsSerializedWithoutOverwritingProjectIntent(mask: Int) = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
assertTrue(f.repository.configureMeasurement(DctMeasurementConfiguration(didPreSetting = mask)) is DctResult.Success) |
|
|
|
assertArrayEquals(byteArrayOf(mask.toByte()), f.sentPackets.single { it.command == 0x17.toByte() }.payload) |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun explicitStartRejectionAndMalformedResponseKeepTcpWithoutPretendingToMeasure() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 0x20.toByte() }) } coAnswers { |
|
|
|
f.packets.emit(DctPacket(0xA0.toByte(), byteArrayOf(2, 1))) |
|
|
|
true |
|
|
|
} |
|
|
|
assertTrue(f.repository.startStreaming(30) is DctResult.Failure) |
|
|
|
assertEquals(DeviceConnectionState.READY, f.repository.connectionState.value) |
|
|
|
assertTrue(f.connected.value) |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 0x20.toByte() }) } coAnswers { |
|
|
|
f.packets.emit(DctPacket(0xA0.toByte(), byteArrayOf(1))) |
|
|
|
true |
|
|
|
} |
|
|
|
assertTrue(f.repository.startStreaming(30) is DctResult.Failure) |
|
|
|
assertTrue(f.connected.value) |
|
|
|
assertTrue(f.repository.isConnected.value) |
|
|
|
assertFalse(f.repository.isStreaming.value) |
|
|
|
assertEquals(DeviceConnectionState.READY, f.repository.connectionState.value) |
|
|
|
assertFalse(f.sentPackets.any { it.command == 0x21.toByte() || it.command == 0x0F.toByte() }) |
|
|
|
coVerify(exactly = 0) { f.wifi.disconnectSuspend() } |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun configurationTimeoutAfterPlaybackKeepsTcpUntilExplicitDisconnectAndNewSessionResetsPolicy() = runTest { |
|
|
|
val f = Fixture(UnconfinedTestDispatcher(testScheduler)) |
|
|
|
f.sessionId = 7L |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
coEvery { f.wifi.sendCommand(match { it.command == 0x20.toByte() }) } coAnswers { |
|
|
|
f.sentPackets += firstArg<DctPacket>() |
|
|
|
f.packets.emit(DctPacket(0xA0.toByte(), byteArrayOf(1, 1))) |
|
|
|
true |
|
|
|
} |
|
|
|
assertTrue(f.repository.startStreaming() is DctResult.Success) |
|
|
|
assertTrue(f.repository.stopStreaming() is DctResult.Success) |
|
|
|
f.silent = 9 |
|
|
|
assertTrue(f.repository.configureMeasurement(DctMeasurementConfiguration()) is DctResult.Failure) |
|
|
|
assertEquals(DeviceConnectionState.READY, f.repository.connectionState.value) |
|
|
|
assertTrue(f.connected.value) |
|
|
|
verify(exactly = 0) { f.wifi.invalidateConnectionIfSession(any()) } |
|
|
|
|
|
|
|
assertTrue(f.repository.disconnect() is DctResult.Success) |
|
|
|
assertFalse(f.connected.value) |
|
|
|
coVerify(exactly = 1) { f.wifi.disconnectSuspend() } |
|
|
|
assertEquals(0x0F.toByte(), f.sentPackets.last().command) |
|
|
|
|
|
|
|
f.sessionId = 8L |
|
|
|
f.connected.value = true |
|
|
|
f.silent = null |
|
|
|
assertTrue(f.repository.connect("") is DctResult.Success) |
|
|
|
f.silent = 9 |
|
|
|
assertTrue(f.repository.configureMeasurement(DctMeasurementConfiguration()) is DctResult.Failure) |
|
|
|
assertFalse(f.connected.value) |
|
|
|
assertEquals(DeviceConnectionState.ERROR, f.repository.connectionState.value) |
|
|
|
verify(exactly = 1) { f.wifi.invalidateConnectionIfSession(8L) } |
|
|
|
} |
|
|
|
|
|
|
|
@Test fun stopTimeoutEndsLocalMeasurementButKeepsTransportAlive() = runTest { |
|
|
|
|