refactor: coroutine dispatchers and modernize testing infrastructure (#4901)
Some checks are pending
Dependency Submission / dependency-submission (push) Waiting to run
Main CI (Verify & Build) / validate-and-build (push) Waiting to run
Main Push Changelog / Generate main push changelog (push) Waiting to run

Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
James Rich 2026-03-23 20:31:48 -05:00 committed by GitHub
parent 664ebf218e
commit 96060a0a4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 621 additions and 182 deletions

View file

@ -23,7 +23,6 @@ import co.touchlab.kermit.Logger
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
@ -220,12 +219,13 @@ class DebugViewModel(
private val nodeRepository: NodeRepository,
private val meshLogPrefs: MeshLogPrefs,
private val alertManager: AlertManager,
private val dispatchers: org.meshtastic.core.di.CoroutineDispatchers,
) : ViewModel() {
val meshLog: StateFlow<ImmutableList<UiMeshLog>> =
meshLogRepository
.getAllLogs()
.mapLatest { logs -> withContext(Dispatchers.Default) { toUiState(logs) } }
.mapLatest { logs -> withContext(dispatchers.default) { toUiState(logs) } }
.stateInWhileSubscribed(initialValue = persistentListOf())
private val _retentionDays = MutableStateFlow(meshLogPrefs.retentionDays.value)

View file

@ -16,27 +16,44 @@
*/
package org.meshtastic.feature.settings.debugging
import dev.mokkery.MockMode
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verify
import io.kotest.matchers.shouldBe
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.testing.FakeMeshLogPrefs
import org.meshtastic.core.testing.FakeMeshLogRepository
import org.meshtastic.core.testing.FakeNodeRepository
import org.meshtastic.core.ui.util.AlertManager
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
@OptIn(ExperimentalCoroutinesApi::class)
class DebugViewModelTest {
/*
private val meshLogRepository = FakeMeshLogRepository()
private val nodeRepository = FakeNodeRepository()
private val meshLogPrefs = FakeMeshLogPrefs()
private val alertManager: AlertManager = mock(MockMode.autofill)
private val testDispatcher = UnconfinedTestDispatcher()
private val dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher)
private lateinit var viewModel: DebugViewModel
@Before
@BeforeTest
fun setUp() {
Dispatchers.setMain(testDispatcher)
every { meshLogRepository.getAllLogs() } returns flowOf(emptyList())
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap())
every { meshLogPrefs.retentionDays.value } returns 7
every { meshLogPrefs.loggingEnabled.value } returns true
meshLogPrefs.setRetentionDays(7)
meshLogPrefs.setLoggingEnabled(true)
viewModel =
DebugViewModel(
@ -44,10 +61,11 @@ class DebugViewModelTest {
nodeRepository = nodeRepository,
meshLogPrefs = meshLogPrefs,
alertManager = alertManager,
dispatchers = dispatchers,
)
}
@After
@AfterTest
fun tearDown() {
Dispatchers.resetMain()
}
@ -56,17 +74,18 @@ class DebugViewModelTest {
fun `setRetentionDays updates prefs and deletes old logs`() = runTest {
viewModel.setRetentionDays(14)
verify { meshLogPrefs.setRetentionDays(14) }
verifySuspend { meshLogRepository.deleteLogsOlderThan(14) }
meshLogPrefs.retentionDays.value shouldBe 14
meshLogRepository.deleteLogsOlderThanCalledDays shouldBe 14
viewModel.retentionDays.value shouldBe 14
}
@Test
fun `setLoggingEnabled false deletes all logs`() = runTest {
meshLogRepository.insert(org.meshtastic.core.model.MeshLog("123", "type", 1L, "raw"))
viewModel.setLoggingEnabled(false)
verify { meshLogPrefs.setLoggingEnabled(false) }
verifySuspend { meshLogRepository.deleteAll() }
meshLogPrefs.loggingEnabled.value shouldBe false
meshLogRepository.currentLogs shouldBe emptyList()
viewModel.loggingEnabled.value shouldBe false
}
@ -91,6 +110,4 @@ class DebugViewModelTest {
viewModel.requestDeleteAllLogs()
verify { alertManager.showAlert(titleRes = any(), messageRes = any(), onConfirm = any()) }
}
*/
}

View file

@ -53,11 +53,11 @@ import org.meshtastic.core.repository.HomoglyphPrefs
import org.meshtastic.core.repository.LocationRepository
import org.meshtastic.core.repository.LocationService
import org.meshtastic.core.repository.MapConsentPrefs
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.PacketRepository
import org.meshtastic.core.repository.RadioConfigRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.UiPrefs
import org.meshtastic.core.testing.FakeNodeRepository
import org.meshtastic.feature.settings.navigation.ConfigRoute
import org.meshtastic.proto.ChannelSet
import org.meshtastic.proto.ChannelSettings
@ -82,7 +82,7 @@ class RadioConfigViewModelTest {
private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill)
private val packetRepository: PacketRepository = mock(MockMode.autofill)
private val serviceRepository: ServiceRepository = mock(MockMode.autofill)
private val nodeRepository: NodeRepository = mock(MockMode.autofill)
private val nodeRepository = FakeNodeRepository()
private val locationRepository: LocationRepository = mock(MockMode.autofill)
private val mapConsentPrefs: MapConsentPrefs = mock(MockMode.autofill)
private val analyticsPrefs: AnalyticsPrefs = mock(MockMode.autofill)
@ -107,8 +107,6 @@ class RadioConfigViewModelTest {
fun setUp() {
Dispatchers.setMain(testDispatcher)
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap())
every { nodeRepository.myNodeInfo } returns MutableStateFlow(null)
every { radioConfigRepository.deviceProfileFlow } returns MutableStateFlow(DeviceProfile())
every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig())
every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet())
@ -153,7 +151,7 @@ class RadioConfigViewModelTest {
@Test
fun `setConfig calls useCase`() = runTest {
val node = Node(num = 123, user = User(id = "!123"))
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node))
nodeRepository.setNodes(listOf(node))
viewModel = createViewModel()
val config = Config(device = Config.DeviceConfig(role = Config.DeviceConfig.Role.ROUTER))
@ -191,7 +189,7 @@ class RadioConfigViewModelTest {
@Test
fun `processPacketResponse updates state on metadata result`() = runTest {
val node = Node(num = 123, user = User(id = "!123"))
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node))
nodeRepository.setNodes(listOf(node))
val packet = MeshPacket()
val metadata = DeviceMetadata(firmware_version = "3.0.0")
@ -214,7 +212,7 @@ class RadioConfigViewModelTest {
@Test
fun `updateChannels calls useCase for each changed channel`() = runTest {
val node = Node(num = 123, user = User(id = "!123"))
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node))
nodeRepository.setNodes(listOf(node))
viewModel = createViewModel()
val old = listOf(ChannelSettings(name = "Old"))
@ -231,7 +229,7 @@ class RadioConfigViewModelTest {
@Test
fun `setResponseStateLoading for REBOOT calls useCase after packet response`() = runTest {
val node = Node(num = 123, user = User(id = "!123"))
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node))
nodeRepository.setNodes(listOf(node))
val packetFlow = MutableSharedFlow<MeshPacket>()
every { serviceRepository.meshPacketFlow } returns packetFlow
@ -252,7 +250,7 @@ class RadioConfigViewModelTest {
@Test
fun `setResponseStateLoading for FACTORY_RESET calls useCase after packet response`() = runTest {
val node = Node(num = 123, user = User(id = "!123"))
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node))
nodeRepository.setNodes(listOf(node))
val packetFlow = MutableSharedFlow<MeshPacket>()
every { serviceRepository.meshPacketFlow } returns packetFlow
@ -283,7 +281,7 @@ class RadioConfigViewModelTest {
@Test
fun `setOwner calls useCase`() = runTest {
val node = Node(num = 123, user = User(id = "!123"))
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node))
nodeRepository.setNodes(listOf(node))
viewModel = createViewModel()
val user = User(long_name = "Test User")
@ -297,7 +295,7 @@ class RadioConfigViewModelTest {
@Test
fun `setRingtone calls useCase`() = runTest {
val node = Node(num = 123, user = User(id = "!123"))
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node))
nodeRepository.setNodes(listOf(node))
viewModel = createViewModel()
everySuspend { radioConfigUseCase.setRingtone(any(), any()) } returns Unit
@ -311,7 +309,7 @@ class RadioConfigViewModelTest {
@Test
fun `setCannedMessages calls useCase`() = runTest {
val node = Node(num = 123, user = User(id = "!123"))
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node))
nodeRepository.setNodes(listOf(node))
viewModel = createViewModel()
everySuspend { radioConfigUseCase.setCannedMessages(any(), any()) } returns Unit
@ -341,7 +339,7 @@ class RadioConfigViewModelTest {
@Test
fun `registerRequestId timeout clears request and sets error`() = runTest {
val node = Node(num = 123, user = User(id = "!123"))
every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(mapOf(123 to node))
nodeRepository.setNodes(listOf(node))
viewModel = createViewModel()
everySuspend { radioConfigUseCase.getOwner(any()) } returns 42