mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor: null safety, update date/time libraries, and migrate tests (#4900)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
f826cac6c8
commit
664ebf218e
163 changed files with 503 additions and 4993 deletions
|
|
@ -17,6 +17,7 @@
|
|||
package org.meshtastic.core.network.radio
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.everySuspend
|
||||
import dev.mokkery.matcher.any
|
||||
|
|
@ -28,9 +29,6 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.ble.BleConnection
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleConnectionState
|
||||
|
|
@ -39,6 +37,9 @@ import org.meshtastic.core.ble.BleScanner
|
|||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.ble.BluetoothState
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
import kotlin.test.BeforeTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class BleRadioInterfaceTest {
|
||||
|
|
@ -54,8 +55,8 @@ class BleRadioInterfaceTest {
|
|||
private val connectionStateFlow = MutableSharedFlow<BleConnectionState>(replay = 1)
|
||||
private val bluetoothStateFlow = MutableStateFlow(BluetoothState())
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
every { connectionFactory.create(any(), any()) } returns connection
|
||||
every { connection.connectionState } returns connectionStateFlow
|
||||
every { bluetoothRepository.state } returns bluetoothStateFlow.asStateFlow()
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.network.radio
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.matcher.any
|
||||
import dev.mokkery.mock
|
||||
import dev.mokkery.verify
|
||||
import dev.mokkery.verify.VerifyMode
|
||||
import dev.mokkery.verifyNoMoreCalls
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
|
||||
class StreamInterfaceTest {
|
||||
|
||||
private val service: RadioInterfaceService = mock(MockMode.autofill)
|
||||
|
||||
// Concrete implementation for testing
|
||||
private class TestStreamInterface(service: RadioInterfaceService) : StreamInterface(service) {
|
||||
override fun sendBytes(p: ByteArray) {}
|
||||
|
||||
fun testReadChar(c: Byte) = readChar(c)
|
||||
}
|
||||
|
||||
private val streamInterface = TestStreamInterface(service)
|
||||
|
||||
@Test
|
||||
fun `readChar delivers a 1-byte packet`() {
|
||||
// Header: START1, START2, LenMSB=0, LenLSB=1
|
||||
val packet = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x00, 0x01, 0x42)
|
||||
|
||||
packet.forEach { streamInterface.testReadChar(it) }
|
||||
|
||||
verify { service.handleFromRadio(byteArrayOf(0x42)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readChar handles zero length packet`() {
|
||||
// Header: START1, START2, LenMSB=0, LenLSB=0
|
||||
val packet = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x00, 0x00)
|
||||
|
||||
packet.forEach { streamInterface.testReadChar(it) }
|
||||
|
||||
verify { service.handleFromRadio(byteArrayOf()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readChar loses sync on invalid START2`() {
|
||||
// START1, wrong START2, START1, START2, LenMSB=0, LenLSB=1, payload
|
||||
val data = byteArrayOf(0x94.toByte(), 0x00, 0x94.toByte(), 0xc3.toByte(), 0x00, 0x01, 0x55)
|
||||
|
||||
data.forEach { streamInterface.testReadChar(it) }
|
||||
|
||||
verify { service.handleFromRadio(byteArrayOf(0x55)) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readChar handles multiple packets sequentially`() {
|
||||
val packet1 = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x00, 0x01, 0x11)
|
||||
val packet2 = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x00, 0x01, 0x22)
|
||||
|
||||
packet1.forEach { streamInterface.testReadChar(it) }
|
||||
packet2.forEach { streamInterface.testReadChar(it) }
|
||||
|
||||
verify { service.handleFromRadio(byteArrayOf(0x11)) }
|
||||
verify { service.handleFromRadio(byteArrayOf(0x22)) }
|
||||
verifyNoMoreCalls(service)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readChar handles large packet up to MAX_TO_FROM_RADIO_SIZE`() {
|
||||
val size = 512
|
||||
val payload = ByteArray(size) { it.toByte() }
|
||||
val header = byteArrayOf(0x94.toByte(), 0xc3.toByte(), (size shr 8).toByte(), (size and 0xff).toByte())
|
||||
|
||||
header.forEach { streamInterface.testReadChar(it) }
|
||||
payload.forEach { streamInterface.testReadChar(it) }
|
||||
|
||||
verify { service.handleFromRadio(payload) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readChar loses sync on overly large packet length`() {
|
||||
// 513 bytes is > 512
|
||||
val header = byteArrayOf(0x94.toByte(), 0xc3.toByte(), 0x02, 0x01)
|
||||
|
||||
header.forEach { streamInterface.testReadChar(it) }
|
||||
|
||||
// Should ignore and reset, not expecting handleFromRadio
|
||||
verify(mode = VerifyMode.exactly(0)) { service.handleFromRadio(any()) }
|
||||
}
|
||||
}
|
||||
|
|
@ -298,7 +298,7 @@ class MockInterface(private val service: RadioInterfaceService, val address: Str
|
|||
private fun sendFakeAck(pr: ToRadio) = service.serviceScope.handledLaunch {
|
||||
val packet = pr.packet ?: return@handledLaunch
|
||||
delay(2000)
|
||||
service.handleFromRadio(makeAck(MY_NODE + 1, packet.from ?: 0, packet.id).encode())
|
||||
service.handleFromRadio(makeAck(MY_NODE + 1, packet.from, packet.id).encode())
|
||||
}
|
||||
|
||||
private fun sendConfigResponse(configId: Int) {
|
||||
|
|
|
|||
|
|
@ -123,22 +123,21 @@ class MQTTRepositoryImpl(
|
|||
|
||||
client = newClient
|
||||
|
||||
clientJob =
|
||||
scope.launch {
|
||||
try {
|
||||
Logger.i { "MQTT Starting client loop for $host:$port" }
|
||||
newClient.runSuspend()
|
||||
} catch (e: io.github.davidepianca98.mqtt.MQTTException) {
|
||||
Logger.e(e) { "MQTT Client loop error (MQTT)" }
|
||||
close(e)
|
||||
} catch (e: io.github.davidepianca98.socket.IOException) {
|
||||
Logger.e(e) { "MQTT Client loop error (IO)" }
|
||||
close(e)
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
Logger.i { "MQTT Client loop cancelled" }
|
||||
throw e
|
||||
}
|
||||
clientJob = scope.launch {
|
||||
try {
|
||||
Logger.i { "MQTT Starting client loop for $host:$port" }
|
||||
newClient.runSuspend()
|
||||
} catch (e: io.github.davidepianca98.mqtt.MQTTException) {
|
||||
Logger.e(e) { "MQTT Client loop error (MQTT)" }
|
||||
close(e)
|
||||
} catch (e: io.github.davidepianca98.socket.IOException) {
|
||||
Logger.e(e) { "MQTT Client loop error (IO)" }
|
||||
close(e)
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
Logger.i { "MQTT Client loop cancelled" }
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
// Subscriptions
|
||||
val subscriptions = mutableListOf<Subscription>()
|
||||
|
|
|
|||
|
|
@ -288,14 +288,13 @@ class TcpTransport(
|
|||
|
||||
private fun startHeartbeat(address: String) {
|
||||
heartbeatJob?.cancel()
|
||||
heartbeatJob =
|
||||
scope.launch {
|
||||
while (true) {
|
||||
delay(HEARTBEAT_INTERVAL_MILLIS)
|
||||
Logger.d { "$logTag: [$address] Sending heartbeat" }
|
||||
sendHeartbeat()
|
||||
}
|
||||
heartbeatJob = scope.launch {
|
||||
while (true) {
|
||||
delay(HEARTBEAT_INTERVAL_MILLIS)
|
||||
Logger.d { "$logTag: [$address] Sending heartbeat" }
|
||||
sendHeartbeat()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
|
|||
|
|
@ -17,11 +17,11 @@
|
|||
package org.meshtastic.core.network.radio
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.meshtastic.core.network.transport.StreamFrameCodec
|
||||
import org.meshtastic.proto.Heartbeat
|
||||
import org.meshtastic.proto.ToRadio
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class TCPInterfaceTest {
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue