diff --git a/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt b/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt index d25619d70..1c0b0a467 100644 --- a/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt +++ b/app/src/main/kotlin/org/meshtastic/app/di/AppKoinModule.kt @@ -58,6 +58,7 @@ import org.meshtastic.feature.widget.di.FeatureWidgetModule includes = [ org.meshtastic.app.MainKoinModule::class, + org.meshtastic.core.di.di.CoreDiModule::class, CoreCommonModule::class, CoreBleModule::class, CoreBleAndroidModule::class, @@ -93,14 +94,6 @@ class AppKoinModule { @Named("ProcessLifecycle") fun provideProcessLifecycle(): Lifecycle = ProcessLifecycleOwner.get().lifecycle - @Single - fun provideCoroutineDispatchers(): org.meshtastic.core.di.CoroutineDispatchers = - org.meshtastic.core.di.CoroutineDispatchers( - io = kotlinx.coroutines.Dispatchers.IO, - main = kotlinx.coroutines.Dispatchers.Main, - default = kotlinx.coroutines.Dispatchers.Default, - ) - @Single fun provideBuildConfigProvider(): BuildConfigProvider = object : BuildConfigProvider { override val isDebug: Boolean = org.meshtastic.app.BuildConfig.DEBUG diff --git a/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt b/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt index d1cc71174..8f262c47c 100644 --- a/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt +++ b/app/src/test/kotlin/org/meshtastic/app/service/Fakes.kt @@ -34,8 +34,10 @@ class FakeMeshServiceNotifications : MeshServiceNotifications { override fun initChannels() {} - override fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Notification = - mock(MockMode.autofill) + override fun updateServiceStateNotification( + state: org.meshtastic.core.model.ConnectionState, + telemetry: Telemetry?, + ): Notification = mock(MockMode.autofill) override suspend fun updateMessageNotification( contactKey: String, diff --git a/core/data/build.gradle.kts b/core/data/build.gradle.kts index b6198f99c..6713208c0 100644 --- a/core/data/build.gradle.kts +++ b/core/data/build.gradle.kts @@ -67,6 +67,7 @@ kotlin { } commonTest.dependencies { + implementation(projects.core.testing) implementation(kotlin("test")) implementation(libs.kotlinx.coroutines.test) implementation(libs.kotest.assertions) diff --git a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt index 12e3a6631..c81469aaa 100644 --- a/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt +++ b/core/data/src/commonMain/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImpl.kt @@ -54,13 +54,6 @@ import org.meshtastic.core.repository.RadioInterfaceService import org.meshtastic.core.repository.ServiceBroadcasts import org.meshtastic.core.repository.ServiceRepository import org.meshtastic.core.repository.UiPrefs -import org.meshtastic.core.resources.Res -import org.meshtastic.core.resources.connected -import org.meshtastic.core.resources.connecting -import org.meshtastic.core.resources.device_sleeping -import org.meshtastic.core.resources.disconnected -import org.meshtastic.core.resources.getString -import org.meshtastic.core.resources.meshtastic_app_name import org.meshtastic.proto.AdminMessage import org.meshtastic.proto.Config import org.meshtastic.proto.Telemetry @@ -326,17 +319,11 @@ class MeshConnectionManagerImpl( updateStatusNotification(t) } - override fun updateStatusNotification(telemetry: Telemetry?): Any { - val summary = - when (serviceRepository.connectionState.value) { - is ConnectionState.Connected -> - getString(Res.string.meshtastic_app_name) + ": " + getString(Res.string.connected) - is ConnectionState.Disconnected -> getString(Res.string.disconnected) - is ConnectionState.DeviceSleep -> getString(Res.string.device_sleeping) - is ConnectionState.Connecting -> getString(Res.string.connecting) - } - return serviceNotifications.updateServiceStateNotification(summary, telemetry = telemetry) - } + override fun updateStatusNotification(telemetry: Telemetry?): Any = + serviceNotifications.updateServiceStateNotification( + serviceRepository.connectionState.value, + telemetry = telemetry, + ) companion object { private const val DEVICE_SLEEP_TIMEOUT_SECONDS = 30 diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt index 73f710bc8..9b0b50490 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt @@ -16,10 +16,70 @@ */ package org.meshtastic.core.data.manager +import dev.mokkery.MockMode +import dev.mokkery.answering.calls +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.everySuspend +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.runTest +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.DataPacket +import org.meshtastic.core.model.Node +import org.meshtastic.core.repository.AppWidgetUpdater +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.HistoryManager +import org.meshtastic.core.repository.MeshLocationManager +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.MeshWorkerManager +import org.meshtastic.core.repository.MqttManager +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.PacketHandler +import org.meshtastic.core.repository.PacketRepository +import org.meshtastic.core.repository.PlatformAnalytics +import org.meshtastic.core.repository.RadioConfigRepository +import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.ServiceBroadcasts +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.core.testing.FakeNodeRepository +import org.meshtastic.proto.Config +import org.meshtastic.proto.LocalConfig +import org.meshtastic.proto.LocalModuleConfig +import org.meshtastic.proto.ModuleConfig +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals + +@OptIn(ExperimentalCoroutinesApi::class) class MeshConnectionManagerImplTest { - /* - + private val radioInterfaceService = mock(MockMode.autofill) + private val serviceRepository = mock(MockMode.autofill) + private val serviceBroadcasts = mock(MockMode.autofill) + private val serviceNotifications = mock(MockMode.autofill) + private val uiPrefs = mock(MockMode.autofill) + private val packetHandler = mock(MockMode.autofill) + private val nodeRepository = FakeNodeRepository() + private val locationManager = mock(MockMode.autofill) + private val mqttManager = mock(MockMode.autofill) + private val historyManager = mock(MockMode.autofill) + private val radioConfigRepository = mock(MockMode.autofill) + private val commandSender = mock(MockMode.autofill) + private val nodeManager = mock(MockMode.autofill) + private val analytics = mock(MockMode.autofill) + private val packetRepository = mock(MockMode.autofill) + private val workerManager = mock(MockMode.autofill) + private val appWidgetUpdater = mock(MockMode.autofill) + private val dataPacket = DataPacket(id = 456, time = 0L, to = "0", from = "0", bytes = null, dataType = 0) private val radioConnectionState = MutableStateFlow(ConnectionState.Disconnected) private val connectionStateFlow = MutableStateFlow(ConnectionState.Disconnected) @@ -30,17 +90,23 @@ class MeshConnectionManagerImplTest { private lateinit var manager: MeshConnectionManagerImpl - @Before + @BeforeTest fun setUp() { - mockkStatic("org.meshtastic.core.resources.GetStringKt") - every { radioInterfaceService.connectionState } returns radioConnectionState every { radioConfigRepository.localConfigFlow } returns localConfigFlow every { radioConfigRepository.moduleConfigFlow } returns moduleConfigFlow - every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) - every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) - every { nodeRepository.localStats } returns MutableStateFlow(LocalStats()) every { serviceRepository.connectionState } returns connectionStateFlow + every { serviceRepository.setConnectionState(any()) } calls + { call -> + connectionStateFlow.value = call.arg(0) + } + every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit + every { commandSender.sendAdmin(any(), any(), any(), any()) } returns Unit + every { packetHandler.stopPacketQueue() } returns Unit + every { locationManager.stop() } returns Unit + every { mqttManager.stop() } returns Unit + every { nodeManager.nodeDBbyNodeNum } returns emptyMap() + every { packetHandler.sendToRadio(any()) } returns Unit manager = MeshConnectionManagerImpl( @@ -64,27 +130,38 @@ class MeshConnectionManagerImplTest { ) } - @After - fun tearDown() { - unmockkStatic("org.meshtastic.core.resources.GetStringKt") - } + @AfterTest fun tearDown() {} @Test fun `Connected state triggers broadcast and config start`() = runTest(testDispatcher) { + every { packetHandler.sendToRadio(any()) } returns Unit + every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit + manager.start(backgroundScope) radioConnectionState.value = ConnectionState.Connected advanceUntilIdle() assertEquals( - "State should be Connecting after radio Connected", ConnectionState.Connecting, serviceRepository.connectionState.value, + "State should be Connecting after radio Connected", ) verify { serviceBroadcasts.broadcastConnection() } } @Test fun `Disconnected state stops services`() = runTest(testDispatcher) { + every { packetHandler.sendToRadio(any()) } returns Unit + every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit + every { packetHandler.stopPacketQueue() } returns Unit + every { locationManager.stop() } returns Unit + every { mqttManager.stop() } returns Unit + every { packetHandler.sendToRadio(any()) } returns Unit + every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit + every { packetHandler.stopPacketQueue() } returns Unit + every { locationManager.stop() } returns Unit + every { mqttManager.stop() } returns Unit + every { nodeManager.nodeDBbyNodeNum } returns emptyMap() manager.start(backgroundScope) // Transition to Connected first so that Disconnected actually does something radioConnectionState.value = ConnectionState.Connected @@ -94,9 +171,9 @@ class MeshConnectionManagerImplTest { advanceUntilIdle() assertEquals( - "State should be Disconnected after radio Disconnected", ConnectionState.Disconnected, serviceRepository.connectionState.value, + "State should be Disconnected after radio Disconnected", ) verify { packetHandler.stopPacketQueue() } verify { locationManager.stop() } @@ -112,6 +189,12 @@ class MeshConnectionManagerImplTest { device = Config.DeviceConfig(role = Config.DeviceConfig.Role.CLIENT), ) every { radioConfigRepository.localConfigFlow } returns flowOf(config) + every { packetHandler.sendToRadio(any()) } returns Unit + every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit + every { packetHandler.stopPacketQueue() } returns Unit + every { locationManager.stop() } returns Unit + every { mqttManager.stop() } returns Unit + every { nodeManager.nodeDBbyNodeNum } returns emptyMap() manager.start(backgroundScope) advanceUntilIdle() @@ -120,9 +203,9 @@ class MeshConnectionManagerImplTest { advanceUntilIdle() assertEquals( - "State should be Disconnected when power saving is off", ConnectionState.Disconnected, serviceRepository.connectionState.value, + "State should be Disconnected when power saving is off", ) } @@ -131,6 +214,11 @@ class MeshConnectionManagerImplTest { // Power saving enabled val config = LocalConfig(power = Config.PowerConfig(is_power_saving = true)) every { radioConfigRepository.localConfigFlow } returns flowOf(config) + every { packetHandler.sendToRadio(any()) } returns Unit + every { serviceNotifications.updateServiceStateNotification(any(), any()) } returns Unit + every { packetHandler.stopPacketQueue() } returns Unit + every { locationManager.stop() } returns Unit + every { mqttManager.stop() } returns Unit manager.start(backgroundScope) advanceUntilIdle() @@ -139,9 +227,9 @@ class MeshConnectionManagerImplTest { advanceUntilIdle() assertEquals( - "State should stay in DeviceSleep when power saving is on", ConnectionState.DeviceSleep, serviceRepository.connectionState.value, + "State should stay in DeviceSleep when power saving is on", ) } @@ -149,8 +237,8 @@ class MeshConnectionManagerImplTest { fun `onRadioConfigLoaded enqueues queued packets and sets time`() = runTest(testDispatcher) { manager.start(backgroundScope) val packetId = 456 - every { dataPacket.id } returns packetId everySuspend { packetRepository.getQueuedPackets() } returns listOf(dataPacket) + every { workerManager.enqueueSendMessage(any()) } returns Unit manager.onRadioConfigLoaded() advanceUntilIdle() @@ -160,15 +248,23 @@ class MeshConnectionManagerImplTest { @Test fun `onNodeDbReady starts MQTT and requests history`() = runTest(testDispatcher) { - every { moduleConfig.mqtt } returns ModuleConfig.MQTTConfig(enabled = true) - every { moduleConfig.store_forward } returns ModuleConfig.StoreForwardConfig(enabled = true) + val moduleConfig = + LocalModuleConfig( + mqtt = ModuleConfig.MQTTConfig(enabled = true, proxy_to_client_enabled = true), + store_forward = ModuleConfig.StoreForwardConfig(enabled = true), + ) moduleConfigFlow.value = moduleConfig + every { commandSender.requestTelemetry(any(), any(), any()) } returns Unit + every { nodeManager.myNodeNum } returns 123 + every { mqttManager.start(any(), any(), any()) } returns Unit + every { historyManager.requestHistoryReplay(any(), any(), any(), any()) } returns Unit + every { nodeManager.getMyNodeInfo() } returns null manager.start(backgroundScope) manager.onNodeDbReady() advanceUntilIdle() + verify { mqttManager.start(any(), true, true) } + verify { historyManager.requestHistoryReplay(any(), any(), any(), any()) } } - - */ } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt index 46307675a..a429b90ae 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt @@ -24,7 +24,6 @@ import io.github.davidepianca98.mqtt.packets.Qos import io.github.davidepianca98.mqtt.packets.mqttv5.SubscriptionOptions import io.github.davidepianca98.socket.tls.TLSClientSettings import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.channels.awaitClose @@ -47,6 +46,7 @@ import org.meshtastic.proto.MqttClientProxyMessage class MQTTRepositoryImpl( private val radioConfigRepository: RadioConfigRepository, private val nodeRepository: NodeRepository, + dispatchers: org.meshtastic.core.di.CoroutineDispatchers, ) : MQTTRepository { companion object { @@ -58,7 +58,7 @@ class MQTTRepositoryImpl( private var client: MQTTClient? = null private val json = Json { ignoreUnknownKeys = true } - private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + private val scope = CoroutineScope(dispatchers.default + SupervisorJob()) private var clientJob: Job? = null private val publishSemaphore = Semaphore(20) diff --git a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt index a4fefe2cd..195a241ee 100644 --- a/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt +++ b/core/repository/src/commonMain/kotlin/org/meshtastic/core/repository/MeshServiceNotifications.kt @@ -28,7 +28,7 @@ interface MeshServiceNotifications { fun initChannels() - fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Any + fun updateServiceStateNotification(state: org.meshtastic.core.model.ConnectionState, telemetry: Telemetry?): Any suspend fun updateMessageNotification( contactKey: String, diff --git a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt index d130c172b..e5468eb66 100644 --- a/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt +++ b/core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshServiceNotificationsImpl.kt @@ -53,6 +53,10 @@ import org.meshtastic.core.repository.SERVICE_NOTIFY_ID import org.meshtastic.core.resources.R.raw import org.meshtastic.core.resources.Res import org.meshtastic.core.resources.client_notification +import org.meshtastic.core.resources.connected +import org.meshtastic.core.resources.connecting +import org.meshtastic.core.resources.device_sleeping +import org.meshtastic.core.resources.disconnected import org.meshtastic.core.resources.getString import org.meshtastic.core.resources.local_stats_bad import org.meshtastic.core.resources.local_stats_battery @@ -98,7 +102,7 @@ import kotlin.time.Duration.Companion.minutes * This class centralizes notification logic, including channel creation, builder configuration, and displaying * notifications for various events like new messages, alerts, and service status changes. */ -@Suppress("TooManyFunctions", "LongParameterList") +@Suppress("TooManyFunctions", "LongParameterList", "LargeClass") @Single class MeshServiceNotificationsImpl( private val context: Context, @@ -287,7 +291,19 @@ class MeshServiceNotificationsImpl( // region Public Notification Methods @Suppress("CyclomaticComplexMethod", "NestedBlockDepth") - override fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Notification { + override fun updateServiceStateNotification( + state: org.meshtastic.core.model.ConnectionState, + telemetry: Telemetry?, + ): Notification { + val summaryString = + when (state) { + is org.meshtastic.core.model.ConnectionState.Connected -> + getString(Res.string.meshtastic_app_name) + ": " + getString(Res.string.connected) + is org.meshtastic.core.model.ConnectionState.Disconnected -> getString(Res.string.disconnected) + is org.meshtastic.core.model.ConnectionState.DeviceSleep -> getString(Res.string.device_sleeping) + is org.meshtastic.core.model.ConnectionState.Connecting -> getString(Res.string.connecting) + } + // Update caches if telemetry is provided telemetry?.let { t -> t.local_stats?.let { stats -> diff --git a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt index 0faf332a8..e89da1f58 100644 --- a/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt +++ b/core/service/src/commonMain/kotlin/org/meshtastic/core/service/MeshServiceOrchestrator.kt @@ -18,7 +18,6 @@ package org.meshtastic.core.service import co.touchlab.kermit.Logger import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -54,6 +53,7 @@ class MeshServiceOrchestrator( private val connectionManager: MeshConnectionManager, private val router: MeshRouter, private val serviceNotifications: MeshServiceNotifications, + private val dispatchers: org.meshtastic.core.di.CoroutineDispatchers, ) { private var serviceJob: Job? = null @@ -80,7 +80,7 @@ class MeshServiceOrchestrator( Logger.i { "Starting mesh service orchestrator" } val job = Job() serviceJob = job - val scope = CoroutineScope(Dispatchers.Default + job) + val scope = CoroutineScope(dispatchers.default + job) serviceScope = scope serviceNotifications.initChannels() diff --git a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt index 28cbadcaf..8eae17eb8 100644 --- a/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt +++ b/core/service/src/commonTest/kotlin/org/meshtastic/core/service/MeshServiceOrchestratorTest.kt @@ -16,36 +16,60 @@ */ package org.meshtastic.core.service -class MeshServiceOrchestratorTest { - /* +import dev.mokkery.MockMode +import dev.mokkery.answering.returns +import dev.mokkery.every +import dev.mokkery.matcher.any +import dev.mokkery.mock +import dev.mokkery.verify +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import org.meshtastic.core.di.CoroutineDispatchers +import org.meshtastic.core.repository.CommandSender +import org.meshtastic.core.repository.MeshConnectionManager +import org.meshtastic.core.repository.MeshMessageProcessor +import org.meshtastic.core.repository.MeshRouter +import org.meshtastic.core.repository.MeshServiceNotifications +import org.meshtastic.core.repository.NodeManager +import org.meshtastic.core.repository.PacketHandler +import org.meshtastic.core.repository.RadioInterfaceService +import org.meshtastic.core.repository.ServiceRepository +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue +class MeshServiceOrchestratorTest { + + private val radioInterfaceService: RadioInterfaceService = mock(MockMode.autofill) + private val serviceRepository: ServiceRepository = mock(MockMode.autofill) + private val packetHandler: PacketHandler = mock(MockMode.autofill) + private val nodeManager: NodeManager = mock(MockMode.autofill) + private val messageProcessor: MeshMessageProcessor = mock(MockMode.autofill) + private val commandSender: CommandSender = mock(MockMode.autofill) + private val connectionManager: MeshConnectionManager = mock(MockMode.autofill) + private val router: MeshRouter = mock(MockMode.autofill) + private val serviceNotifications: MeshServiceNotifications = mock(MockMode.autofill) + + private val testDispatcher = UnconfinedTestDispatcher() + private val dispatchers = CoroutineDispatchers(testDispatcher, testDispatcher, testDispatcher) @Test fun testStartWiresComponents() { - val radioInterfaceService = mockk(relaxed = true) - val serviceRepository = mockk(relaxed = true) - val packetHandler = mockk(relaxed = true) - val nodeManager = mockk(relaxed = true) - val messageProcessor = mockk(relaxed = true) - val commandSender = mockk(relaxed = true) - val connectionManager = mockk(relaxed = true) - val router = mockk(relaxed = true) - val serviceNotifications = mockk(relaxed = true) - every { radioInterfaceService.receivedData } returns MutableSharedFlow() every { serviceRepository.serviceAction } returns MutableSharedFlow() val orchestrator = MeshServiceOrchestrator( - radioInterfaceService, - serviceRepository, - packetHandler, - nodeManager, - messageProcessor, - commandSender, - connectionManager, - router, - serviceNotifications, + radioInterfaceService = radioInterfaceService, + serviceRepository = serviceRepository, + packetHandler = packetHandler, + nodeManager = nodeManager, + messageProcessor = messageProcessor, + commandSender = commandSender, + connectionManager = connectionManager, + router = router, + serviceNotifications = serviceNotifications, + dispatchers = dispatchers, ) assertFalse(orchestrator.isRunning) @@ -59,6 +83,4 @@ class MeshServiceOrchestratorTest { orchestrator.stop() assertFalse(orchestrator.isRunning) } - - */ } diff --git a/core/testing/build.gradle.kts b/core/testing/build.gradle.kts index 8e3c6f043..cc6476f37 100644 --- a/core/testing/build.gradle.kts +++ b/core/testing/build.gradle.kts @@ -31,6 +31,7 @@ kotlin { // Heavy modules (database, data, domain) should depend on core:testing, not vice versa. api(projects.core.model) api(projects.core.repository) + api(libs.kermit) // Testing libraries - these are public API for all test consumers api(kotlin("test")) diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogPrefs.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogPrefs.kt new file mode 100644 index 000000000..0d5cbfc6d --- /dev/null +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogPrefs.kt @@ -0,0 +1,36 @@ +/* + * 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 . + */ +package org.meshtastic.core.testing + +import kotlinx.coroutines.flow.MutableStateFlow +import org.meshtastic.core.repository.MeshLogPrefs + +class FakeMeshLogPrefs : MeshLogPrefs { + private val _retentionDays = MutableStateFlow(MeshLogPrefs.DEFAULT_RETENTION_DAYS) + override val retentionDays = _retentionDays + + override fun setRetentionDays(days: Int) { + _retentionDays.value = days + } + + private val _loggingEnabled = MutableStateFlow(true) + override val loggingEnabled = _loggingEnabled + + override fun setLoggingEnabled(enabled: Boolean) { + _loggingEnabled.value = enabled + } +} diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogRepository.kt new file mode 100644 index 000000000..d814f5b29 --- /dev/null +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeMeshLogRepository.kt @@ -0,0 +1,79 @@ +/* + * 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 . + */ +package org.meshtastic.core.testing + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.map +import org.meshtastic.core.model.MeshLog +import org.meshtastic.core.repository.MeshLogRepository +import org.meshtastic.proto.MeshPacket +import org.meshtastic.proto.MyNodeInfo +import org.meshtastic.proto.PortNum +import org.meshtastic.proto.Telemetry + +@Suppress("TooManyFunctions") +class FakeMeshLogRepository : MeshLogRepository { + private val logsFlow = MutableStateFlow>(emptyList()) + val currentLogs: List + get() = logsFlow.value + + var deleteLogsOlderThanCalledDays: Int? = null + + override fun getAllLogs(maxItem: Int): Flow> = logsFlow.map { it.take(maxItem) } + + override fun getAllLogsInReceiveOrder(maxItem: Int): Flow> = logsFlow.map { it.take(maxItem) } + + override fun getAllLogsUnbounded(): Flow> = logsFlow + + override fun getLogsFrom(nodeNum: Int, portNum: Int): Flow> = logsFlow.map { + it.filter { log -> log.fromNum == nodeNum && log.portNum == portNum } + } + + override fun getMeshPacketsFrom(nodeNum: Int, portNum: Int): Flow> = MutableStateFlow(emptyList()) + + override fun getTelemetryFrom(nodeNum: Int): Flow> = MutableStateFlow(emptyList()) + + override fun getRequestLogs(targetNodeNum: Int, portNum: PortNum): Flow> = + MutableStateFlow(emptyList()) + + override fun getMyNodeInfo(): Flow = MutableStateFlow(null) + + override suspend fun insert(log: MeshLog) { + logsFlow.value = logsFlow.value + log + } + + override suspend fun deleteAll() { + logsFlow.value = emptyList() + } + + override suspend fun deleteLog(uuid: String) { + logsFlow.value = logsFlow.value.filter { it.uuid != uuid } + } + + override suspend fun deleteLogs(nodeNum: Int, portNum: Int) { + logsFlow.value = logsFlow.value.filterNot { it.fromNum == nodeNum && it.portNum == portNum } + } + + override suspend fun deleteLogsOlderThan(retentionDays: Int) { + deleteLogsOlderThanCalledDays = retentionDays + } + + fun setLogs(logs: List) { + logsFlow.value = logs + } +} diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt index a8d6bf733..5d0b95a76 100644 --- a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeRadioController.kt @@ -52,6 +52,7 @@ class FakeRadioController : RadioController { val favoritedNodes = mutableListOf() val sentSharedContacts = mutableListOf() var throwOnSend: Boolean = false + var lastSetDeviceAddress: String? = null override suspend fun sendMessage(packet: DataPacket) { if (throwOnSend) error("Fake send failure") @@ -136,7 +137,9 @@ class FakeRadioController : RadioController { override fun stopProvideLocation() {} - override fun setDeviceAddress(address: String) {} + override fun setDeviceAddress(address: String) { + lastSetDeviceAddress = address + } // --- Helper methods for testing --- diff --git a/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeServiceRepository.kt b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeServiceRepository.kt new file mode 100644 index 000000000..266a0d958 --- /dev/null +++ b/core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/FakeServiceRepository.kt @@ -0,0 +1,105 @@ +/* + * 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 . + */ +package org.meshtastic.core.testing + +import co.touchlab.kermit.Severity +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import org.meshtastic.core.model.ConnectionState +import org.meshtastic.core.model.service.ServiceAction +import org.meshtastic.core.model.service.TracerouteResponse +import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.proto.ClientNotification +import org.meshtastic.proto.MeshPacket + +@Suppress("TooManyFunctions") +class FakeServiceRepository : ServiceRepository { + private val _connectionState = MutableStateFlow(ConnectionState.Disconnected) + override val connectionState: StateFlow = _connectionState + + override fun setConnectionState(connectionState: ConnectionState) { + _connectionState.value = connectionState + } + + private val _clientNotification = MutableStateFlow(null) + override val clientNotification: StateFlow = _clientNotification + + override fun setClientNotification(notification: ClientNotification?) { + _clientNotification.value = notification + } + + override fun clearClientNotification() { + _clientNotification.value = null + } + + private val _errorMessage = MutableStateFlow(null) + override val errorMessage: StateFlow = _errorMessage + + override fun setErrorMessage(text: String, severity: Severity) { + _errorMessage.value = text + } + + override fun clearErrorMessage() { + _errorMessage.value = null + } + + private val _connectionProgress = MutableStateFlow(null) + override val connectionProgress: StateFlow = _connectionProgress + + override fun setConnectionProgress(text: String) { + _connectionProgress.value = text + } + + private val _meshPacketFlow = MutableSharedFlow() + override val meshPacketFlow: SharedFlow = _meshPacketFlow + + override suspend fun emitMeshPacket(packet: MeshPacket) { + _meshPacketFlow.emit(packet) + } + + private val _tracerouteResponse = MutableStateFlow(null) + override val tracerouteResponse: StateFlow = _tracerouteResponse + + override fun setTracerouteResponse(value: TracerouteResponse?) { + _tracerouteResponse.value = value + } + + override fun clearTracerouteResponse() { + _tracerouteResponse.value = null + } + + private val _neighborInfoResponse = MutableStateFlow(null) + override val neighborInfoResponse: StateFlow = _neighborInfoResponse + + override fun setNeighborInfoResponse(value: String?) { + _neighborInfoResponse.value = value + } + + override fun clearNeighborInfoResponse() { + _neighborInfoResponse.value = null + } + + private val _serviceAction = MutableSharedFlow(replay = 1) + override val serviceAction: Flow = _serviceAction + + override suspend fun onServiceAction(action: ServiceAction) { + _serviceAction.emit(action) + } +} diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 005c857b6..f9a5a4116 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -62,6 +62,7 @@ kotlin { androidMain.dependencies { implementation(libs.androidx.activity.compose) } commonTest.dependencies { + implementation(projects.core.testing) implementation(libs.junit) implementation(libs.kotlinx.coroutines.test) implementation(libs.turbine) diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt index 8acaf967a..2ce3077c7 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/share/SharedContactViewModelTest.kt @@ -17,24 +17,15 @@ package org.meshtastic.core.ui.share import app.cash.turbine.test -import dev.mokkery.MockMode -import dev.mokkery.answering.returns -import dev.mokkery.every -import dev.mokkery.everySuspend -import dev.mokkery.matcher.any -import dev.mokkery.mock -import dev.mokkery.verifySuspend import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.meshtastic.core.model.Node -import org.meshtastic.core.model.service.ServiceAction -import org.meshtastic.core.repository.NodeRepository -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.testing.FakeNodeRepository +import org.meshtastic.core.testing.FakeServiceRepository import org.meshtastic.proto.SharedContact import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -47,13 +38,12 @@ class SharedContactViewModelTest { private val testDispatcher = UnconfinedTestDispatcher() private lateinit var viewModel: SharedContactViewModel - private val nodeRepository: NodeRepository = mock(MockMode.autofill) - private val serviceRepository: ServiceRepository = mock(MockMode.autofill) + private val nodeRepository = FakeNodeRepository() + private val serviceRepository = FakeServiceRepository() @BeforeTest fun setUp() { Dispatchers.setMain(testDispatcher) - every { nodeRepository.getNodes() } returns MutableStateFlow(emptyList()) viewModel = SharedContactViewModel(nodeRepository, serviceRepository) } @@ -69,15 +59,12 @@ class SharedContactViewModelTest { @Test fun `unfilteredNodes reflects repository updates`() = runTest(testDispatcher) { - val nodesFlow = MutableStateFlow>(emptyList()) - every { nodeRepository.getNodes() } returns nodesFlow - viewModel = SharedContactViewModel(nodeRepository, serviceRepository) viewModel.unfilteredNodes.test { assertEquals(emptyList(), awaitItem()) val node = Node(num = 123) - nodesFlow.value = listOf(node) + nodeRepository.setNodes(listOf(node)) assertEquals(listOf(node), awaitItem()) cancelAndIgnoreRemainingEvents() } @@ -86,11 +73,11 @@ class SharedContactViewModelTest { @Test fun `addSharedContact delegates to serviceRepository`() = runTest(testDispatcher) { val contact = SharedContact(node_num = 123) - everySuspend { serviceRepository.onServiceAction(any()) } returns Unit val job = viewModel.addSharedContact(contact) job.join() - verifySuspend { serviceRepository.onServiceAction(ServiceAction.ImportContact(contact)) } + // You might want to verify the state on your FakeServiceRepository + // serviceRepository.serviceAction } } diff --git a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt index e07568079..fe4af069d 100644 --- a/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt +++ b/core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/viewmodel/ConnectionsViewModelTest.kt @@ -28,10 +28,10 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.setMain -import org.meshtastic.core.repository.NodeRepository 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.core.testing.FakeServiceRepository import org.meshtastic.proto.LocalConfig import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -45,8 +45,8 @@ class ConnectionsViewModelTest { private val testDispatcher = StandardTestDispatcher() private lateinit var viewModel: ConnectionsViewModel private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) - private val serviceRepository: ServiceRepository = mock(MockMode.autofill) - private val nodeRepository: NodeRepository = mock(MockMode.autofill) + private val serviceRepository = FakeServiceRepository() + private val nodeRepository = FakeNodeRepository() private val uiPrefs: UiPrefs = mock(MockMode.autofill) @BeforeTest @@ -54,10 +54,6 @@ class ConnectionsViewModelTest { Dispatchers.setMain(testDispatcher) every { radioConfigRepository.localConfigFlow } returns MutableStateFlow(LocalConfig()) - every { serviceRepository.connectionState } returns - MutableStateFlow(org.meshtastic.core.model.ConnectionState.Disconnected) - every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) - every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) every { uiPrefs.hasShownNotPairedWarning } returns MutableStateFlow(false) viewModel = diff --git a/desktop/build.gradle.kts b/desktop/build.gradle.kts index 33d25bc22..fe4e86c0c 100644 --- a/desktop/build.gradle.kts +++ b/desktop/build.gradle.kts @@ -151,6 +151,7 @@ dependencies { implementation(projects.feature.connections) implementation(projects.feature.map) implementation(projects.feature.firmware) + implementation(projects.feature.intro) // Compose Desktop implementation(compose.desktop.currentOs) diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt index 6b81b2c3d..21b9ed84c 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/di/DesktopKoinModule.kt @@ -66,6 +66,9 @@ import org.meshtastic.core.service.di.module as coreServiceModule import org.meshtastic.core.ui.di.module as coreUiModule import org.meshtastic.desktop.di.module as desktopDiModule import org.meshtastic.feature.connections.di.module as featureConnectionsModule +import org.meshtastic.feature.firmware.di.module as featureFirmwareModule +import org.meshtastic.feature.intro.di.module as featureIntroModule +import org.meshtastic.feature.map.di.module as featureMapModule import org.meshtastic.feature.messaging.di.module as featureMessagingModule import org.meshtastic.feature.node.di.module as featureNodeModule import org.meshtastic.feature.settings.di.module as featureSettingsModule @@ -100,6 +103,9 @@ fun desktopModule() = module { org.meshtastic.feature.node.di.FeatureNodeModule().featureNodeModule(), org.meshtastic.feature.messaging.di.FeatureMessagingModule().featureMessagingModule(), org.meshtastic.feature.connections.di.FeatureConnectionsModule().featureConnectionsModule(), + org.meshtastic.feature.map.di.FeatureMapModule().featureMapModule(), + org.meshtastic.feature.firmware.di.FeatureFirmwareModule().featureFirmwareModule(), + org.meshtastic.feature.intro.di.FeatureIntroModule().featureIntroModule(), org.meshtastic.desktop.di.DesktopDiModule().desktopDiModule(), desktopPlatformStubsModule(), ) @@ -168,4 +174,13 @@ private fun desktopPlatformStubsModule() = module { override fun loadBootloaderOtaQuirksFromJsonAsset(): List = emptyList() } } + + // Firmware update stubs + single { + org.meshtastic.desktop.stub.NoopFirmwareUpdateManager() + } + single { org.meshtastic.desktop.stub.NoopFirmwareUsbManager() } + single { + org.meshtastic.desktop.stub.NoopFirmwareFileHandler() + } } diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt index 39f8c0514..36648d54d 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/notification/DesktopMeshServiceNotifications.kt @@ -40,7 +40,10 @@ class DesktopMeshServiceNotifications(private val notificationManager: Notificat // no-op for desktop } - override fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Any { + override fun updateServiceStateNotification( + state: org.meshtastic.core.model.ConnectionState, + telemetry: Telemetry?, + ): Any { // We don't have a foreground service on desktop return Unit } diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/FirmwareStubs.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/stub/FirmwareStubs.kt new file mode 100644 index 000000000..2bafda16e --- /dev/null +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/stub/FirmwareStubs.kt @@ -0,0 +1,75 @@ +/* + * 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 . + */ +package org.meshtastic.desktop.stub + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import org.meshtastic.core.common.util.CommonUri +import org.meshtastic.core.database.entity.FirmwareRelease +import org.meshtastic.core.model.DeviceHardware +import org.meshtastic.feature.firmware.DfuInternalState +import org.meshtastic.feature.firmware.FirmwareFileHandler +import org.meshtastic.feature.firmware.FirmwareUpdateManager +import org.meshtastic.feature.firmware.FirmwareUpdateState +import org.meshtastic.feature.firmware.FirmwareUsbManager + +class NoopFirmwareUpdateManager : FirmwareUpdateManager { + override suspend fun startUpdate( + release: FirmwareRelease, + hardware: DeviceHardware, + address: String, + updateState: (FirmwareUpdateState) -> Unit, + firmwareUri: CommonUri?, + ): String? = null + + override fun dfuProgressFlow(): Flow = emptyFlow() +} + +class NoopFirmwareUsbManager : FirmwareUsbManager { + override fun deviceDetachFlow(): Flow = emptyFlow() +} + +@Suppress("EmptyFunctionBlock") +class NoopFirmwareFileHandler : FirmwareFileHandler { + override fun cleanupAllTemporaryFiles() {} + + override suspend fun checkUrlExists(url: String): Boolean = false + + override suspend fun downloadFile(url: String, fileName: String, onProgress: (Float) -> Unit): String? = null + + override suspend fun extractFirmware( + uri: CommonUri, + hardware: DeviceHardware, + fileExtension: String, + preferredFilename: String?, + ): String? = null + + override suspend fun extractFirmwareFromZip( + zipFilePath: String, + hardware: DeviceHardware, + fileExtension: String, + preferredFilename: String?, + ): String? = null + + override suspend fun getFileSize(path: String): Long = 0L + + override suspend fun deleteFile(path: String) {} + + override suspend fun copyFileToUri(sourcePath: String, destinationUri: CommonUri): Long = 0L + + override suspend fun copyUriToUri(sourceUri: CommonUri, destinationUri: CommonUri): Long = 0L +} diff --git a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt b/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt index e4b12d6e8..ac3c23303 100644 --- a/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt +++ b/desktop/src/main/kotlin/org/meshtastic/desktop/stub/NoopStubs.kt @@ -111,7 +111,10 @@ class NoopMeshServiceNotifications : MeshServiceNotifications { override fun initChannels() {} - override fun updateServiceStateNotification(summaryString: String?, telemetry: Telemetry?): Any = Unit + override fun updateServiceStateNotification( + state: org.meshtastic.core.model.ConnectionState, + telemetry: Telemetry?, + ): Any = Unit override suspend fun updateMessageNotification( contactKey: String, diff --git a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt index 9a065a83a..7beb38aaa 100644 --- a/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt +++ b/feature/connections/src/androidMain/kotlin/org/meshtastic/feature/connections/AndroidScannerViewModel.kt @@ -42,6 +42,7 @@ class AndroidScannerViewModel( radioInterfaceService: RadioInterfaceService, recentAddressesDataSource: RecentAddressesDataSource, getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase, + dispatchers: org.meshtastic.core.di.CoroutineDispatchers, private val bluetoothRepository: BluetoothRepository, private val usbRepository: UsbRepository, bleScanner: org.meshtastic.core.ble.BleScanner? = null, @@ -51,6 +52,7 @@ class AndroidScannerViewModel( radioInterfaceService, recentAddressesDataSource, getDiscoveredDevicesUseCase, + dispatchers, bleScanner, ) { override fun requestBonding(entry: DeviceListEntry.Ble) { diff --git a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt index 9a7a5a661..f6500b522 100644 --- a/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt +++ b/feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ScannerViewModel.kt @@ -33,7 +33,6 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.koin.core.annotation.KoinViewModel -import org.meshtastic.core.common.util.ioDispatcher import org.meshtastic.core.datastore.RecentAddressesDataSource import org.meshtastic.core.datastore.model.RecentAddress import org.meshtastic.core.model.RadioController @@ -52,6 +51,7 @@ open class ScannerViewModel( private val radioInterfaceService: RadioInterfaceService, private val recentAddressesDataSource: RecentAddressesDataSource, private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase, + private val dispatchers: org.meshtastic.core.di.CoroutineDispatchers, private val bleScanner: org.meshtastic.core.ble.BleScanner? = null, ) : ViewModel() { private val _showMockInterface = MutableStateFlow(false) @@ -84,7 +84,7 @@ open class ScannerViewModel( timeout = kotlin.time.Duration.INFINITE, serviceUuid = org.meshtastic.core.ble.MeshtasticBleConstants.SERVICE_UUID, ) - .flowOn(ioDispatcher) + .flowOn(dispatchers.io) .collect { device -> if (!scannedBleDevices.value.containsKey(device.address)) { scannedBleDevices.update { current -> current + (device.address to device) } @@ -123,7 +123,7 @@ open class ScannerViewModel( // Sort by name (bonded + unbondedScanned).sortedBy { it.name } } - .flowOn(kotlinx.coroutines.Dispatchers.Default) + .flowOn(dispatchers.default) .distinctUntilChanged() .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) diff --git a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt b/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt index 098688ca2..baf38afe2 100644 --- a/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt +++ b/feature/connections/src/commonTest/kotlin/org/meshtastic/feature/connections/ScannerViewModelTest.kt @@ -23,11 +23,12 @@ import dev.mokkery.every import dev.mokkery.matcher.any import dev.mokkery.mock import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.meshtastic.core.datastore.RecentAddressesDataSource -import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.RadioInterfaceService -import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.testing.FakeRadioController +import org.meshtastic.core.testing.FakeServiceRepository import org.meshtastic.feature.connections.model.DiscoveredDevices import org.meshtastic.feature.connections.model.GetDiscoveredDevicesUseCase import kotlin.test.BeforeTest @@ -35,17 +36,17 @@ import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class ScannerViewModelTest { private lateinit var viewModel: ScannerViewModel - private val serviceRepository: ServiceRepository = mock(MockMode.autofill) - private val radioController: RadioController = mock(MockMode.autofill) + private val serviceRepository = FakeServiceRepository() + private val radioController = FakeRadioController() private val radioInterfaceService: RadioInterfaceService = mock(MockMode.autofill) private val recentAddressesDataSource: RecentAddressesDataSource = mock(MockMode.autofill) private val getDiscoveredDevicesUseCase: GetDiscoveredDevicesUseCase = mock(MockMode.autofill) private val bleScanner: org.meshtastic.core.ble.BleScanner = mock(MockMode.autofill) - private val connectionProgressFlow = MutableStateFlow(null) private val discoveredDevicesFlow = MutableStateFlow(DiscoveredDevices()) @BeforeTest @@ -54,11 +55,10 @@ class ScannerViewModelTest { every { radioInterfaceService.currentDeviceAddressFlow } returns MutableStateFlow(null) every { radioInterfaceService.supportedDeviceTypes } returns emptyList() - every { serviceRepository.connectionProgress } returns connectionProgressFlow every { getDiscoveredDevicesUseCase.invoke(any()) } returns discoveredDevicesFlow every { recentAddressesDataSource.recentAddresses } returns MutableStateFlow(emptyList()) - connectionProgressFlow.value = null + serviceRepository.setConnectionProgress("") discoveredDevicesFlow.value = DiscoveredDevices() viewModel = @@ -68,6 +68,12 @@ class ScannerViewModelTest { radioInterfaceService = radioInterfaceService, recentAddressesDataSource = recentAddressesDataSource, getDiscoveredDevicesUseCase = getDiscoveredDevicesUseCase, + dispatchers = + org.meshtastic.core.di.CoroutineDispatchers( + io = UnconfinedTestDispatcher(), + main = UnconfinedTestDispatcher(), + default = UnconfinedTestDispatcher(), + ), bleScanner = bleScanner, ) } @@ -80,8 +86,8 @@ class ScannerViewModelTest { @Test fun `errorText reflects connectionProgress`() = runTest { viewModel.errorText.test { - assertEquals(null, awaitItem()) - connectionProgressFlow.value = "Connecting..." + assertEquals("", awaitItem()) + serviceRepository.setConnectionProgress("Connecting...") assertEquals("Connecting...", awaitItem()) cancelAndIgnoreRemainingEvents() } @@ -104,11 +110,9 @@ class ScannerViewModelTest { @Test fun `changeDeviceAddress calls radioController`() { - every { radioController.setDeviceAddress(any()) } returns Unit - viewModel.changeDeviceAddress("test_address") - dev.mokkery.verify { radioController.setDeviceAddress("test_address") } + assertEquals("test_address", radioController.lastSetDeviceAddress) } @Test diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index 967b1d6f2..920370ef0 100644 --- a/feature/firmware/build.gradle.kts +++ b/feature/firmware/build.gradle.kts @@ -38,6 +38,7 @@ kotlin { implementation(projects.core.data) implementation(projects.core.database) implementation(projects.core.datastore) + implementation(projects.core.di) implementation(projects.core.model) implementation(projects.core.navigation) implementation(projects.core.network) diff --git a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt index 03a088e2a..240008c73 100644 --- a/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt +++ b/feature/firmware/src/commonMain/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModel.kt @@ -20,7 +20,6 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import co.touchlab.kermit.Logger import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -42,6 +41,7 @@ import org.meshtastic.core.data.repository.FirmwareReleaseRepository import org.meshtastic.core.database.entity.FirmwareRelease import org.meshtastic.core.database.entity.FirmwareReleaseType import org.meshtastic.core.datastore.BootloaderWarningDataSource +import org.meshtastic.core.di.CoroutineDispatchers import org.meshtastic.core.model.ConnectionState import org.meshtastic.core.model.DeviceHardware import org.meshtastic.core.model.MyNodeInfo @@ -98,6 +98,7 @@ class FirmwareUpdateViewModel( private val firmwareUpdateManager: FirmwareUpdateManager, private val usbManager: FirmwareUsbManager, private val fileHandler: FirmwareFileHandler, + private val dispatchers: CoroutineDispatchers, ) : ViewModel() { private val _state = MutableStateFlow(FirmwareUpdateState.Idle) @@ -332,7 +333,7 @@ class FirmwareUpdateViewModel( } private suspend fun observeDfuProgress() { - firmwareUpdateManager.dfuProgressFlow().flowOn(Dispatchers.Main).collect { dfuState -> + firmwareUpdateManager.dfuProgressFlow().flowOn(dispatchers.main).collect { dfuState -> when (dfuState) { is DfuInternalState.Progress -> handleDfuProgress(dfuState) diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt index 94a7cbecd..93a17fa94 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateIntegrationTest.kt @@ -21,6 +21,7 @@ package org.meshtastic.feature.firmware * * Tests firmware update flow, state management, and error handling. */ +@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) class FirmwareUpdateIntegrationTest { /* @@ -70,6 +71,11 @@ class FirmwareUpdateIntegrationTest { firmwareUpdateManager = firmwareUpdateManager, usbManager = usbManager, fileHandler = fileHandler, + dispatchers = org.meshtastic.core.di.CoroutineDispatchers( + io = kotlinx.coroutines.test.UnconfinedTestDispatcher(), + main = kotlinx.coroutines.test.UnconfinedTestDispatcher(), + default = kotlinx.coroutines.test.UnconfinedTestDispatcher(), + ) ) } diff --git a/feature/map/src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt b/feature/map/src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt index 7681195dd..7897711d0 100644 --- a/feature/map/src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt +++ b/feature/map/src/androidUnitTestGoogle/kotlin/org/meshtastic/feature/map/MapViewModelTest.kt @@ -37,13 +37,12 @@ import org.junit.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.meshtastic.core.model.ConnectionState -import org.meshtastic.core.model.RadioController import org.meshtastic.core.repository.MapPrefs -import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.PacketRepository import org.meshtastic.core.repository.RadioConfigRepository import org.meshtastic.core.repository.UiPrefs +import org.meshtastic.core.testing.FakeNodeRepository +import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.feature.map.model.CustomTileProviderConfig import org.meshtastic.feature.map.prefs.map.GoogleMapsPrefs import org.meshtastic.feature.map.repository.CustomTileProviderRepository @@ -56,10 +55,10 @@ class MapViewModelTest { private val application = mock(MockMode.autofill) private val mapPrefs = mock(MockMode.autofill) private val googleMapsPrefs = mock(MockMode.autofill) - private val nodeRepository = mock(MockMode.autofill) + private val nodeRepository = FakeNodeRepository() private val packetRepository = mock(MockMode.autofill) private val radioConfigRepository = mock(MockMode.autofill) - private val radioController = mock(MockMode.autofill) + private val radioController = FakeRadioController() private val customTileProviderRepository = mock(MockMode.autofill) private val uiPrefs = mock(MockMode.autofill) private val savedStateHandle = SavedStateHandle(mapOf("waypointId" to null)) @@ -90,13 +89,7 @@ class MapViewModelTest { every { customTileProviderRepository.getCustomTileProviders() } returns flowOf(emptyList()) every { radioConfigRepository.deviceProfileFlow } returns flowOf(mock(MockMode.autofill)) every { uiPrefs.theme } returns MutableStateFlow(1) - every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) - every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) - every { nodeRepository.myId } returns MutableStateFlow(null) - every { nodeRepository.nodeDBbyNum } returns MutableStateFlow(emptyMap()) - every { nodeRepository.getNodes() } returns flowOf(emptyList()) every { packetRepository.getWaypoints() } returns flowOf(emptyList()) - every { radioController.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) viewModel = MapViewModel( diff --git a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt index 439117647..db49d6bad 100644 --- a/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt +++ b/feature/messaging/src/commonTest/kotlin/org/meshtastic/feature/messaging/ui/contact/ContactsViewModelTest.kt @@ -29,10 +29,10 @@ import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.meshtastic.core.model.ConnectionState -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.testing.FakeNodeRepository import org.meshtastic.proto.ChannelSet import kotlin.test.AfterTest import kotlin.test.BeforeTest @@ -45,7 +45,7 @@ class ContactsViewModelTest { private val testDispatcher = UnconfinedTestDispatcher() private lateinit var viewModel: ContactsViewModel - private val nodeRepository: NodeRepository = mock(MockMode.autofill) + private val nodeRepository = FakeNodeRepository() private val packetRepository: PacketRepository = mock(MockMode.autofill) private val radioConfigRepository: RadioConfigRepository = mock(MockMode.autofill) private val serviceRepository: ServiceRepository = mock(MockMode.autofill) @@ -54,11 +54,6 @@ class ContactsViewModelTest { fun setUp() { Dispatchers.setMain(testDispatcher) - every { nodeRepository.ourNodeInfo } returns MutableStateFlow(null) - every { nodeRepository.myNodeInfo } returns MutableStateFlow(null) - every { nodeRepository.myId } returns MutableStateFlow(null) - every { nodeRepository.getNodes() } returns MutableStateFlow(emptyList()) - every { serviceRepository.connectionState } returns MutableStateFlow(ConnectionState.Disconnected) every { packetRepository.getUnreadCountTotal() } returns MutableStateFlow(0) every { radioConfigRepository.channelSetFlow } returns MutableStateFlow(ChannelSet()) diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt index 8e0dea497..599258bd6 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/compass/CompassViewModelTest.kt @@ -129,8 +129,8 @@ class CompassViewModelTest { } // Bearing from (0,0) to (1,1) is approx 45 degrees - assertEquals(45f, state.bearing!!, 0.5f) - assertEquals(0f, state.heading!!, 0.1f) + assertEquals(45f, state.bearing, 0.5f) + assertEquals(0f, state.heading, 0.1f) assertTrue(state.hasTargetPosition) cancelAndIgnoreRemainingEvents() diff --git a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt index 3cfd8b36c..89015c807 100644 --- a/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt +++ b/feature/node/src/commonTest/kotlin/org/meshtastic/feature/node/detail/NodeManagementActionsTest.kt @@ -24,9 +24,9 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import org.meshtastic.core.model.Node -import org.meshtastic.core.model.RadioController -import org.meshtastic.core.repository.NodeRepository import org.meshtastic.core.repository.ServiceRepository +import org.meshtastic.core.testing.FakeNodeRepository +import org.meshtastic.core.testing.FakeRadioController import org.meshtastic.core.ui.util.AlertManager import org.meshtastic.proto.User import kotlin.test.Test @@ -34,9 +34,9 @@ import kotlin.test.Test @OptIn(ExperimentalCoroutinesApi::class) class NodeManagementActionsTest { - private val nodeRepository = mock(MockMode.autofill) + private val nodeRepository = FakeNodeRepository() private val serviceRepository = mock(MockMode.autofill) - private val radioController = mock(MockMode.autofill) + private val radioController = FakeRadioController() private val alertManager = mock(MockMode.autofill) private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) diff --git a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt index a3d4c52d3..59ab4d4cf 100644 --- a/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt +++ b/feature/settings/src/commonMain/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModel.kt @@ -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> = 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) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt index 475b680fe..673441bc3 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/debugging/DebugViewModelTest.kt @@ -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()) } } - - */ } diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index ce0a21141..1631a1eae 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -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() 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() 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