mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
test: migrate MigrationTest to runTest and add missing repository fakes (#5171)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
90f6e21a9c
commit
9f3fe865e3
8 changed files with 844 additions and 6 deletions
295
.pr5167.diff
Normal file
295
.pr5167.diff
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt
|
||||
new file mode 100644
|
||||
index 0000000000..2a27b96906
|
||||
--- /dev/null
|
||||
+++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/di/ApplicationCoroutineScope.kt
|
||||
@@ -0,0 +1,39 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 Meshtastic LLC
|
||||
+ *
|
||||
+ * This program is free software: you can redistribute it and/or modify
|
||||
+ * it under the terms of the GNU General Public License as published by
|
||||
+ * the Free Software Foundation, either version 3 of the License, or
|
||||
+ * (at your option) any later version.
|
||||
+ *
|
||||
+ * This program is distributed in the hope that it will be useful,
|
||||
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
+ * GNU General Public License for more details.
|
||||
+ *
|
||||
+ * You should have received a copy of the GNU General Public License
|
||||
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
+ */
|
||||
+package org.meshtastic.core.common.di
|
||||
+
|
||||
+import kotlinx.coroutines.CoroutineScope
|
||||
+import kotlinx.coroutines.SupervisorJob
|
||||
+import org.koin.core.annotation.Single
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
+
|
||||
+/**
|
||||
+ * A process-wide [CoroutineScope] that outlives individual ViewModels and UI components.
|
||||
+ *
|
||||
+ * Use this scope for fire-and-forget cleanup work that must continue after a ViewModel's own scope has been cancelled
|
||||
+ * (for example, deleting temporary files in `onCleared()`). Backed by a [SupervisorJob] so failures in one child do not
|
||||
+ * cancel siblings, and by [ioDispatcher] so work runs off the main thread.
|
||||
+ *
|
||||
+ * Prefer scoping work to a more specific scope (like `viewModelScope`) whenever possible; this scope is an escape hatch
|
||||
+ * and should be used sparingly.
|
||||
+ */
|
||||
+interface ApplicationCoroutineScope : CoroutineScope
|
||||
+
|
||||
+@Single(binds = [ApplicationCoroutineScope::class])
|
||||
+internal class ApplicationCoroutineScopeImpl : ApplicationCoroutineScope {
|
||||
+ override val coroutineContext = SupervisorJob() + ioDispatcher
|
||||
+}
|
||||
diff --git a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
index 231c84d401..5365ab95e2 100644
|
||||
--- a/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
+++ b/core/ui/src/androidMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
@@ -37,12 +37,12 @@ import androidx.lifecycle.compose.LifecycleEventEffect
|
||||
import co.touchlab.kermit.Logger
|
||||
import com.eygraber.uri.toAndroidUri
|
||||
import com.eygraber.uri.toKmpUri
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import org.meshtastic.core.common.gpsDisabled
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import java.net.URLEncoder
|
||||
|
||||
@Composable
|
||||
@@ -146,7 +146,7 @@ actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) ->
|
||||
val context = LocalContext.current
|
||||
return remember(context) {
|
||||
{ uri, maxChars ->
|
||||
- withContext(Dispatchers.IO) {
|
||||
+ withContext(ioDispatcher) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val androidUri = uri.toAndroidUri()
|
||||
diff --git a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
index 031e1fe35d..a938f92ea6 100644
|
||||
--- a/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
+++ b/core/ui/src/jvmMain/kotlin/org/meshtastic/core/ui/util/PlatformUtils.kt
|
||||
@@ -20,10 +20,10 @@ package org.meshtastic.core.ui.util
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import co.touchlab.kermit.Logger
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import java.awt.Desktop
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
@@ -89,7 +89,7 @@ actual fun rememberOpenFileLauncher(onUriReceived: (CommonUri?) -> Unit): (mimeT
|
||||
/** JVM — Reads text from a file URI. */
|
||||
@Composable
|
||||
actual fun rememberReadTextFromUri(): suspend (uri: CommonUri, maxChars: Int) -> String? = { uri, maxChars ->
|
||||
- withContext(Dispatchers.IO) {
|
||||
+ withContext(ioDispatcher) {
|
||||
@Suppress("TooGenericExceptionCaught")
|
||||
try {
|
||||
val file = File(URI(uri.toString()))
|
||||
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 dc1c459716..f8ff9fcac8 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
|
||||
@@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.jetbrains.compose.resources.StringResource
|
||||
import org.koin.core.annotation.KoinViewModel
|
||||
+import org.meshtastic.core.common.di.ApplicationCoroutineScope
|
||||
import org.meshtastic.core.common.util.CommonUri
|
||||
import org.meshtastic.core.common.util.safeCatching
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
@@ -91,6 +92,7 @@ class FirmwareUpdateViewModel(
|
||||
private val firmwareUpdateManager: FirmwareUpdateManager,
|
||||
private val usbManager: FirmwareUsbManager,
|
||||
private val fileHandler: FirmwareFileHandler,
|
||||
+ private val applicationScope: ApplicationCoroutineScope,
|
||||
) : ViewModel() {
|
||||
|
||||
private val _state = MutableStateFlow<FirmwareUpdateState>(FirmwareUpdateState.Idle)
|
||||
@@ -124,12 +126,10 @@ class FirmwareUpdateViewModel(
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
- // viewModelScope is already cancelled when onCleared() runs, so launch cleanup in a
|
||||
- // standalone scope. SupervisorJob prevents the coroutine from propagating failures to a
|
||||
- // shared parent, and NonCancellable on the launch keeps cleanup running even if the scope
|
||||
- // is cancelled concurrently.
|
||||
- @OptIn(kotlinx.coroutines.DelicateCoroutinesApi::class)
|
||||
- kotlinx.coroutines.GlobalScope.launch(NonCancellable) {
|
||||
+ // viewModelScope is already cancelled when onCleared() runs, so launch cleanup on the
|
||||
+ // application-wide scope (SupervisorJob + ioDispatcher). NonCancellable keeps cleanup
|
||||
+ // running even if something tries to cancel it mid-flight.
|
||||
+ applicationScope.launch(NonCancellable) {
|
||||
tempFirmwareFile = cleanupTemporaryFiles(fileHandler, tempFirmwareFile)
|
||||
}
|
||||
}
|
||||
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 4c48a1ced5..030d84effd 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
|
||||
@@ -108,6 +108,7 @@ class FirmwareUpdateIntegrationTest {
|
||||
firmwareUpdateManager,
|
||||
usbManager,
|
||||
fileHandler,
|
||||
+ TestApplicationCoroutineScope(testDispatcher),
|
||||
)
|
||||
|
||||
@Test
|
||||
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
|
||||
index 7032ed4088..a8eddff838 100644
|
||||
--- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
|
||||
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelTest.kt
|
||||
@@ -124,6 +124,7 @@ class FirmwareUpdateViewModelTest {
|
||||
firmwareUpdateManager,
|
||||
usbManager,
|
||||
fileHandler,
|
||||
+ TestApplicationCoroutineScope(testDispatcher),
|
||||
)
|
||||
|
||||
@Test
|
||||
diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt
|
||||
new file mode 100644
|
||||
index 0000000000..3ef5c44ef4
|
||||
--- /dev/null
|
||||
+++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/TestApplicationCoroutineScope.kt
|
||||
@@ -0,0 +1,26 @@
|
||||
+/*
|
||||
+ * Copyright (c) 2026 Meshtastic LLC
|
||||
+ *
|
||||
+ * This program is free software: you can redistribute it and/or modify
|
||||
+ * it under the terms of the GNU General Public License as published by
|
||||
+ * the Free Software Foundation, either version 3 of the License, or
|
||||
+ * (at your option) any later version.
|
||||
+ *
|
||||
+ * This program is distributed in the hope that it will be useful,
|
||||
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
+ * GNU General Public License for more details.
|
||||
+ *
|
||||
+ * You should have received a copy of the GNU General Public License
|
||||
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
+ */
|
||||
+package org.meshtastic.feature.firmware
|
||||
+
|
||||
+import kotlinx.coroutines.CoroutineDispatcher
|
||||
+import kotlinx.coroutines.CoroutineScope
|
||||
+import kotlinx.coroutines.SupervisorJob
|
||||
+import org.meshtastic.core.common.di.ApplicationCoroutineScope
|
||||
+
|
||||
+internal class TestApplicationCoroutineScope(dispatcher: CoroutineDispatcher) :
|
||||
+ ApplicationCoroutineScope,
|
||||
+ CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher)
|
||||
diff --git a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
|
||||
index acb1545bdd..23a0d03ab2 100644
|
||||
--- a/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
|
||||
+++ b/feature/firmware/src/jvmTest/kotlin/org/meshtastic/feature/firmware/FirmwareUpdateViewModelFileTest.kt
|
||||
@@ -116,6 +116,7 @@ class FirmwareUpdateViewModelFileTest {
|
||||
firmwareUpdateManager,
|
||||
usbManager,
|
||||
fileHandler,
|
||||
+ TestApplicationCoroutineScope(testDispatcher),
|
||||
)
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
index c251b4d5ef..315ad1da85 100644
|
||||
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
@@ -27,6 +27,7 @@ import co.touchlab.kermit.Logger
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import org.meshtastic.core.resources.Res
|
||||
import org.meshtastic.core.resources.debug_export_failed
|
||||
import org.meshtastic.core.resources.debug_export_success
|
||||
@@ -48,7 +49,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
|
||||
}
|
||||
|
||||
private suspend fun exportAllLogsToUri(context: Context, targetUri: Uri, logs: List<DebugViewModel.UiMeshLog>) =
|
||||
- withContext(Dispatchers.IO) {
|
||||
+ withContext(ioDispatcher) {
|
||||
try {
|
||||
if (logs.isEmpty()) {
|
||||
withContext(Dispatchers.Main) { context.showToast(Res.string.debug_export_failed, "No logs to export") }
|
||||
diff --git a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
index 9afde85e5f..a28a576788 100644
|
||||
--- a/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
+++ b/feature/settings/src/androidMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
@@ -24,9 +24,9 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import co.touchlab.kermit.Logger
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
|
||||
@Composable
|
||||
actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteArray): (fileName: String) -> Unit {
|
||||
@@ -41,7 +41,7 @@ actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteAr
|
||||
return { fileName -> exportLauncher.launch(fileName) }
|
||||
}
|
||||
|
||||
-private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(Dispatchers.IO) {
|
||||
+private suspend fun exportZipToUri(context: Context, targetUri: Uri, data: ByteArray) = withContext(ioDispatcher) {
|
||||
try {
|
||||
context.contentResolver.openOutputStream(targetUri)?.use { os -> os.write(data) }
|
||||
Logger.i { "TAK data package exported successfully to $targetUri" }
|
||||
diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
index 5b63cc90a3..a9a7285593 100644
|
||||
--- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
+++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/debugging/LogExporter.kt
|
||||
@@ -19,9 +19,9 @@ package org.meshtastic.feature.settings.debugging
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import co.touchlab.kermit.Logger
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
@@ -41,7 +41,7 @@ actual fun rememberLogExporter(logsProvider: suspend () -> List<DebugViewModel.U
|
||||
return@launch
|
||||
}
|
||||
|
||||
- withContext(Dispatchers.IO) {
|
||||
+ withContext(ioDispatcher) {
|
||||
// Run file dialog to ask user where to save
|
||||
val fileDialog = FileDialog(null as Frame?, "Export Logs", FileDialog.SAVE)
|
||||
fileDialog.file = fileName
|
||||
diff --git a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
index 9fb71379fc..bfbb85bc0d 100644
|
||||
--- a/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
+++ b/feature/settings/src/jvmMain/kotlin/org/meshtastic/feature/settings/tak/PrefExporter.kt
|
||||
@@ -19,9 +19,9 @@ package org.meshtastic.feature.settings.tak
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import co.touchlab.kermit.Logger
|
||||
-import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
+import org.meshtastic.core.common.util.ioDispatcher
|
||||
import java.awt.FileDialog
|
||||
import java.awt.Frame
|
||||
import java.io.File
|
||||
@@ -44,7 +44,7 @@ actual fun rememberDataPackageExporter(dataPackageProvider: suspend () -> ByteAr
|
||||
if (directory != null && file != null) {
|
||||
val targetFile = File(directory, file)
|
||||
val data = dataPackageProvider()
|
||||
- withContext(Dispatchers.IO) { targetFile.writeBytes(data) }
|
||||
+ withContext(ioDispatcher) { targetFile.writeBytes(data) }
|
||||
Logger.i { "TAK data package exported successfully to ${targetFile.absolutePath}" }
|
||||
}
|
||||
}
|
||||
|
|
@ -20,7 +20,7 @@ import androidx.room3.Room
|
|||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import okio.ByteString.Companion.toByteString
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
|
|
@ -59,7 +59,7 @@ class MigrationTest {
|
|||
)
|
||||
|
||||
@Before
|
||||
fun createDb(): Unit = runBlocking {
|
||||
fun createDb(): Unit = runTest {
|
||||
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
|
||||
database =
|
||||
Room.inMemoryDatabaseBuilder<MeshtasticDatabase>(
|
||||
|
|
@ -77,7 +77,7 @@ class MigrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun testMigrateChannelsByPSK_duplicatePSK() = runBlocking {
|
||||
fun testMigrateChannelsByPSK_duplicatePSK() = runTest {
|
||||
// PSK \"AQ==\" is base64 for single byte 0x01
|
||||
val pskBytes = byteArrayOf(0x01).toByteString()
|
||||
|
||||
|
|
@ -103,7 +103,7 @@ class MigrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun testMigrateChannelsByPSK_reorder() = runBlocking {
|
||||
fun testMigrateChannelsByPSK_reorder() = runTest {
|
||||
val pskA = byteArrayOf(0x01).toByteString()
|
||||
val pskB = byteArrayOf(0x02).toByteString()
|
||||
|
||||
|
|
@ -122,7 +122,7 @@ class MigrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun testMigrateChannelsByPSK_disambiguateByName() = runBlocking {
|
||||
fun testMigrateChannelsByPSK_disambiguateByName() = runTest {
|
||||
val pskA = byteArrayOf(0x01).toByteString()
|
||||
|
||||
insertPacket(channel = 0, text = "Msg A1")
|
||||
|
|
@ -141,7 +141,7 @@ class MigrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runBlocking {
|
||||
fun testMigrateChannelsByPSK_preferSameIndexIfStillAmbiguous() = runTest {
|
||||
val pskA = byteArrayOf(0x01).toByteString()
|
||||
|
||||
insertPacket(channel = 0, text = "Msg A")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.testing
|
||||
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.repository.DeviceHardwareRepository
|
||||
|
||||
/**
|
||||
* A test double for [DeviceHardwareRepository] backed by an in-memory map keyed by `(hwModel, target)`.
|
||||
*
|
||||
* Call [setHardware] (or [setHardwareForModel]) to seed results, or [setResult] to control the exact [Result] returned
|
||||
* for a given lookup. By default, lookups return `Result.success(null)`.
|
||||
*/
|
||||
class FakeDeviceHardwareRepository :
|
||||
BaseFake(),
|
||||
DeviceHardwareRepository {
|
||||
|
||||
private val hardware = mutableMapOf<Pair<Int, String?>, Result<DeviceHardware?>>()
|
||||
private val calls = mutableListOf<Triple<Int, String?, Boolean>>()
|
||||
|
||||
init {
|
||||
registerResetAction {
|
||||
hardware.clear()
|
||||
calls.clear()
|
||||
}
|
||||
}
|
||||
|
||||
/** Records every [getDeviceHardwareByModel] invocation for assertion. */
|
||||
val recordedCalls: List<Triple<Int, String?, Boolean>>
|
||||
get() = calls.toList()
|
||||
|
||||
override suspend fun getDeviceHardwareByModel(
|
||||
hwModel: Int,
|
||||
target: String?,
|
||||
forceRefresh: Boolean,
|
||||
): Result<DeviceHardware?> {
|
||||
calls.add(Triple(hwModel, target, forceRefresh))
|
||||
return hardware[hwModel to target] ?: hardware[hwModel to null] ?: Result.success(null)
|
||||
}
|
||||
|
||||
/** Seeds a successful lookup for the given model/target pair. */
|
||||
fun setHardware(hwModel: Int, target: String? = null, device: DeviceHardware?) {
|
||||
hardware[hwModel to target] = Result.success(device)
|
||||
}
|
||||
|
||||
/** Seeds a successful lookup for any target of the given model. */
|
||||
fun setHardwareForModel(hwModel: Int, device: DeviceHardware?) {
|
||||
hardware[hwModel to null] = Result.success(device)
|
||||
}
|
||||
|
||||
/** Seeds an arbitrary [Result] for the given lookup (use to test failure paths). */
|
||||
fun setResult(hwModel: Int, target: String? = null, result: Result<DeviceHardware?>) {
|
||||
hardware[hwModel to target] = result
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.testing
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.repository.FirmwareReleaseRepository
|
||||
|
||||
/**
|
||||
* A test double for [FirmwareReleaseRepository] that exposes stable and alpha releases as
|
||||
* [kotlinx.coroutines.flow.MutableStateFlow]s.
|
||||
*
|
||||
* Use [setStableRelease] and [setAlphaRelease] to drive the emitted values.
|
||||
*/
|
||||
class FakeFirmwareReleaseRepository :
|
||||
BaseFake(),
|
||||
FirmwareReleaseRepository {
|
||||
|
||||
private val _stableRelease = mutableStateFlow<FirmwareRelease?>(null)
|
||||
private val _alphaRelease = mutableStateFlow<FirmwareRelease?>(null)
|
||||
|
||||
override val stableRelease: Flow<FirmwareRelease?> = _stableRelease
|
||||
override val alphaRelease: Flow<FirmwareRelease?> = _alphaRelease
|
||||
|
||||
var invalidateCacheCalls: Int = 0
|
||||
private set
|
||||
|
||||
init {
|
||||
registerResetAction { invalidateCacheCalls = 0 }
|
||||
}
|
||||
|
||||
override suspend fun invalidateCache() {
|
||||
invalidateCacheCalls++
|
||||
}
|
||||
|
||||
fun setStableRelease(release: FirmwareRelease?) {
|
||||
_stableRelease.value = release
|
||||
}
|
||||
|
||||
fun setAlphaRelease(release: FirmwareRelease?) {
|
||||
_alphaRelease.value = release
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.testing
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import org.meshtastic.core.repository.QuickChatActionRepository
|
||||
|
||||
/**
|
||||
* A test double for [QuickChatActionRepository] that keeps actions in an in-memory list (sorted by `position`).
|
||||
*
|
||||
* The in-memory list is exposed reactively through [getAllActions].
|
||||
*/
|
||||
class FakeQuickChatActionRepository :
|
||||
BaseFake(),
|
||||
QuickChatActionRepository {
|
||||
|
||||
private val actionsFlow = mutableStateFlow<List<QuickChatAction>>(emptyList())
|
||||
|
||||
override fun getAllActions(): Flow<List<QuickChatAction>> = actionsFlow
|
||||
|
||||
override suspend fun upsert(action: QuickChatAction) {
|
||||
val existingIndex = actionsFlow.value.indexOfFirst { it.uuid == action.uuid }
|
||||
actionsFlow.value =
|
||||
if (existingIndex >= 0) {
|
||||
actionsFlow.value.toMutableList().also { it[existingIndex] = action }
|
||||
} else {
|
||||
actionsFlow.value + action
|
||||
}
|
||||
.sortedBy { it.position }
|
||||
}
|
||||
|
||||
override suspend fun deleteAll() {
|
||||
actionsFlow.value = emptyList()
|
||||
}
|
||||
|
||||
override suspend fun delete(action: QuickChatAction) {
|
||||
actionsFlow.value =
|
||||
actionsFlow.value
|
||||
.filterNot { it.uuid == action.uuid }
|
||||
.map { if (it.position > action.position) it.copy(position = it.position - 1) else it }
|
||||
}
|
||||
|
||||
override suspend fun setItemPosition(uuid: Long, newPos: Int) {
|
||||
actionsFlow.value =
|
||||
actionsFlow.value.map { if (it.uuid == uuid) it.copy(position = newPos) else it }.sortedBy { it.position }
|
||||
}
|
||||
|
||||
/** Seeds the current list of actions (useful for test setup). */
|
||||
fun setActions(actions: List<QuickChatAction>) {
|
||||
actionsFlow.value = actions.sortedBy { it.position }
|
||||
}
|
||||
|
||||
/** Returns the current in-memory snapshot. */
|
||||
val currentActions: List<QuickChatAction>
|
||||
get() = actionsFlow.value
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.testing
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.core.repository.RadioConfigRepository
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.ChannelSet
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.Config
|
||||
import org.meshtastic.proto.DeviceProfile
|
||||
import org.meshtastic.proto.DeviceUIConfig
|
||||
import org.meshtastic.proto.FileInfo
|
||||
import org.meshtastic.proto.LocalConfig
|
||||
import org.meshtastic.proto.LocalModuleConfig
|
||||
import org.meshtastic.proto.ModuleConfig
|
||||
|
||||
/**
|
||||
* A test double for [RadioConfigRepository] backed by in-memory [kotlinx.coroutines.flow.MutableStateFlow]s.
|
||||
*
|
||||
* All mutator methods update the underlying state flows synchronously so tests can observe changes immediately.
|
||||
* [deviceProfileFlow] is derived from [localConfigFlow], [moduleConfigFlow], and the current channel set.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class FakeRadioConfigRepository :
|
||||
BaseFake(),
|
||||
RadioConfigRepository {
|
||||
|
||||
private val channelSetBacking = mutableStateFlow(ChannelSet())
|
||||
override val channelSetFlow: Flow<ChannelSet> = channelSetBacking
|
||||
|
||||
private val localConfigBacking = mutableStateFlow(LocalConfig())
|
||||
override val localConfigFlow: Flow<LocalConfig> = localConfigBacking
|
||||
|
||||
private val moduleConfigBacking = mutableStateFlow(LocalModuleConfig())
|
||||
override val moduleConfigFlow: Flow<LocalModuleConfig> = moduleConfigBacking
|
||||
|
||||
private val deviceProfileBacking = mutableStateFlow(DeviceProfile())
|
||||
override val deviceProfileFlow: Flow<DeviceProfile> = deviceProfileBacking
|
||||
val currentDeviceProfile: DeviceProfile
|
||||
get() = deviceProfileBacking.value
|
||||
|
||||
private val deviceUIConfigBacking = mutableStateFlow<DeviceUIConfig?>(null)
|
||||
override val deviceUIConfigFlow: Flow<DeviceUIConfig?> = deviceUIConfigBacking
|
||||
|
||||
private val fileManifestBacking = mutableStateFlow<List<FileInfo>>(emptyList())
|
||||
override val fileManifestFlow: Flow<List<FileInfo>> = fileManifestBacking
|
||||
|
||||
val currentChannelSet: ChannelSet
|
||||
get() = channelSetBacking.value
|
||||
|
||||
val currentLocalConfig: LocalConfig
|
||||
get() = localConfigBacking.value
|
||||
|
||||
val currentModuleConfig: LocalModuleConfig
|
||||
get() = moduleConfigBacking.value
|
||||
|
||||
val currentDeviceUIConfig: DeviceUIConfig?
|
||||
get() = deviceUIConfigBacking.value
|
||||
|
||||
val currentFileManifest: List<FileInfo>
|
||||
get() = fileManifestBacking.value
|
||||
|
||||
/**
|
||||
* Last [Config] passed to [setLocalConfig] (null until called). Tests should use [setLocalConfigDirect] to drive
|
||||
* state.
|
||||
*/
|
||||
var lastSetLocalConfig: Config? = null
|
||||
private set
|
||||
|
||||
/** Last [ModuleConfig] passed to [setLocalModuleConfig] (null until called). */
|
||||
var lastSetModuleConfig: ModuleConfig? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
registerResetAction {
|
||||
lastSetLocalConfig = null
|
||||
lastSetModuleConfig = null
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearChannelSet() {
|
||||
channelSetBacking.value = ChannelSet()
|
||||
}
|
||||
|
||||
override suspend fun replaceAllSettings(settingsList: List<ChannelSettings>) {
|
||||
channelSetBacking.value = channelSetBacking.value.copy(settings = settingsList)
|
||||
}
|
||||
|
||||
override suspend fun updateChannelSettings(channel: Channel) {
|
||||
val current = channelSetBacking.value.settings.toMutableList()
|
||||
while (current.size <= channel.index) current.add(ChannelSettings())
|
||||
current[channel.index] = channel.settings ?: ChannelSettings()
|
||||
channelSetBacking.value = channelSetBacking.value.copy(settings = current)
|
||||
}
|
||||
|
||||
override suspend fun clearLocalConfig() {
|
||||
localConfigBacking.value = LocalConfig()
|
||||
}
|
||||
|
||||
override suspend fun setLocalConfig(config: Config) {
|
||||
lastSetLocalConfig = config
|
||||
}
|
||||
|
||||
override suspend fun clearLocalModuleConfig() {
|
||||
moduleConfigBacking.value = LocalModuleConfig()
|
||||
}
|
||||
|
||||
override suspend fun setLocalModuleConfig(config: ModuleConfig) {
|
||||
lastSetModuleConfig = config
|
||||
}
|
||||
|
||||
override suspend fun setDeviceUIConfig(config: DeviceUIConfig) {
|
||||
deviceUIConfigBacking.value = config
|
||||
}
|
||||
|
||||
override suspend fun clearDeviceUIConfig() {
|
||||
deviceUIConfigBacking.value = null
|
||||
}
|
||||
|
||||
override suspend fun addFileInfo(info: FileInfo) {
|
||||
fileManifestBacking.value = fileManifestBacking.value + info
|
||||
}
|
||||
|
||||
override suspend fun clearFileManifest() {
|
||||
fileManifestBacking.value = emptyList()
|
||||
}
|
||||
|
||||
/** Directly sets the [LocalConfig] without merging (preferred for test setup). */
|
||||
fun setLocalConfigDirect(config: LocalConfig) {
|
||||
localConfigBacking.value = config
|
||||
}
|
||||
|
||||
/** Directly sets the [LocalModuleConfig] without merging (preferred for test setup). */
|
||||
fun setLocalModuleConfigDirect(config: LocalModuleConfig) {
|
||||
moduleConfigBacking.value = config
|
||||
}
|
||||
|
||||
/** Directly sets the combined [DeviceProfile] emitted by [deviceProfileFlow]. */
|
||||
fun setDeviceProfile(profile: DeviceProfile) {
|
||||
deviceProfileBacking.value = profile
|
||||
}
|
||||
|
||||
/** Directly sets the [ChannelSet] (bypasses [updateChannelSettings]/[replaceAllSettings]). */
|
||||
fun setChannelSet(channelSet: ChannelSet) {
|
||||
channelSetBacking.value = channelSet
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.testing
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.meshtastic.core.repository.TracerouteSnapshotRepository
|
||||
import org.meshtastic.proto.Position
|
||||
|
||||
/**
|
||||
* A test double for [TracerouteSnapshotRepository] keyed by `logUuid`.
|
||||
*
|
||||
* Use [upsertSnapshotPositions] as you would in production, or [seedSnapshot] to directly inject state for a log.
|
||||
*/
|
||||
class FakeTracerouteSnapshotRepository :
|
||||
BaseFake(),
|
||||
TracerouteSnapshotRepository {
|
||||
|
||||
private val snapshots = mutableStateFlow<Map<String, Map<Int, Position>>>(emptyMap())
|
||||
private val requestIds = mutableMapOf<String, Int>()
|
||||
|
||||
init {
|
||||
registerResetAction { requestIds.clear() }
|
||||
}
|
||||
|
||||
override fun getSnapshotPositions(logUuid: String): Flow<Map<Int, Position>> =
|
||||
snapshots.map { it[logUuid].orEmpty() }
|
||||
|
||||
override suspend fun upsertSnapshotPositions(logUuid: String, requestId: Int, positions: Map<Int, Position>) {
|
||||
requestIds[logUuid] = requestId
|
||||
snapshots.value = snapshots.value.toMutableMap().also { it[logUuid] = positions }
|
||||
}
|
||||
|
||||
/** Directly seeds the snapshot for a log (bypasses request-id tracking). */
|
||||
fun seedSnapshot(logUuid: String, positions: Map<Int, Position>) {
|
||||
snapshots.value = snapshots.value.toMutableMap().also { it[logUuid] = positions }
|
||||
}
|
||||
|
||||
/** Returns the last request-id recorded for [logUuid], or `null` if none. */
|
||||
fun lastRequestId(logUuid: String): Int? = requestIds[logUuid]
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright (c) 2026 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.testing
|
||||
|
||||
import app.cash.turbine.test
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.meshtastic.core.database.entity.FirmwareRelease
|
||||
import org.meshtastic.core.database.entity.QuickChatAction
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.proto.Channel
|
||||
import org.meshtastic.proto.ChannelSettings
|
||||
import org.meshtastic.proto.Position
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class RepositoryFakesTest {
|
||||
|
||||
@Test
|
||||
fun `FakeDeviceHardwareRepository returns seeded hardware and records calls`() = runTest {
|
||||
val repo = FakeDeviceHardwareRepository()
|
||||
val hw = DeviceHardware(hwModel = 42, hwModelSlug = "TEST", platformioTarget = "tlora")
|
||||
repo.setHardware(hwModel = 42, target = "tlora", device = hw)
|
||||
|
||||
val hit = repo.getDeviceHardwareByModel(hwModel = 42, target = "tlora", forceRefresh = false)
|
||||
val miss = repo.getDeviceHardwareByModel(hwModel = 99)
|
||||
|
||||
assertEquals(hw, hit.getOrNull())
|
||||
assertNull(miss.getOrNull())
|
||||
assertEquals(2, repo.recordedCalls.size)
|
||||
assertEquals(Triple(42, "tlora", false), repo.recordedCalls.first())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FakeFirmwareReleaseRepository emits stable and alpha releases`() = runTest {
|
||||
val repo = FakeFirmwareReleaseRepository()
|
||||
val stable = FirmwareRelease(id = "1.0", title = "1.0", pageUrl = "", zipUrl = "")
|
||||
val alpha = FirmwareRelease(id = "1.1-a", title = "1.1-a", pageUrl = "", zipUrl = "")
|
||||
|
||||
repo.setStableRelease(stable)
|
||||
repo.setAlphaRelease(alpha)
|
||||
|
||||
assertEquals(stable, repo.stableRelease.first())
|
||||
assertEquals(alpha, repo.alphaRelease.first())
|
||||
|
||||
repo.invalidateCache()
|
||||
repo.invalidateCache()
|
||||
assertEquals(2, repo.invalidateCacheCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FakeQuickChatActionRepository upsert delete and reorder`() = runTest {
|
||||
val repo = FakeQuickChatActionRepository()
|
||||
val a = QuickChatAction(uuid = 1L, name = "A", message = "hi", position = 0)
|
||||
val b = QuickChatAction(uuid = 2L, name = "B", message = "bye", position = 1)
|
||||
|
||||
repo.upsert(a)
|
||||
repo.upsert(b)
|
||||
assertEquals(listOf(a, b), repo.getAllActions().first())
|
||||
|
||||
repo.setItemPosition(uuid = 1L, newPos = 5)
|
||||
assertEquals(listOf(2L, 1L), repo.getAllActions().first().map { it.uuid })
|
||||
|
||||
repo.delete(b)
|
||||
assertEquals(1, repo.currentActions.size)
|
||||
|
||||
repo.deleteAll()
|
||||
assertTrue(repo.currentActions.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FakeQuickChatActionRepository delete compacts positions`() = runTest {
|
||||
val repo = FakeQuickChatActionRepository()
|
||||
val a = QuickChatAction(uuid = 1L, name = "A", message = "", position = 0)
|
||||
val b = QuickChatAction(uuid = 2L, name = "B", message = "", position = 1)
|
||||
val c = QuickChatAction(uuid = 3L, name = "C", message = "", position = 2)
|
||||
repo.upsert(a)
|
||||
repo.upsert(b)
|
||||
repo.upsert(c)
|
||||
|
||||
repo.delete(b)
|
||||
|
||||
// Matches real DAO's decrementPositionsAfter: positions must stay contiguous.
|
||||
assertEquals(listOf(1L to 0, 3L to 1), repo.currentActions.map { it.uuid to it.position })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FakeTracerouteSnapshotRepository roundtrips positions keyed by log uuid`() = runTest {
|
||||
val repo = FakeTracerouteSnapshotRepository()
|
||||
val positions = mapOf(1 to Position(latitude_i = 10), 2 to Position(latitude_i = 20))
|
||||
repo.upsertSnapshotPositions(logUuid = "log-1", requestId = 99, positions = positions)
|
||||
|
||||
repo.getSnapshotPositions("log-1").test { assertEquals(positions, awaitItem()) }
|
||||
assertEquals(99, repo.lastRequestId("log-1"))
|
||||
assertNull(repo.lastRequestId("other"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FakeRadioConfigRepository tracks channel set and module config`() = runTest {
|
||||
val repo = FakeRadioConfigRepository()
|
||||
val a = ChannelSettings(name = "A")
|
||||
val b = ChannelSettings(name = "B")
|
||||
|
||||
repo.replaceAllSettings(listOf(a, b))
|
||||
assertEquals(listOf(a, b), repo.currentChannelSet.settings)
|
||||
|
||||
repo.updateChannelSettings(Channel(index = 1, settings = ChannelSettings(name = "B2")))
|
||||
assertEquals("B2", repo.currentChannelSet.settings[1].name)
|
||||
|
||||
repo.clearChannelSet()
|
||||
assertTrue(repo.currentChannelSet.settings.isEmpty())
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue