mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-05-19 12:18:43 -04:00
test(discovery): add DAO, packet collection, history, and deep-link tests (D010, D023, D042)
This commit is contained in:
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
@file:Suppress("MagicNumber")
|
||||
|
||||
package org.meshtastic.core.database.dao
|
||||
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.database.MeshtasticDatabase
|
||||
import org.meshtastic.core.database.entity.DiscoveredNodeEntity
|
||||
import org.meshtastic.core.database.entity.DiscoveryPresetResultEntity
|
||||
import org.meshtastic.core.database.entity.DiscoverySessionEntity
|
||||
import org.meshtastic.core.database.getInMemoryDatabaseBuilder
|
||||
import kotlin.test.AfterTest
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNotNull
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
abstract class CommonDiscoveryDaoTest {
|
||||
private lateinit var database: MeshtasticDatabase
|
||||
private lateinit var dao: DiscoveryDao
|
||||
|
||||
suspend fun createDb() {
|
||||
database = getInMemoryDatabaseBuilder().build()
|
||||
dao = database.discoveryDao()
|
||||
}
|
||||
|
||||
@AfterTest
|
||||
fun closeDb() {
|
||||
database.close()
|
||||
}
|
||||
|
||||
// region Session CRUD
|
||||
|
||||
@Test
|
||||
fun insertSession_returnsAutoGeneratedId() = runTest {
|
||||
val session = testSession(timestamp = 1_000_000L)
|
||||
val id = dao.insertSession(session)
|
||||
assertTrue(id > 0, "Auto-generated id should be > 0")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getSession_returnsInsertedSession() = runTest {
|
||||
val id = dao.insertSession(testSession(timestamp = 2_000_000L, homePreset = "MEDIUM_SLOW"))
|
||||
val loaded = dao.getSession(id)
|
||||
assertNotNull(loaded)
|
||||
assertEquals(id, loaded.id)
|
||||
assertEquals("MEDIUM_SLOW", loaded.homePreset)
|
||||
assertEquals(2_000_000L, loaded.timestamp)
|
||||
}
|
||||
|
||||
@Test fun getSession_returnsNullForMissing() = runTest { assertNull(dao.getSession(999L)) }
|
||||
|
||||
@Test
|
||||
fun updateSession_modifiesExistingRow() = runTest {
|
||||
val id = dao.insertSession(testSession(timestamp = 3_000_000L))
|
||||
val original = dao.getSession(id)!!
|
||||
dao.updateSession(original.copy(completionStatus = "stopped", totalUniqueNodes = 5))
|
||||
val updated = dao.getSession(id)!!
|
||||
assertEquals("stopped", updated.completionStatus)
|
||||
assertEquals(5, updated.totalUniqueNodes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSession_removesRow() = runTest {
|
||||
val id = dao.insertSession(testSession())
|
||||
dao.deleteSession(id)
|
||||
assertNull(dao.getSession(id))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Session sort order (getAllSessions returns newest-first)
|
||||
|
||||
@Test
|
||||
fun getAllSessions_orderedByTimestampDescending() = runTest {
|
||||
dao.insertSession(testSession(timestamp = 100L))
|
||||
dao.insertSession(testSession(timestamp = 300L))
|
||||
dao.insertSession(testSession(timestamp = 200L))
|
||||
val sessions = dao.getAllSessions().first()
|
||||
assertEquals(3, sessions.size)
|
||||
assertEquals(300L, sessions[0].timestamp)
|
||||
assertEquals(200L, sessions[1].timestamp)
|
||||
assertEquals(100L, sessions[2].timestamp)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Preset result relation loading
|
||||
|
||||
@Test
|
||||
fun getPresetResults_returnsResultsForSession() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
dao.insertPresetResult(testPresetResult(sessionId, presetName = "LONG_FAST"))
|
||||
dao.insertPresetResult(testPresetResult(sessionId, presetName = "SHORT_FAST"))
|
||||
val results = dao.getPresetResults(sessionId)
|
||||
assertEquals(2, results.size)
|
||||
assertTrue(results.any { it.presetName == "LONG_FAST" })
|
||||
assertTrue(results.any { it.presetName == "SHORT_FAST" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getPresetResults_doesNotReturnOtherSessionResults() = runTest {
|
||||
val session1 = dao.insertSession(testSession(timestamp = 1L))
|
||||
val session2 = dao.insertSession(testSession(timestamp = 2L))
|
||||
dao.insertPresetResult(testPresetResult(session1, presetName = "A"))
|
||||
dao.insertPresetResult(testPresetResult(session2, presetName = "B"))
|
||||
val results = dao.getPresetResults(session1)
|
||||
assertEquals(1, results.size)
|
||||
assertEquals("A", results[0].presetName)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getPresetResultsFlow_emitsOnInsert() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
val initial = dao.getPresetResultsFlow(sessionId).first()
|
||||
assertTrue(initial.isEmpty())
|
||||
dao.insertPresetResult(testPresetResult(sessionId, presetName = "LONG_FAST"))
|
||||
val updated = dao.getPresetResultsFlow(sessionId).first()
|
||||
assertEquals(1, updated.size)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Discovered node relation loading
|
||||
|
||||
@Test
|
||||
fun getDiscoveredNodes_returnsNodesForPresetResult() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
val presetId = dao.insertPresetResult(testPresetResult(sessionId))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 100))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 200))
|
||||
val nodes = dao.getDiscoveredNodes(presetId)
|
||||
assertEquals(2, nodes.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun insertDiscoveredNodes_batchInsert() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
val presetId = dao.insertPresetResult(testPresetResult(sessionId))
|
||||
val batch =
|
||||
listOf(testNode(presetId, nodeNum = 1), testNode(presetId, nodeNum = 2), testNode(presetId, nodeNum = 3))
|
||||
dao.insertDiscoveredNodes(batch)
|
||||
assertEquals(3, dao.getDiscoveredNodes(presetId).size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun updateDiscoveredNode_modifiesExistingRow() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
val presetId = dao.insertPresetResult(testPresetResult(sessionId))
|
||||
val nodeId = dao.insertDiscoveredNode(testNode(presetId, nodeNum = 42))
|
||||
val original = dao.getDiscoveredNodes(presetId).first { it.id == nodeId }
|
||||
dao.updateDiscoveredNode(original.copy(snr = 12.5f, rssi = -55))
|
||||
val updated = dao.getDiscoveredNodes(presetId).first { it.id == nodeId }
|
||||
assertEquals(12.5f, updated.snr)
|
||||
assertEquals(-55, updated.rssi)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Cascade deletion
|
||||
|
||||
@Test
|
||||
fun deleteSession_cascadesPresetResults() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
dao.insertPresetResult(testPresetResult(sessionId, presetName = "LONG_FAST"))
|
||||
dao.insertPresetResult(testPresetResult(sessionId, presetName = "SHORT_FAST"))
|
||||
dao.deleteSession(sessionId)
|
||||
assertTrue(dao.getPresetResults(sessionId).isEmpty(), "Preset results should be cascade-deleted")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSession_cascadesDiscoveredNodes() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
val presetId = dao.insertPresetResult(testPresetResult(sessionId))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 1))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 2))
|
||||
dao.deleteSession(sessionId)
|
||||
assertTrue(dao.getDiscoveredNodes(presetId).isEmpty(), "Discovered nodes should be cascade-deleted")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun deleteSession_doesNotAffectOtherSessions() = runTest {
|
||||
val session1 = dao.insertSession(testSession(timestamp = 1L))
|
||||
val session2 = dao.insertSession(testSession(timestamp = 2L))
|
||||
val preset1 = dao.insertPresetResult(testPresetResult(session1))
|
||||
val preset2 = dao.insertPresetResult(testPresetResult(session2))
|
||||
dao.insertDiscoveredNode(testNode(preset1, nodeNum = 1))
|
||||
dao.insertDiscoveredNode(testNode(preset2, nodeNum = 2))
|
||||
dao.deleteSession(session1)
|
||||
assertNotNull(dao.getSession(session2))
|
||||
assertEquals(1, dao.getPresetResults(session2).size)
|
||||
assertEquals(1, dao.getDiscoveredNodes(preset2).size)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Aggregate queries
|
||||
|
||||
@Test
|
||||
fun getUniqueNodeCount_countsAcrossPresets() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
val preset1 = dao.insertPresetResult(testPresetResult(sessionId, presetName = "A"))
|
||||
val preset2 = dao.insertPresetResult(testPresetResult(sessionId, presetName = "B"))
|
||||
// Same node 100 appears in both presets
|
||||
dao.insertDiscoveredNode(testNode(preset1, nodeNum = 100))
|
||||
dao.insertDiscoveredNode(testNode(preset1, nodeNum = 200))
|
||||
dao.insertDiscoveredNode(testNode(preset2, nodeNum = 100))
|
||||
dao.insertDiscoveredNode(testNode(preset2, nodeNum = 300))
|
||||
assertEquals(3, dao.getUniqueNodeCount(sessionId), "Node 100 appears in both presets but should count once")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getUniqueNodeNums_returnsDistinctNodeNums() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
val presetId = dao.insertPresetResult(testPresetResult(sessionId))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 10))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 20))
|
||||
val nums = dao.getUniqueNodeNums(sessionId)
|
||||
assertEquals(setOf(10L, 20L), nums.toSet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getMaxDistance_returnsLargestDistance() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
val presetId = dao.insertPresetResult(testPresetResult(sessionId))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 1, distanceFromUser = 500.0))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 2, distanceFromUser = 15_000.0))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 3, distanceFromUser = 3_000.0))
|
||||
assertEquals(15_000.0, dao.getMaxDistance(sessionId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getMaxDistance_returnsNullWhenNoNodes() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
assertNull(dao.getMaxDistance(sessionId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun getMaxDistance_returnsNullWhenAllDistancesNull() = runTest {
|
||||
val sessionId = dao.insertSession(testSession())
|
||||
val presetId = dao.insertPresetResult(testPresetResult(sessionId))
|
||||
dao.insertDiscoveredNode(testNode(presetId, nodeNum = 1, distanceFromUser = null))
|
||||
assertNull(dao.getMaxDistance(sessionId))
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Flow queries
|
||||
|
||||
@Test
|
||||
fun getSessionFlow_emitsUpdatesOnChange() = runTest {
|
||||
val id = dao.insertSession(testSession(timestamp = 5_000_000L))
|
||||
val initial = dao.getSessionFlow(id).first()
|
||||
assertNotNull(initial)
|
||||
assertEquals("in_progress", initial.completionStatus)
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
||||
// region Helpers
|
||||
|
||||
private fun testSession(timestamp: Long = 1_000_000L, homePreset: String = "LONG_FAST") = DiscoverySessionEntity(
|
||||
timestamp = timestamp,
|
||||
presetsScanned = "LONG_FAST,SHORT_FAST",
|
||||
homePreset = homePreset,
|
||||
completionStatus = "in_progress",
|
||||
)
|
||||
|
||||
private fun testPresetResult(sessionId: Long, presetName: String = "LONG_FAST") = DiscoveryPresetResultEntity(
|
||||
sessionId = sessionId,
|
||||
presetName = presetName,
|
||||
dwellDurationSeconds = 30,
|
||||
uniqueNodes = 5,
|
||||
)
|
||||
|
||||
private fun testNode(presetResultId: Long, nodeNum: Long, distanceFromUser: Double? = null) = DiscoveredNodeEntity(
|
||||
presetResultId = presetResultId,
|
||||
nodeNum = nodeNum,
|
||||
snr = 5.0f,
|
||||
rssi = -70,
|
||||
distanceFromUser = distanceFromUser,
|
||||
)
|
||||
|
||||
// endregion
|
||||
}
|
||||
Reference in New Issue
Block a user