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:
James Rich 2026-03-23 18:17:50 -05:00 committed by GitHub
parent f826cac6c8
commit 664ebf218e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
163 changed files with 503 additions and 4993 deletions

View file

@ -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()

View file

@ -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()) }
}
}

View file

@ -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) {

View file

@ -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>()

View file

@ -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

View file

@ -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 {