mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Refactor map layer management and navigation infrastructure (#4921)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
b608a04ca4
commit
a005231d94
142 changed files with 5408 additions and 3090 deletions
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.testing.setupTestContext
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import kotlin.test.BeforeTest
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class MeshLogRepositoryTest : CommonMeshLogRepositoryTest() {
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
setupTestContext()
|
||||
setupRepo()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.testing.setupTestContext
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import kotlin.test.BeforeTest
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class NodeRepositoryTest : CommonNodeRepositoryTest() {
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
setupTestContext()
|
||||
setupRepo()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import org.junit.runner.RunWith
|
||||
import org.meshtastic.core.testing.setupTestContext
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import kotlin.test.BeforeTest
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [34])
|
||||
class PacketRepositoryTest : CommonPacketRepositoryTest() {
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
setupTestContext()
|
||||
setupRepo()
|
||||
}
|
||||
}
|
||||
|
|
@ -29,13 +29,14 @@ import org.meshtastic.core.database.entity.FirmwareReleaseType
|
|||
import org.meshtastic.core.database.entity.asExternalModel
|
||||
import org.meshtastic.core.model.util.TimeConstants
|
||||
import org.meshtastic.core.network.FirmwareReleaseRemoteDataSource
|
||||
import org.meshtastic.core.repository.FirmwareReleaseRepository
|
||||
|
||||
@Single
|
||||
class FirmwareReleaseRepository(
|
||||
open class FirmwareReleaseRepositoryImpl(
|
||||
private val remoteDataSource: FirmwareReleaseRemoteDataSource,
|
||||
private val localDataSource: FirmwareReleaseLocalDataSource,
|
||||
private val jsonDataSource: FirmwareReleaseJsonDataSource,
|
||||
) {
|
||||
) : FirmwareReleaseRepository {
|
||||
|
||||
/**
|
||||
* A flow that provides the latest STABLE firmware release. It follows a "cache-then-network" strategy:
|
||||
|
|
@ -44,14 +45,14 @@ class FirmwareReleaseRepository(
|
|||
* 3. Emits the updated version upon successful fetch. Collectors should use `.distinctUntilChanged()` to avoid
|
||||
* redundant UI updates.
|
||||
*/
|
||||
val stableRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.STABLE)
|
||||
override val stableRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.STABLE)
|
||||
|
||||
/**
|
||||
* A flow that provides the latest ALPHA firmware release.
|
||||
*
|
||||
* @see stableRelease for behavior details.
|
||||
*/
|
||||
val alphaRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.ALPHA)
|
||||
override val alphaRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.ALPHA)
|
||||
|
||||
private fun getLatestFirmware(
|
||||
releaseType: FirmwareReleaseType,
|
||||
|
|
@ -118,7 +119,7 @@ class FirmwareReleaseRepository(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun invalidateCache() {
|
||||
override suspend fun invalidateCache() {
|
||||
localDataSource.deleteAllFirmwareReleases()
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
/*
|
||||
* Copyright (c) 2025-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.data.repository
|
||||
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.mock
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.MeshLog
|
||||
import org.meshtastic.core.testing.FakeDatabaseProvider
|
||||
import org.meshtastic.core.testing.FakeMeshLogPrefs
|
||||
import org.meshtastic.proto.Data
|
||||
import org.meshtastic.proto.EnvironmentMetrics
|
||||
import org.meshtastic.proto.FromRadio
|
||||
import org.meshtastic.proto.MeshPacket
|
||||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
abstract class CommonMeshLogRepositoryTest {
|
||||
|
||||
protected lateinit var dbProvider: FakeDatabaseProvider
|
||||
protected lateinit var meshLogPrefs: FakeMeshLogPrefs
|
||||
protected lateinit var nodeInfoReadDataSource: NodeInfoReadDataSource
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher)
|
||||
|
||||
protected lateinit var repository: MeshLogRepositoryImpl
|
||||
|
||||
private val nowMillis = 1000000000L
|
||||
|
||||
fun setupRepo() {
|
||||
dbProvider = FakeDatabaseProvider()
|
||||
meshLogPrefs = FakeMeshLogPrefs()
|
||||
meshLogPrefs.setLoggingEnabled(true)
|
||||
nodeInfoReadDataSource = mock(MockMode.autofill)
|
||||
|
||||
every { nodeInfoReadDataSource.myNodeInfoFlow() } returns MutableStateFlow(null)
|
||||
|
||||
repository = MeshLogRepositoryImpl(dbProvider, dispatchers, meshLogPrefs, nodeInfoReadDataSource)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
dbProvider.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseTelemetryLog preserves zero temperature`() = runTest(testDispatcher) {
|
||||
val zeroTemp = 0.0f
|
||||
val telemetry = Telemetry(environment_metrics = EnvironmentMetrics(temperature = zeroTemp))
|
||||
|
||||
val meshPacket =
|
||||
MeshPacket(decoded = Data(payload = telemetry.encode().toByteString(), portnum = PortNum.TELEMETRY_APP))
|
||||
|
||||
val meshLog =
|
||||
MeshLog(
|
||||
uuid = "123",
|
||||
message_type = "telemetry",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum = 0,
|
||||
portNum = PortNum.TELEMETRY_APP.value,
|
||||
fromRadio = FromRadio(packet = meshPacket),
|
||||
)
|
||||
|
||||
repository.insert(meshLog)
|
||||
|
||||
val result = repository.getTelemetryFrom(0).first()
|
||||
|
||||
assertNotNull(result)
|
||||
assertEquals(1, result.size)
|
||||
val resultMetrics = result[0].environment_metrics
|
||||
assertNotNull(resultMetrics)
|
||||
assertEquals(zeroTemp, resultMetrics.temperature ?: 0f, 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteLogs redirects local node number to NODE_NUM_LOCAL`() = runTest(testDispatcher) {
|
||||
val localNodeNum = 999
|
||||
val port = PortNum.TEXT_MESSAGE_APP.value
|
||||
val myNodeEntity =
|
||||
MyNodeEntity(
|
||||
myNodeNum = localNodeNum,
|
||||
model = "model",
|
||||
firmwareVersion = "1.0",
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false,
|
||||
currentPacketId = 0L,
|
||||
messageTimeoutMsec = 0,
|
||||
minAppVersion = 0,
|
||||
maxChannels = 0,
|
||||
hasWifi = false,
|
||||
)
|
||||
every { nodeInfoReadDataSource.myNodeInfoFlow() } returns MutableStateFlow(myNodeEntity)
|
||||
|
||||
val log =
|
||||
MeshLog(
|
||||
uuid = "123",
|
||||
message_type = "TEXT",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum =
|
||||
0, // asEntity will map it if we pass localNodeNum to asEntity, but here we set it manually
|
||||
portNum = port,
|
||||
fromRadio =
|
||||
FromRadio(
|
||||
packet = MeshPacket(from = localNodeNum, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP)),
|
||||
),
|
||||
)
|
||||
repository.insert(log)
|
||||
|
||||
// Verify it's there
|
||||
assertEquals(1, repository.getAllLogsUnbounded().first().size)
|
||||
|
||||
repository.deleteLogs(localNodeNum, port)
|
||||
|
||||
val logs = repository.getAllLogsUnbounded().first()
|
||||
assertTrue(logs.isEmpty())
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
* Copyright (c) 2025-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.data.repository
|
||||
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.LifecycleRegistry
|
||||
import dev.mokkery.MockMode
|
||||
import dev.mokkery.answering.returns
|
||||
import dev.mokkery.every
|
||||
import dev.mokkery.mock
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.meshtastic.core.data.datasource.NodeInfoReadDataSource
|
||||
import org.meshtastic.core.data.datasource.NodeInfoWriteDataSource
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.database.entity.NodeWithRelations
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.MeshLog
|
||||
import org.meshtastic.core.testing.FakeLocalStatsDataSource
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
abstract class CommonNodeRepositoryTest {
|
||||
|
||||
protected lateinit var lifecycleOwner: LifecycleOwner
|
||||
protected lateinit var readDataSource: NodeInfoReadDataSource
|
||||
protected lateinit var writeDataSource: NodeInfoWriteDataSource
|
||||
protected lateinit var localStatsDataSource: FakeLocalStatsDataSource
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher)
|
||||
|
||||
private val myNodeInfoFlow = MutableStateFlow<MyNodeEntity?>(null)
|
||||
|
||||
protected lateinit var repository: NodeRepositoryImpl
|
||||
|
||||
fun setupRepo() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
lifecycleOwner =
|
||||
object : LifecycleOwner {
|
||||
override val lifecycle = LifecycleRegistry(this)
|
||||
}
|
||||
(lifecycleOwner.lifecycle as LifecycleRegistry).handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
|
||||
|
||||
readDataSource = mock(MockMode.autofill)
|
||||
writeDataSource = mock(MockMode.autofill)
|
||||
localStatsDataSource = FakeLocalStatsDataSource()
|
||||
|
||||
every { readDataSource.myNodeInfoFlow() } returns myNodeInfoFlow
|
||||
every { readDataSource.nodeDBbyNumFlow() } returns MutableStateFlow<Map<Int, NodeWithRelations>>(emptyMap())
|
||||
|
||||
repository =
|
||||
NodeRepositoryImpl(
|
||||
lifecycleOwner.lifecycle,
|
||||
readDataSource,
|
||||
writeDataSource,
|
||||
dispatchers,
|
||||
localStatsDataSource,
|
||||
)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
// Essential to stop background jobs in NodeRepositoryImpl
|
||||
(lifecycleOwner.lifecycle as LifecycleRegistry).handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun createMyNodeEntity(nodeNum: Int) = MyNodeEntity(
|
||||
myNodeNum = nodeNum,
|
||||
model = "model",
|
||||
firmwareVersion = "1.0",
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false,
|
||||
currentPacketId = 0L,
|
||||
messageTimeoutMsec = 0,
|
||||
minAppVersion = 0,
|
||||
maxChannels = 0,
|
||||
hasWifi = false,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `effectiveLogNodeId maps local node number to NODE_NUM_LOCAL`() = runTest(testDispatcher) {
|
||||
val myNodeNum = 12345
|
||||
myNodeInfoFlow.value = createMyNodeEntity(myNodeNum)
|
||||
|
||||
val result = repository.effectiveLogNodeId(myNodeNum).filter { it == MeshLog.NODE_NUM_LOCAL }.first()
|
||||
|
||||
assertEquals(MeshLog.NODE_NUM_LOCAL, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `effectiveLogNodeId preserves remote node numbers`() = runTest(testDispatcher) {
|
||||
val myNodeNum = 12345
|
||||
val remoteNodeNum = 67890
|
||||
myNodeInfoFlow.value = createMyNodeEntity(myNodeNum)
|
||||
|
||||
val result = repository.effectiveLogNodeId(remoteNodeNum).first()
|
||||
|
||||
assertEquals(remoteNodeNum, result)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.database.entity.MyNodeEntity
|
||||
import org.meshtastic.core.di.CoroutineDispatchers
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.testing.FakeDatabaseProvider
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
abstract class CommonPacketRepositoryTest {
|
||||
|
||||
protected lateinit var dbProvider: FakeDatabaseProvider
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher)
|
||||
|
||||
protected lateinit var repository: PacketRepositoryImpl
|
||||
|
||||
fun setupRepo() {
|
||||
dbProvider = FakeDatabaseProvider()
|
||||
repository = PacketRepositoryImpl(dbProvider, dispatchers)
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun tearDown() {
|
||||
dbProvider.close()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `savePacket persists and retrieves waypoints`() = runTest(testDispatcher) {
|
||||
val myNodeNum = 1
|
||||
val contact = "contact"
|
||||
|
||||
// Ensure my_node is present so getMessageCount finds the packet
|
||||
dbProvider.currentDb.value
|
||||
.nodeInfoDao()
|
||||
.setMyNodeInfo(
|
||||
MyNodeEntity(
|
||||
myNodeNum = myNodeNum,
|
||||
model = "model",
|
||||
firmwareVersion = "1.0",
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false,
|
||||
currentPacketId = 0L,
|
||||
messageTimeoutMsec = 0,
|
||||
minAppVersion = 0,
|
||||
maxChannels = 0,
|
||||
hasWifi = false,
|
||||
),
|
||||
)
|
||||
|
||||
val packet = DataPacket(to = "0!ffffffff", bytes = okio.ByteString.EMPTY, dataType = 1, id = 123)
|
||||
|
||||
repository.savePacket(myNodeNum, contact, packet, 1000L)
|
||||
|
||||
// Verify it was saved.
|
||||
val count = repository.getMessageCount(contact)
|
||||
assertEquals(1, count)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearAllUnreadCounts works with real DB`() = runTest(testDispatcher) {
|
||||
repository.clearAllUnreadCounts()
|
||||
// No exception thrown
|
||||
}
|
||||
}
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-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.data.repository
|
||||
|
||||
class MeshLogRepositoryTest {
|
||||
/*
|
||||
|
||||
|
||||
private val dbManager: DatabaseProvider = mock()
|
||||
private val appDatabase: MeshtasticDatabase = mock()
|
||||
private val meshLogDao: MeshLogDao = mock()
|
||||
private val meshLogPrefs: MeshLogPrefs = mock()
|
||||
private val nodeInfoReadDataSource: NodeInfoReadDataSource = mock()
|
||||
private val testDispatcher = UnconfinedTestDispatcher()
|
||||
private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher)
|
||||
|
||||
private val repository = MeshLogRepositoryImpl(dbManager, dispatchers, meshLogPrefs, nodeInfoReadDataSource)
|
||||
|
||||
init {
|
||||
every { dbManager.currentDb } returns MutableStateFlow(appDatabase)
|
||||
every { appDatabase.meshLogDao() } returns meshLogDao
|
||||
every { nodeInfoReadDataSource.myNodeInfoFlow() } returns MutableStateFlow(null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseTelemetryLog preserves zero temperature`() = runTest(testDispatcher) {
|
||||
val zeroTemp = 0.0f
|
||||
val telemetry = Telemetry(environment_metrics = EnvironmentMetrics(temperature = zeroTemp))
|
||||
|
||||
val meshPacket =
|
||||
MeshPacket(decoded = Data(payload = telemetry.encode().toByteString(), portnum = PortNum.TELEMETRY_APP))
|
||||
|
||||
val meshLog =
|
||||
MeshLog(
|
||||
uuid = Uuid.random().toString(),
|
||||
message_type = "telemetry",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromRadio = FromRadio(packet = meshPacket),
|
||||
)
|
||||
|
||||
// Using reflection to test private method parseTelemetryLog
|
||||
val method = MeshLogRepositoryImpl::class.java.getDeclaredMethod("parseTelemetryLog", MeshLog::class.java)
|
||||
method.isAccessible = true
|
||||
val result = method.invoke(repository, meshLog) as Telemetry?
|
||||
|
||||
assertNotNull(result)
|
||||
val resultMetrics = result?.environment_metrics
|
||||
assertNotNull(resultMetrics)
|
||||
assertEquals(zeroTemp, resultMetrics?.temperature ?: 0f, 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `parseTelemetryLog maps missing temperature to NaN`() = runTest(testDispatcher) {
|
||||
val telemetry = Telemetry(environment_metrics = EnvironmentMetrics(temperature = null))
|
||||
|
||||
val meshPacket =
|
||||
MeshPacket(decoded = Data(payload = telemetry.encode().toByteString(), portnum = PortNum.TELEMETRY_APP))
|
||||
|
||||
val meshLog =
|
||||
MeshLog(
|
||||
uuid = Uuid.random().toString(),
|
||||
message_type = "telemetry",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromRadio = FromRadio(packet = meshPacket),
|
||||
)
|
||||
|
||||
val method = MeshLogRepositoryImpl::class.java.getDeclaredMethod("parseTelemetryLog", MeshLog::class.java)
|
||||
method.isAccessible = true
|
||||
val result = method.invoke(repository, meshLog) as Telemetry?
|
||||
|
||||
assertNotNull(result)
|
||||
val resultMetrics = result?.environment_metrics
|
||||
|
||||
// Should be NaN as per repository logic for missing fields
|
||||
assertEquals(Float.NaN, resultMetrics?.temperature ?: 0f, 0.01f)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getRequestLogs filters correctly`() = runTest(testDispatcher) {
|
||||
val targetNode = 123
|
||||
val otherNode = 456
|
||||
val port = PortNum.TRACEROUTE_APP
|
||||
|
||||
val logs =
|
||||
listOf(
|
||||
// Valid request
|
||||
MeshLogEntity(
|
||||
uuid = "1",
|
||||
message_type = "Packet",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum = 0,
|
||||
portNum = port.value,
|
||||
fromRadio =
|
||||
FromRadio(
|
||||
packet =
|
||||
MeshPacket(to = targetNode, decoded = Data(portnum = port, want_response = true)),
|
||||
),
|
||||
),
|
||||
// Wrong target
|
||||
MeshLogEntity(
|
||||
uuid = "2",
|
||||
message_type = "Packet",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum = 0,
|
||||
portNum = port.value,
|
||||
fromRadio =
|
||||
FromRadio(
|
||||
packet =
|
||||
MeshPacket(to = otherNode, decoded = Data(portnum = port, want_response = true)),
|
||||
),
|
||||
),
|
||||
// Not a request (want_response = false)
|
||||
MeshLogEntity(
|
||||
uuid = "3",
|
||||
message_type = "Packet",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum = 0,
|
||||
portNum = port.value,
|
||||
fromRadio =
|
||||
FromRadio(
|
||||
packet =
|
||||
MeshPacket(to = targetNode, decoded = Data(portnum = port, want_response = false)),
|
||||
),
|
||||
),
|
||||
// Wrong fromNum
|
||||
MeshLogEntity(
|
||||
uuid = "4",
|
||||
message_type = "Packet",
|
||||
received_date = nowMillis,
|
||||
raw_message = "",
|
||||
fromNum = 789,
|
||||
portNum = port.value,
|
||||
fromRadio =
|
||||
FromRadio(
|
||||
packet =
|
||||
MeshPacket(to = targetNode, decoded = Data(portnum = port, want_response = true)),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
val result = repository.getRequestLogs(targetNode, port).first()
|
||||
|
||||
assertEquals(1, result.size)
|
||||
assertEquals("1", result[0].uuid)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteLogs redirects local node number to NODE_NUM_LOCAL`() = runTest(testDispatcher) {
|
||||
val localNodeNum = 999
|
||||
val port = 100
|
||||
val myNodeEntity = mock<MyNodeEntity>()
|
||||
every { myNodeEntity.myNodeNum } returns localNodeNum
|
||||
every { nodeInfoReadDataSource.myNodeInfoFlow() } returns MutableStateFlow(myNodeEntity)
|
||||
|
||||
repository.deleteLogs(localNodeNum, port)
|
||||
|
||||
verifySuspend { meshLogDao.deleteLogs(MeshLog.NODE_NUM_LOCAL, port) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteLogs preserves remote node numbers`() = runTest(testDispatcher) {
|
||||
val localNodeNum = 999
|
||||
val remoteNodeNum = 888
|
||||
val port = 100
|
||||
val myNodeEntity = mock<MyNodeEntity>()
|
||||
every { myNodeEntity.myNodeNum } returns localNodeNum
|
||||
every { nodeInfoReadDataSource.myNodeInfoFlow() } returns MutableStateFlow(myNodeEntity)
|
||||
|
||||
repository.deleteLogs(remoteNodeNum, port)
|
||||
|
||||
verifySuspend { meshLogDao.deleteLogs(remoteNodeNum, port) }
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025-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.data.repository
|
||||
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class NodeRepositoryTest {
|
||||
/*
|
||||
|
||||
|
||||
private val lifecycleScope: LifecycleCoroutineScope = mock()
|
||||
|
||||
private val testDispatcher = StandardTestDispatcher()
|
||||
private val dispatchers = CoroutineDispatchers(main = testDispatcher, io = testDispatcher, default = testDispatcher)
|
||||
|
||||
private val myNodeInfoFlow = MutableStateFlow<MyNodeEntity?>(null)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
mockkStatic("androidx.lifecycle.LifecycleKt")
|
||||
every { lifecycleScope.coroutineContext } returns testDispatcher + Job()
|
||||
every { lifecycle.coroutineScope } returns lifecycleScope
|
||||
every { readDataSource.myNodeInfoFlow() } returns myNodeInfoFlow
|
||||
every { readDataSource.nodeDBbyNumFlow() } returns MutableStateFlow(emptyMap())
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
private fun createMyNodeEntity(nodeNum: Int) = MyNodeEntity(
|
||||
myNodeNum = nodeNum,
|
||||
model = "model",
|
||||
firmwareVersion = "1.0",
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false,
|
||||
currentPacketId = 0L,
|
||||
messageTimeoutMsec = 0,
|
||||
minAppVersion = 0,
|
||||
maxChannels = 0,
|
||||
hasWifi = false,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `effectiveLogNodeId maps local node number to NODE_NUM_LOCAL`() = runTest(testDispatcher) {
|
||||
val myNodeNum = 12345
|
||||
myNodeInfoFlow.value = createMyNodeEntity(myNodeNum)
|
||||
|
||||
val repository =
|
||||
NodeRepositoryImpl(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
val result = repository.effectiveLogNodeId(myNodeNum).filter { it == MeshLog.NODE_NUM_LOCAL }.first()
|
||||
|
||||
assertEquals(MeshLog.NODE_NUM_LOCAL, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `effectiveLogNodeId preserves remote node numbers`() = runTest(testDispatcher) {
|
||||
val myNodeNum = 12345
|
||||
val remoteNodeNum = 67890
|
||||
myNodeInfoFlow.value = createMyNodeEntity(myNodeNum)
|
||||
|
||||
val repository =
|
||||
NodeRepositoryImpl(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
val result = repository.effectiveLogNodeId(remoteNodeNum).first()
|
||||
|
||||
assertEquals(remoteNodeNum, result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `effectiveLogNodeId updates when local node number changes`() = runTest(testDispatcher) {
|
||||
val firstNodeNum = 111
|
||||
val secondNodeNum = 222
|
||||
val targetNodeNum = 111
|
||||
|
||||
myNodeInfoFlow.value = createMyNodeEntity(firstNodeNum)
|
||||
val repository =
|
||||
NodeRepositoryImpl(lifecycle, readDataSource, writeDataSource, dispatchers, localStatsDataSource)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Initially should be mapped to LOCAL because it matches
|
||||
assertEquals(
|
||||
MeshLog.NODE_NUM_LOCAL,
|
||||
repository.effectiveLogNodeId(targetNodeNum).filter { it == MeshLog.NODE_NUM_LOCAL }.first(),
|
||||
)
|
||||
|
||||
// Change local node num
|
||||
myNodeInfoFlow.value = createMyNodeEntity(secondNodeNum)
|
||||
testScheduler.runCurrent()
|
||||
|
||||
// Now it shouldn't match, so should return the original num
|
||||
assertEquals(
|
||||
targetNodeNum,
|
||||
repository.effectiveLogNodeId(targetNodeNum).filter { it == targetNodeNum }.first(),
|
||||
)
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import kotlin.test.BeforeTest
|
||||
|
||||
class MeshLogRepositoryTest : CommonMeshLogRepositoryTest() {
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
setupRepo()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import kotlin.test.BeforeTest
|
||||
|
||||
class NodeRepositoryTest : CommonNodeRepositoryTest() {
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
setupRepo()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.data.repository
|
||||
|
||||
import kotlin.test.BeforeTest
|
||||
|
||||
class PacketRepositoryTest : CommonPacketRepositoryTest() {
|
||||
@BeforeTest
|
||||
fun setup() {
|
||||
setupRepo()
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue