test(discovery): add DAO, packet collection, history, and deep-link tests (D010, D023, D042)

This commit is contained in:
James Rich
2026-05-07 19:32:38 -05:00
parent 92bf9a6a31
commit cffafb175d
5 changed files with 1020 additions and 3 deletions

View File

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