mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
Refactor map layer management and navigation infrastructure (#4921)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
b608a04ca4
commit
a005231d94
142 changed files with 5408 additions and 3090 deletions
|
|
@ -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.core.testing
|
||||
|
||||
import org.meshtastic.core.repository.Location
|
||||
|
||||
/** Creates an Android [Location] for testing. */
|
||||
actual fun createLocation(latitude: Double, longitude: Double, altitude: Double): Location = Location("fake").apply {
|
||||
this.latitude = latitude
|
||||
this.longitude = longitude
|
||||
this.altitude = altitude
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 androidx.test.core.app.ApplicationProvider
|
||||
import org.meshtastic.core.common.ContextServices
|
||||
|
||||
actual fun setupTestContext() {
|
||||
ContextServices.app = ApplicationProvider.getApplicationContext()
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
/** Base class for fakes that provides common utilities for state management and reset capabilities. */
|
||||
abstract class BaseFake {
|
||||
private val resetActions = mutableListOf<() -> Unit>()
|
||||
|
||||
/** Creates a [MutableStateFlow] and registers it for automatic reset. */
|
||||
protected fun <T> mutableStateFlow(initialValue: T): MutableStateFlow<T> {
|
||||
val flow = MutableStateFlow(initialValue)
|
||||
resetActions.add { flow.value = initialValue }
|
||||
return flow
|
||||
}
|
||||
|
||||
/** Creates a [MutableSharedFlow] and registers it for automatic reset (replay cache cleared). */
|
||||
protected fun <T> mutableSharedFlow(replay: Int = 0): MutableSharedFlow<T> {
|
||||
val flow = MutableSharedFlow<T>(replay = replay)
|
||||
resetActions.add { flow.resetReplayCache() }
|
||||
return flow
|
||||
}
|
||||
|
||||
/** Registers a custom reset action (e.g. clearing a list of recorded calls). */
|
||||
protected fun registerResetAction(action: () -> Unit) {
|
||||
resetActions.add(action)
|
||||
}
|
||||
|
||||
/** Resets all registered state flows and custom actions to their initial state. */
|
||||
open fun reset() {
|
||||
resetActions.forEach { it() }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* 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.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.repository.AnalyticsPrefs
|
||||
import org.meshtastic.core.repository.AppPreferences
|
||||
import org.meshtastic.core.repository.CustomEmojiPrefs
|
||||
import org.meshtastic.core.repository.FilterPrefs
|
||||
import org.meshtastic.core.repository.HomoglyphPrefs
|
||||
import org.meshtastic.core.repository.MapConsentPrefs
|
||||
import org.meshtastic.core.repository.MapPrefs
|
||||
import org.meshtastic.core.repository.MapTileProviderPrefs
|
||||
import org.meshtastic.core.repository.MeshPrefs
|
||||
import org.meshtastic.core.repository.RadioPrefs
|
||||
import org.meshtastic.core.repository.UiPrefs
|
||||
|
||||
class FakeAnalyticsPrefs : AnalyticsPrefs {
|
||||
override val analyticsAllowed = MutableStateFlow(false)
|
||||
|
||||
override fun setAnalyticsAllowed(allowed: Boolean) {
|
||||
analyticsAllowed.value = allowed
|
||||
}
|
||||
|
||||
override val installId = MutableStateFlow("fake-install-id")
|
||||
}
|
||||
|
||||
class FakeHomoglyphPrefs : HomoglyphPrefs {
|
||||
override val homoglyphEncodingEnabled = MutableStateFlow(false)
|
||||
|
||||
override fun setHomoglyphEncodingEnabled(enabled: Boolean) {
|
||||
homoglyphEncodingEnabled.value = enabled
|
||||
}
|
||||
}
|
||||
|
||||
class FakeFilterPrefs : FilterPrefs {
|
||||
override val filterEnabled = MutableStateFlow(false)
|
||||
|
||||
override fun setFilterEnabled(enabled: Boolean) {
|
||||
filterEnabled.value = enabled
|
||||
}
|
||||
|
||||
override val filterWords = MutableStateFlow(emptySet<String>())
|
||||
|
||||
override fun setFilterWords(words: Set<String>) {
|
||||
filterWords.value = words
|
||||
}
|
||||
}
|
||||
|
||||
class FakeCustomEmojiPrefs : CustomEmojiPrefs {
|
||||
override val customEmojiFrequency = MutableStateFlow<String?>(null)
|
||||
|
||||
override fun setCustomEmojiFrequency(frequency: String?) {
|
||||
customEmojiFrequency.value = frequency
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("TooManyFunctions")
|
||||
class FakeUiPrefs : UiPrefs {
|
||||
override val appIntroCompleted = MutableStateFlow(false)
|
||||
|
||||
override fun setAppIntroCompleted(completed: Boolean) {
|
||||
appIntroCompleted.value = completed
|
||||
}
|
||||
|
||||
override val theme = MutableStateFlow(0)
|
||||
|
||||
override fun setTheme(value: Int) {
|
||||
theme.value = value
|
||||
}
|
||||
|
||||
override val locale = MutableStateFlow("en")
|
||||
|
||||
override fun setLocale(languageTag: String) {
|
||||
locale.value = languageTag
|
||||
}
|
||||
|
||||
override val nodeSort = MutableStateFlow(0)
|
||||
|
||||
override fun setNodeSort(value: Int) {
|
||||
nodeSort.value = value
|
||||
}
|
||||
|
||||
override val includeUnknown = MutableStateFlow(true)
|
||||
|
||||
override fun setIncludeUnknown(value: Boolean) {
|
||||
includeUnknown.value = value
|
||||
}
|
||||
|
||||
override val excludeInfrastructure = MutableStateFlow(false)
|
||||
|
||||
override fun setExcludeInfrastructure(value: Boolean) {
|
||||
excludeInfrastructure.value = value
|
||||
}
|
||||
|
||||
override val onlyOnline = MutableStateFlow(false)
|
||||
|
||||
override fun setOnlyOnline(value: Boolean) {
|
||||
onlyOnline.value = value
|
||||
}
|
||||
|
||||
override val onlyDirect = MutableStateFlow(false)
|
||||
|
||||
override fun setOnlyDirect(value: Boolean) {
|
||||
onlyDirect.value = value
|
||||
}
|
||||
|
||||
override val showIgnored = MutableStateFlow(false)
|
||||
|
||||
override fun setShowIgnored(value: Boolean) {
|
||||
showIgnored.value = value
|
||||
}
|
||||
|
||||
override val excludeMqtt = MutableStateFlow(false)
|
||||
|
||||
override fun setExcludeMqtt(value: Boolean) {
|
||||
excludeMqtt.value = value
|
||||
}
|
||||
|
||||
override val hasShownNotPairedWarning = MutableStateFlow(false)
|
||||
|
||||
override fun setHasShownNotPairedWarning(shown: Boolean) {
|
||||
hasShownNotPairedWarning.value = shown
|
||||
}
|
||||
|
||||
override val showQuickChat = MutableStateFlow(true)
|
||||
|
||||
override fun setShowQuickChat(show: Boolean) {
|
||||
showQuickChat.value = show
|
||||
}
|
||||
|
||||
private val nodeLocationEnabled = mutableMapOf<Int, MutableStateFlow<Boolean>>()
|
||||
|
||||
override fun shouldProvideNodeLocation(nodeNum: Int): StateFlow<Boolean> =
|
||||
nodeLocationEnabled.getOrPut(nodeNum) { MutableStateFlow(true) }
|
||||
|
||||
override fun setShouldProvideNodeLocation(nodeNum: Int, provide: Boolean) {
|
||||
nodeLocationEnabled.getOrPut(nodeNum) { MutableStateFlow(provide) }.value = provide
|
||||
}
|
||||
}
|
||||
|
||||
class FakeMapPrefs : MapPrefs {
|
||||
override val mapStyle = MutableStateFlow(0)
|
||||
|
||||
override fun setMapStyle(style: Int) {
|
||||
mapStyle.value = style
|
||||
}
|
||||
|
||||
override val showOnlyFavorites = MutableStateFlow(false)
|
||||
|
||||
override fun setShowOnlyFavorites(show: Boolean) {
|
||||
showOnlyFavorites.value = show
|
||||
}
|
||||
|
||||
override val showWaypointsOnMap = MutableStateFlow(true)
|
||||
|
||||
override fun setShowWaypointsOnMap(show: Boolean) {
|
||||
showWaypointsOnMap.value = show
|
||||
}
|
||||
|
||||
override val showPrecisionCircleOnMap = MutableStateFlow(true)
|
||||
|
||||
override fun setShowPrecisionCircleOnMap(show: Boolean) {
|
||||
showPrecisionCircleOnMap.value = show
|
||||
}
|
||||
|
||||
override val lastHeardFilter = MutableStateFlow(0L)
|
||||
|
||||
override fun setLastHeardFilter(seconds: Long) {
|
||||
lastHeardFilter.value = seconds
|
||||
}
|
||||
|
||||
override val lastHeardTrackFilter = MutableStateFlow(0L)
|
||||
|
||||
override fun setLastHeardTrackFilter(seconds: Long) {
|
||||
lastHeardTrackFilter.value = seconds
|
||||
}
|
||||
}
|
||||
|
||||
class FakeMapConsentPrefs : MapConsentPrefs {
|
||||
private val consent = mutableMapOf<Int?, MutableStateFlow<Boolean>>()
|
||||
|
||||
override fun shouldReportLocation(nodeNum: Int?): StateFlow<Boolean> =
|
||||
consent.getOrPut(nodeNum) { MutableStateFlow(false) }
|
||||
|
||||
override fun setShouldReportLocation(nodeNum: Int?, report: Boolean) {
|
||||
consent.getOrPut(nodeNum) { MutableStateFlow(report) }.value = report
|
||||
}
|
||||
}
|
||||
|
||||
class FakeMapTileProviderPrefs : MapTileProviderPrefs {
|
||||
override val customTileProviders = MutableStateFlow<String?>(null)
|
||||
|
||||
override fun setCustomTileProviders(providers: String?) {
|
||||
customTileProviders.value = providers
|
||||
}
|
||||
}
|
||||
|
||||
class FakeRadioPrefs : RadioPrefs {
|
||||
override val devAddr = MutableStateFlow<String?>(null)
|
||||
override val devName = MutableStateFlow<String?>(null)
|
||||
|
||||
override fun setDevAddr(address: String?) {
|
||||
devAddr.value = address
|
||||
}
|
||||
|
||||
override fun setDevName(name: String?) {
|
||||
devName.value = name
|
||||
}
|
||||
}
|
||||
|
||||
class FakeMeshPrefs : MeshPrefs {
|
||||
override val deviceAddress = MutableStateFlow<String?>(null)
|
||||
|
||||
override fun setDeviceAddress(address: String?) {
|
||||
deviceAddress.value = address
|
||||
}
|
||||
|
||||
private val provideLocation = mutableMapOf<Int?, MutableStateFlow<Boolean>>()
|
||||
|
||||
override fun shouldProvideNodeLocation(nodeNum: Int?): StateFlow<Boolean> =
|
||||
provideLocation.getOrPut(nodeNum) { MutableStateFlow(true) }
|
||||
|
||||
override fun setShouldProvideNodeLocation(nodeNum: Int?, provide: Boolean) {
|
||||
provideLocation.getOrPut(nodeNum) { MutableStateFlow(provide) }.value = provide
|
||||
}
|
||||
|
||||
private val lastRequest = mutableMapOf<String?, MutableStateFlow<Int>>()
|
||||
|
||||
override fun getStoreForwardLastRequest(address: String?): StateFlow<Int> =
|
||||
lastRequest.getOrPut(address) { MutableStateFlow(0) }
|
||||
|
||||
override fun setStoreForwardLastRequest(address: String?, timestamp: Int) {
|
||||
lastRequest.getOrPut(address) { MutableStateFlow(timestamp) }.value = timestamp
|
||||
}
|
||||
}
|
||||
|
||||
class FakeAppPreferences : AppPreferences {
|
||||
override val analytics = FakeAnalyticsPrefs()
|
||||
override val homoglyph = FakeHomoglyphPrefs()
|
||||
override val filter = FakeFilterPrefs()
|
||||
override val meshLog = FakeMeshLogPrefs()
|
||||
override val emoji = FakeCustomEmojiPrefs()
|
||||
override val ui = FakeUiPrefs()
|
||||
override val map = FakeMapPrefs()
|
||||
override val mapConsent = FakeMapConsentPrefs()
|
||||
override val mapTileProvider = FakeMapTileProviderPrefs()
|
||||
override val radio = FakeRadioPrefs()
|
||||
override val mesh = FakeMeshPrefs()
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
/*
|
||||
* 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.CoroutineScope
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import org.meshtastic.core.ble.BleConnection
|
||||
import org.meshtastic.core.ble.BleConnectionFactory
|
||||
import org.meshtastic.core.ble.BleConnectionState
|
||||
import org.meshtastic.core.ble.BleDevice
|
||||
import org.meshtastic.core.ble.BleScanner
|
||||
import org.meshtastic.core.ble.BleService
|
||||
import org.meshtastic.core.ble.BleWriteType
|
||||
import org.meshtastic.core.ble.BluetoothRepository
|
||||
import org.meshtastic.core.ble.BluetoothState
|
||||
import kotlin.time.Duration
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
class FakeBleDevice(
|
||||
override val address: String,
|
||||
override val name: String? = "Fake Device",
|
||||
initialState: BleConnectionState = BleConnectionState.Disconnected,
|
||||
) : BaseFake(),
|
||||
BleDevice {
|
||||
private val _state = mutableStateFlow(initialState)
|
||||
override val state: StateFlow<BleConnectionState> = _state.asStateFlow()
|
||||
|
||||
private val _isBonded = mutableStateFlow(false)
|
||||
override val isBonded: Boolean
|
||||
get() = _isBonded.value
|
||||
|
||||
override val isConnected: Boolean
|
||||
get() = _state.value == BleConnectionState.Connected
|
||||
|
||||
override suspend fun readRssi(): Int = DEFAULT_RSSI
|
||||
|
||||
override suspend fun bond() {
|
||||
_isBonded.value = true
|
||||
}
|
||||
|
||||
fun setState(newState: BleConnectionState) {
|
||||
_state.value = newState
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_RSSI = -60
|
||||
}
|
||||
}
|
||||
|
||||
class FakeBleScanner :
|
||||
BaseFake(),
|
||||
BleScanner {
|
||||
private val foundDevices = mutableSharedFlow<BleDevice>(replay = 10)
|
||||
|
||||
override fun scan(timeout: Duration, serviceUuid: Uuid?, address: String?): Flow<BleDevice> = flow {
|
||||
emitAll(foundDevices)
|
||||
}
|
||||
|
||||
fun emitDevice(device: BleDevice) {
|
||||
foundDevices.tryEmit(device)
|
||||
}
|
||||
}
|
||||
|
||||
class FakeBleConnection :
|
||||
BaseFake(),
|
||||
BleConnection {
|
||||
private val _device = mutableStateFlow<BleDevice?>(null)
|
||||
override val device: BleDevice?
|
||||
get() = _device.value
|
||||
|
||||
private val _deviceFlow = mutableSharedFlow<BleDevice?>(replay = 1)
|
||||
override val deviceFlow: SharedFlow<BleDevice?> = _deviceFlow.asSharedFlow()
|
||||
|
||||
private val _connectionState = mutableSharedFlow<BleConnectionState>(replay = 1)
|
||||
override val connectionState: SharedFlow<BleConnectionState> = _connectionState.asSharedFlow()
|
||||
|
||||
override suspend fun connect(device: BleDevice) {
|
||||
_device.value = device
|
||||
_deviceFlow.emit(device)
|
||||
_connectionState.emit(BleConnectionState.Connecting)
|
||||
if (device is FakeBleDevice) {
|
||||
device.setState(BleConnectionState.Connecting)
|
||||
}
|
||||
_connectionState.emit(BleConnectionState.Connected)
|
||||
if (device is FakeBleDevice) {
|
||||
device.setState(BleConnectionState.Connected)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun connectAndAwait(
|
||||
device: BleDevice,
|
||||
timeoutMs: Long,
|
||||
onRegister: suspend () -> Unit,
|
||||
): BleConnectionState {
|
||||
connect(device)
|
||||
onRegister()
|
||||
return BleConnectionState.Connected
|
||||
}
|
||||
|
||||
override suspend fun disconnect() {
|
||||
val currentDevice = _device.value
|
||||
_connectionState.emit(BleConnectionState.Disconnected)
|
||||
if (currentDevice is FakeBleDevice) {
|
||||
currentDevice.setState(BleConnectionState.Disconnected)
|
||||
}
|
||||
_device.value = null
|
||||
_deviceFlow.emit(null)
|
||||
}
|
||||
|
||||
override suspend fun <T> profile(
|
||||
serviceUuid: Uuid,
|
||||
timeout: Duration,
|
||||
setup: suspend CoroutineScope.(BleService) -> T,
|
||||
): T = CoroutineScope(kotlinx.coroutines.Dispatchers.Unconfined).setup(FakeBleService())
|
||||
|
||||
override fun maximumWriteValueLength(writeType: BleWriteType): Int = 512
|
||||
}
|
||||
|
||||
class FakeBleService : BleService
|
||||
|
||||
class FakeBleConnectionFactory(private val fakeConnection: FakeBleConnection = FakeBleConnection()) :
|
||||
BleConnectionFactory {
|
||||
override fun create(scope: CoroutineScope, tag: String): BleConnection = fakeConnection
|
||||
}
|
||||
|
||||
@Suppress("EmptyFunctionBlock")
|
||||
class FakeBluetoothRepository :
|
||||
BaseFake(),
|
||||
BluetoothRepository {
|
||||
private val _state = mutableStateFlow(BluetoothState(hasPermissions = true, enabled = true))
|
||||
override val state: StateFlow<BluetoothState> = _state.asStateFlow()
|
||||
|
||||
override fun refreshState() {}
|
||||
|
||||
override fun isValid(bleAddress: String): Boolean = bleAddress.isNotBlank()
|
||||
|
||||
override fun isBonded(address: String): Boolean = _state.value.bondedDevices.any { it.address == address }
|
||||
|
||||
override suspend fun bond(device: BleDevice) {
|
||||
val currentState = _state.value
|
||||
if (!currentState.bondedDevices.contains(device)) {
|
||||
_state.value = currentState.copy(bondedDevices = currentState.bondedDevices + device)
|
||||
}
|
||||
}
|
||||
|
||||
fun setBluetoothEnabled(enabled: Boolean) {
|
||||
_state.value = _state.value.copy(enabled = enabled)
|
||||
}
|
||||
|
||||
fun setHasPermissions(hasPermissions: Boolean) {
|
||||
_state.value = _state.value.copy(hasPermissions = hasPermissions)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (c) 2025-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.StateFlow
|
||||
import org.meshtastic.core.common.database.DatabaseManager
|
||||
|
||||
/** A test double for [DatabaseManager] that provides a simple implementation and tracks calls. */
|
||||
class FakeDatabaseManager :
|
||||
BaseFake(),
|
||||
DatabaseManager {
|
||||
private val _cacheLimit = mutableStateFlow(DEFAULT_CACHE_LIMIT)
|
||||
override val cacheLimit: StateFlow<Int> = _cacheLimit
|
||||
|
||||
var lastSwitchedAddress: String? = null
|
||||
val existingDatabases = mutableSetOf<String>()
|
||||
|
||||
init {
|
||||
registerResetAction {
|
||||
_cacheLimit.value = DEFAULT_CACHE_LIMIT
|
||||
lastSwitchedAddress = null
|
||||
existingDatabases.clear()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCurrentCacheLimit(): Int = _cacheLimit.value
|
||||
|
||||
override fun setCacheLimit(limit: Int) {
|
||||
_cacheLimit.value = limit
|
||||
}
|
||||
|
||||
override suspend fun switchActiveDatabase(address: String?) {
|
||||
lastSwitchedAddress = address
|
||||
}
|
||||
|
||||
override fun hasDatabaseFor(address: String?): Boolean = address != null && existingDatabases.contains(address)
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_CACHE_LIMIT = 100
|
||||
}
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.testing
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.database.DatabaseProvider
|
||||
import org.meshtastic.core.database.MeshtasticDatabase
|
||||
import org.meshtastic.core.database.getInMemoryDatabaseBuilder
|
||||
|
||||
/** A real [DatabaseProvider] that uses an in-memory database for testing. */
|
||||
class FakeDatabaseProvider : DatabaseProvider {
|
||||
private val db: MeshtasticDatabase = getInMemoryDatabaseBuilder().build()
|
||||
private val _currentDb = MutableStateFlow(db)
|
||||
override val currentDb: StateFlow<MeshtasticDatabase> = _currentDb
|
||||
|
||||
override suspend fun <T> withDb(block: suspend (MeshtasticDatabase) -> T): T? = block(db)
|
||||
|
||||
fun close() {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* 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.StateFlow
|
||||
import org.meshtastic.core.datastore.LocalStatsDataSource
|
||||
import org.meshtastic.proto.LocalStats
|
||||
|
||||
/** A test double for [LocalStatsDataSource] that provides an in-memory implementation. */
|
||||
class FakeLocalStatsDataSource :
|
||||
BaseFake(),
|
||||
LocalStatsDataSource {
|
||||
private val _localStatsFlow = mutableStateFlow(LocalStats())
|
||||
override val localStatsFlow: StateFlow<LocalStats> = _localStatsFlow
|
||||
|
||||
override suspend fun setLocalStats(stats: LocalStats) {
|
||||
_localStatsFlow.value = stats
|
||||
}
|
||||
|
||||
override suspend fun clearLocalStats() {
|
||||
_localStatsFlow.value = LocalStats()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.repository.Location
|
||||
import org.meshtastic.core.repository.LocationRepository
|
||||
|
||||
/** A test double for [LocationRepository] that provides a manual location emission mechanism. */
|
||||
class FakeLocationRepository : LocationRepository {
|
||||
private val _receivingLocationUpdates = MutableStateFlow(false)
|
||||
override val receivingLocationUpdates: StateFlow<Boolean> = _receivingLocationUpdates
|
||||
|
||||
private val _locations = MutableSharedFlow<Location>(replay = 1)
|
||||
|
||||
override fun getLocations(): Flow<Location> = _locations
|
||||
|
||||
fun setReceivingLocationUpdates(receiving: Boolean) {
|
||||
_receivingLocationUpdates.value = receiving
|
||||
}
|
||||
|
||||
suspend fun emitLocation(location: Location) {
|
||||
_locations.emit(location)
|
||||
}
|
||||
}
|
||||
|
||||
/** Platform-specific factory for creating [Location] objects in tests. */
|
||||
expect fun createLocation(latitude: Double, longitude: Double, altitude: Double = 0.0): Location
|
||||
|
|
@ -16,18 +16,19 @@
|
|||
*/
|
||||
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)
|
||||
class FakeMeshLogPrefs :
|
||||
BaseFake(),
|
||||
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)
|
||||
private val _loggingEnabled = mutableStateFlow(true)
|
||||
override val loggingEnabled = _loggingEnabled
|
||||
|
||||
override fun setLoggingEnabled(enabled: Boolean) {
|
||||
|
|
|
|||
|
|
@ -26,13 +26,26 @@ import org.meshtastic.proto.MyNodeInfo
|
|||
import org.meshtastic.proto.PortNum
|
||||
import org.meshtastic.proto.Telemetry
|
||||
|
||||
/** A test double for [MeshLogRepository] that provides in-memory log storage. */
|
||||
@Suppress("TooManyFunctions")
|
||||
class FakeMeshLogRepository : MeshLogRepository {
|
||||
private val logsFlow = MutableStateFlow<List<MeshLog>>(emptyList())
|
||||
class FakeMeshLogRepository :
|
||||
BaseFake(),
|
||||
MeshLogRepository {
|
||||
private val logsFlow = mutableStateFlow<List<MeshLog>>(emptyList())
|
||||
val currentLogs: List<MeshLog>
|
||||
get() = logsFlow.value
|
||||
|
||||
var deleteLogsOlderThanCalledDays: Int? = null
|
||||
var lastDeletedOlderThan: Int? = null
|
||||
private set
|
||||
|
||||
var deleteAllCalled = false
|
||||
private set
|
||||
|
||||
override fun reset() {
|
||||
super.reset()
|
||||
lastDeletedOlderThan = null
|
||||
deleteAllCalled = false
|
||||
}
|
||||
|
||||
override fun getAllLogs(maxItem: Int): Flow<List<MeshLog>> = logsFlow.map { it.take(maxItem) }
|
||||
|
||||
|
|
@ -59,6 +72,7 @@ class FakeMeshLogRepository : MeshLogRepository {
|
|||
|
||||
override suspend fun deleteAll() {
|
||||
logsFlow.value = emptyList()
|
||||
deleteAllCalled = true
|
||||
}
|
||||
|
||||
override suspend fun deleteLog(uuid: String) {
|
||||
|
|
@ -70,7 +84,7 @@ class FakeMeshLogRepository : MeshLogRepository {
|
|||
}
|
||||
|
||||
override suspend fun deleteLogsOlderThan(retentionDays: Int) {
|
||||
deleteLogsOlderThanCalledDays = retentionDays
|
||||
lastDeletedOlderThan = retentionDays
|
||||
}
|
||||
|
||||
fun setLogs(logs: List<MeshLog>) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
/**
|
||||
* A container for all mesh-related fakes to simplify test setup.
|
||||
*
|
||||
* Instead of manually instantiating and wiring multiple fakes, you can use [FakeMeshService] to get a consistent set of
|
||||
* test doubles.
|
||||
*/
|
||||
class FakeMeshService {
|
||||
val nodeRepository = FakeNodeRepository()
|
||||
val serviceRepository = FakeServiceRepository()
|
||||
val radioController = FakeRadioController()
|
||||
val radioInterfaceService = FakeRadioInterfaceService()
|
||||
val notifications = FakeMeshServiceNotifications()
|
||||
val transport = FakeRadioTransport()
|
||||
val logRepository = FakeMeshLogRepository()
|
||||
val packetRepository = FakePacketRepository()
|
||||
val contactRepository = FakeContactRepository()
|
||||
val locationRepository = FakeLocationRepository()
|
||||
|
||||
// Add more as they are implemented
|
||||
}
|
||||
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.meshtastic.core.testing
|
||||
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.repository.MeshServiceNotifications
|
||||
import org.meshtastic.proto.ClientNotification
|
||||
import org.meshtastic.proto.Telemetry
|
||||
|
||||
/** A test double for [MeshServiceNotifications] that provides a no-op implementation. */
|
||||
@Suppress("TooManyFunctions", "EmptyFunctionBlock")
|
||||
class FakeMeshServiceNotifications : MeshServiceNotifications {
|
||||
override fun clearNotifications() {}
|
||||
|
||||
override fun initChannels() {}
|
||||
|
||||
override fun updateServiceStateNotification(
|
||||
state: org.meshtastic.core.model.ConnectionState,
|
||||
telemetry: Telemetry?,
|
||||
): Any = Any()
|
||||
|
||||
override suspend fun updateMessageNotification(
|
||||
contactKey: String,
|
||||
name: String,
|
||||
message: String,
|
||||
isBroadcast: Boolean,
|
||||
channelName: String?,
|
||||
isSilent: Boolean,
|
||||
) {}
|
||||
|
||||
override suspend fun updateWaypointNotification(
|
||||
contactKey: String,
|
||||
name: String,
|
||||
message: String,
|
||||
waypointId: Int,
|
||||
isSilent: Boolean,
|
||||
) {}
|
||||
|
||||
override suspend fun updateReactionNotification(
|
||||
contactKey: String,
|
||||
name: String,
|
||||
emoji: String,
|
||||
isBroadcast: Boolean,
|
||||
channelName: String?,
|
||||
isSilent: Boolean,
|
||||
) {}
|
||||
|
||||
override fun showAlertNotification(contactKey: String, name: String, alert: String) {}
|
||||
|
||||
override fun showNewNodeSeenNotification(node: Node) {}
|
||||
|
||||
override fun showOrUpdateLowBatteryNotification(node: Node, isRemote: Boolean) {}
|
||||
|
||||
override fun showClientNotification(clientNotification: ClientNotification) {}
|
||||
|
||||
override fun cancelMessageNotification(contactKey: String) {}
|
||||
|
||||
override fun cancelLowBatteryNotification(node: Node) {}
|
||||
|
||||
override fun clearClientNotification(notification: ClientNotification) {}
|
||||
}
|
||||
|
|
@ -41,21 +41,23 @@ import org.meshtastic.proto.User
|
|||
* ```
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class FakeNodeRepository : NodeRepository {
|
||||
class FakeNodeRepository :
|
||||
BaseFake(),
|
||||
NodeRepository {
|
||||
|
||||
private val _myNodeInfo = MutableStateFlow<MyNodeInfo?>(null)
|
||||
private val _myNodeInfo = mutableStateFlow<MyNodeInfo?>(null)
|
||||
override val myNodeInfo: StateFlow<MyNodeInfo?> = _myNodeInfo
|
||||
|
||||
private val _ourNodeInfo = MutableStateFlow<Node?>(null)
|
||||
private val _ourNodeInfo = mutableStateFlow<Node?>(null)
|
||||
override val ourNodeInfo: StateFlow<Node?> = _ourNodeInfo
|
||||
|
||||
private val _myId = MutableStateFlow<String?>(null)
|
||||
private val _myId = mutableStateFlow<String?>(null)
|
||||
override val myId: StateFlow<String?> = _myId
|
||||
|
||||
private val _localStats = MutableStateFlow(LocalStats())
|
||||
private val _localStats = mutableStateFlow(LocalStats())
|
||||
override val localStats: StateFlow<LocalStats> = _localStats
|
||||
|
||||
private val _nodeDBbyNum = MutableStateFlow<Map<Int, Node>>(emptyMap())
|
||||
private val _nodeDBbyNum = mutableStateFlow<Map<Int, Node>>(emptyMap())
|
||||
override val nodeDBbyNum: StateFlow<Map<Int, Node>> = _nodeDBbyNum
|
||||
|
||||
override val onlineNodeCount: Flow<Int> = _nodeDBbyNum.map { it.size }
|
||||
|
|
@ -82,18 +84,51 @@ class FakeNodeRepository : NodeRepository {
|
|||
onlyDirect: Boolean,
|
||||
): Flow<List<Node>> = _nodeDBbyNum.map { db ->
|
||||
db.values
|
||||
.asSequence()
|
||||
.filter { filterNode(it, filter, includeUnknown, onlyOnline, onlyDirect) }
|
||||
.toList()
|
||||
.let { nodes -> if (filter.isBlank()) nodes else nodes.filter { it.user.long_name.contains(filter) } }
|
||||
.sortedBy { it.num }
|
||||
.let { nodes ->
|
||||
when (sort) {
|
||||
NodeSortOption.ALPHABETICAL -> nodes.sortedBy { it.user.long_name.lowercase() }
|
||||
NodeSortOption.LAST_HEARD -> nodes.sortedByDescending { it.lastHeard }
|
||||
NodeSortOption.DISTANCE -> nodes.sortedBy { it.position.latitude_i } // Simplified
|
||||
NodeSortOption.HOPS_AWAY -> nodes.sortedBy { it.hopsAway }
|
||||
NodeSortOption.CHANNEL -> nodes.sortedBy { it.channel }
|
||||
NodeSortOption.VIA_MQTT -> nodes.sortedBy { if (it.viaMqtt) 0 else 1 }
|
||||
NodeSortOption.VIA_FAVORITE -> nodes.sortedBy { if (it.isFavorite) 0 else 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterNode(
|
||||
node: Node,
|
||||
filter: String,
|
||||
includeUnknown: Boolean,
|
||||
onlyOnline: Boolean,
|
||||
onlyDirect: Boolean,
|
||||
): Boolean {
|
||||
val matchesFilter =
|
||||
filter.isBlank() ||
|
||||
node.user.long_name.contains(filter, ignoreCase = true) ||
|
||||
node.user.id.contains(filter, ignoreCase = true)
|
||||
val matchesUnknown = includeUnknown || !node.isUnknownUser
|
||||
val matchesOnline = !onlyOnline || node.isOnline
|
||||
val matchesDirect = !onlyDirect || node.hopsAway == 0
|
||||
|
||||
return matchesFilter && matchesUnknown && matchesOnline && matchesDirect
|
||||
}
|
||||
|
||||
override suspend fun getNodesOlderThan(lastHeard: Int): List<Node> =
|
||||
_nodeDBbyNum.value.values.filter { it.lastHeard < lastHeard }
|
||||
|
||||
override suspend fun getUnknownNodes(): List<Node> = emptyList()
|
||||
override suspend fun getUnknownNodes(): List<Node> = _nodeDBbyNum.value.values.filter { it.isUnknownUser }
|
||||
|
||||
override suspend fun clearNodeDB(preserveFavorites: Boolean) {
|
||||
_nodeDBbyNum.value = emptyMap()
|
||||
if (preserveFavorites) {
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value.filter { it.value.isFavorite }
|
||||
} else {
|
||||
_nodeDBbyNum.value = emptyMap()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun clearMyNodeInfo() {
|
||||
|
|
@ -108,7 +143,10 @@ class FakeNodeRepository : NodeRepository {
|
|||
_nodeDBbyNum.value = _nodeDBbyNum.value - nodeNums.toSet()
|
||||
}
|
||||
|
||||
override suspend fun setNodeNotes(num: Int, notes: String) = Unit
|
||||
override suspend fun setNodeNotes(num: Int, notes: String) {
|
||||
val node = _nodeDBbyNum.value[num] ?: return
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value + (num to node.copy(notes = notes))
|
||||
}
|
||||
|
||||
override suspend fun upsert(node: Node) {
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value + (node.num to node)
|
||||
|
|
@ -119,7 +157,10 @@ class FakeNodeRepository : NodeRepository {
|
|||
_nodeDBbyNum.value = nodes.associateBy { it.num }
|
||||
}
|
||||
|
||||
override suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) = Unit
|
||||
override suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) {
|
||||
val node = _nodeDBbyNum.value[nodeNum] ?: return
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value + (nodeNum to node.copy(metadata = metadata))
|
||||
}
|
||||
|
||||
// --- Helper methods for testing ---
|
||||
|
||||
|
|
@ -134,4 +175,8 @@ class FakeNodeRepository : NodeRepository {
|
|||
fun setOurNode(node: Node?) {
|
||||
_ourNodeInfo.value = node
|
||||
}
|
||||
|
||||
fun setMyNodeInfo(info: MyNodeInfo?) {
|
||||
_myNodeInfo.value = info
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* 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.MutableStateFlow
|
||||
import org.meshtastic.core.repository.NotificationPrefs
|
||||
|
||||
class FakeNotificationPrefs : NotificationPrefs {
|
||||
override val messagesEnabled = MutableStateFlow(true)
|
||||
|
||||
override fun setMessagesEnabled(enabled: Boolean) {
|
||||
messagesEnabled.value = enabled
|
||||
}
|
||||
|
||||
override val nodeEventsEnabled = MutableStateFlow(true)
|
||||
|
||||
override fun setNodeEventsEnabled(enabled: Boolean) {
|
||||
nodeEventsEnabled.value = enabled
|
||||
}
|
||||
|
||||
override val lowBatteryEnabled = MutableStateFlow(true)
|
||||
|
||||
override fun setLowBatteryEnabled(enabled: Boolean) {
|
||||
lowBatteryEnabled.value = enabled
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@
|
|||
*/
|
||||
package org.meshtastic.core.testing
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
|
|
@ -25,34 +24,41 @@ import org.meshtastic.proto.ClientNotification
|
|||
|
||||
/**
|
||||
* A test double for [RadioController] that provides a no-op implementation and tracks calls for assertions in tests.
|
||||
*
|
||||
* Use this in place of mocking the entire RadioController interface when you need fine-grained control over connection
|
||||
* state and packet tracking.
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* val radioController = FakeRadioController()
|
||||
* radioController.setConnectionState(ConnectionState.Connected)
|
||||
* // ... perform test ...
|
||||
* assertEquals(1, radioController.sentPackets.size)
|
||||
* ```
|
||||
*/
|
||||
@Suppress("TooManyFunctions", "EmptyFunctionBlock")
|
||||
class FakeRadioController : RadioController {
|
||||
class FakeRadioController :
|
||||
BaseFake(),
|
||||
RadioController {
|
||||
|
||||
// Mutable state flows so we can manipulate them in our tests
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Connected)
|
||||
private val _connectionState = mutableStateFlow<ConnectionState>(ConnectionState.Connected)
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||
|
||||
private val _clientNotification = MutableStateFlow<ClientNotification?>(null)
|
||||
private val _clientNotification = mutableStateFlow<ClientNotification?>(null)
|
||||
override val clientNotification: StateFlow<ClientNotification?> = _clientNotification
|
||||
|
||||
// Track sent packets to assert in tests
|
||||
val sentPackets = mutableListOf<DataPacket>()
|
||||
val favoritedNodes = mutableListOf<Int>()
|
||||
val sentSharedContacts = mutableListOf<Int>()
|
||||
var throwOnSend: Boolean = false
|
||||
var lastSetDeviceAddress: String? = null
|
||||
var beginEditSettingsCalled = false
|
||||
var commitEditSettingsCalled = false
|
||||
var startProvideLocationCalled = false
|
||||
var stopProvideLocationCalled = false
|
||||
|
||||
init {
|
||||
registerResetAction {
|
||||
sentPackets.clear()
|
||||
favoritedNodes.clear()
|
||||
sentSharedContacts.clear()
|
||||
throwOnSend = false
|
||||
lastSetDeviceAddress = null
|
||||
beginEditSettingsCalled = false
|
||||
commitEditSettingsCalled = false
|
||||
startProvideLocationCalled = false
|
||||
stopProvideLocationCalled = false
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun sendMessage(packet: DataPacket) {
|
||||
if (throwOnSend) error("Fake send failure")
|
||||
|
|
@ -127,15 +133,23 @@ class FakeRadioController : RadioController {
|
|||
|
||||
override suspend fun requestNeighborInfo(requestId: Int, destNum: Int) {}
|
||||
|
||||
override suspend fun beginEditSettings(destNum: Int) {}
|
||||
override suspend fun beginEditSettings(destNum: Int) {
|
||||
beginEditSettingsCalled = true
|
||||
}
|
||||
|
||||
override suspend fun commitEditSettings(destNum: Int) {}
|
||||
override suspend fun commitEditSettings(destNum: Int) {
|
||||
commitEditSettingsCalled = true
|
||||
}
|
||||
|
||||
override fun getPacketId(): Int = 1
|
||||
|
||||
override fun startProvideLocation() {}
|
||||
override fun startProvideLocation() {
|
||||
startProvideLocationCalled = true
|
||||
}
|
||||
|
||||
override fun stopProvideLocation() {}
|
||||
override fun stopProvideLocation() {
|
||||
stopProvideLocationCalled = true
|
||||
}
|
||||
|
||||
override fun setDeviceAddress(address: String) {
|
||||
lastSetDeviceAddress = address
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* 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.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
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.DeviceType
|
||||
import org.meshtastic.core.model.InterfaceId
|
||||
import org.meshtastic.core.model.MeshActivity
|
||||
import org.meshtastic.core.repository.RadioInterfaceService
|
||||
|
||||
/** A test double for [RadioInterfaceService] that provides an in-memory implementation. */
|
||||
@Suppress("TooManyFunctions")
|
||||
class FakeRadioInterfaceService(override val serviceScope: CoroutineScope = MainScope()) : RadioInterfaceService {
|
||||
|
||||
override val supportedDeviceTypes: List<DeviceType> = emptyList()
|
||||
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||
|
||||
private val _currentDeviceAddressFlow = MutableStateFlow<String?>(null)
|
||||
override val currentDeviceAddressFlow: StateFlow<String?> = _currentDeviceAddressFlow
|
||||
|
||||
private val _receivedData = MutableSharedFlow<ByteArray>()
|
||||
override val receivedData: SharedFlow<ByteArray> = _receivedData
|
||||
|
||||
private val _meshActivity = MutableSharedFlow<MeshActivity>()
|
||||
override val meshActivity: SharedFlow<MeshActivity> = _meshActivity
|
||||
|
||||
val sentToRadio = mutableListOf<ByteArray>()
|
||||
var connectCalled = false
|
||||
|
||||
override fun isMockInterface(): Boolean = true
|
||||
|
||||
override fun sendToRadio(bytes: ByteArray) {
|
||||
sentToRadio.add(bytes)
|
||||
}
|
||||
|
||||
override fun connect() {
|
||||
connectCalled = true
|
||||
}
|
||||
|
||||
override fun getDeviceAddress(): String? = _currentDeviceAddressFlow.value
|
||||
|
||||
override fun setDeviceAddress(deviceAddr: String?): Boolean {
|
||||
_currentDeviceAddressFlow.value = deviceAddr
|
||||
return true
|
||||
}
|
||||
|
||||
override fun toInterfaceAddress(interfaceId: InterfaceId, rest: String): String = "$interfaceId:$rest"
|
||||
|
||||
override fun onConnect() {
|
||||
_connectionState.value = ConnectionState.Connected
|
||||
}
|
||||
|
||||
override fun onDisconnect(isPermanent: Boolean, errorMessage: String?) {
|
||||
_connectionState.value = ConnectionState.Disconnected
|
||||
}
|
||||
|
||||
override fun handleFromRadio(bytes: ByteArray) {
|
||||
// In a real implementation, this would emit to receivedData
|
||||
}
|
||||
|
||||
// --- Helper methods for testing ---
|
||||
|
||||
suspend fun emitFromRadio(bytes: ByteArray) {
|
||||
_receivedData.emit(bytes)
|
||||
}
|
||||
|
||||
fun setConnectionState(state: ConnectionState) {
|
||||
_connectionState.value = state
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.repository.RadioTransport
|
||||
|
||||
/** A test double for [RadioTransport] that tracks sent data. */
|
||||
class FakeRadioTransport : RadioTransport {
|
||||
val sentData = mutableListOf<ByteArray>()
|
||||
var closeCalled = false
|
||||
var keepAliveCalled = false
|
||||
|
||||
override fun handleSendToRadio(p: ByteArray) {
|
||||
sentData.add(p)
|
||||
}
|
||||
|
||||
override fun keepAlive() {
|
||||
keepAliveCalled = true
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
closeCalled = true
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,7 @@
|
|||
package org.meshtastic.core.testing
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
|
|
@ -36,6 +37,7 @@ object TestDataFactory {
|
|||
* @param longName User long name (default: "Test User")
|
||||
* @param shortName User short name (default: "T")
|
||||
* @param lastHeard Last heard timestamp in seconds (default: 0)
|
||||
* @param hwModel Hardware model (default: UNSET)
|
||||
* @return A Node instance with provided or default values
|
||||
*/
|
||||
fun createTestNode(
|
||||
|
|
@ -44,18 +46,31 @@ object TestDataFactory {
|
|||
longName: String = "Test User",
|
||||
shortName: String = "T",
|
||||
lastHeard: Int = 0,
|
||||
hwModel: org.meshtastic.proto.HardwareModel = org.meshtastic.proto.HardwareModel.UNSET,
|
||||
batteryLevel: Int? = 100,
|
||||
): Node {
|
||||
val user = User(id = userId, long_name = longName, short_name = shortName)
|
||||
return Node(num = num, user = user, lastHeard = lastHeard, snr = 0f, rssi = 0, channel = 0)
|
||||
val user = User(id = userId, long_name = longName, short_name = shortName, hw_model = hwModel)
|
||||
val metrics = org.meshtastic.proto.DeviceMetrics(battery_level = batteryLevel)
|
||||
return Node(
|
||||
num = num,
|
||||
user = user,
|
||||
lastHeard = lastHeard,
|
||||
snr = 0f,
|
||||
rssi = 0,
|
||||
channel = 0,
|
||||
deviceMetrics = metrics,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates multiple test nodes with sequential IDs.
|
||||
*
|
||||
* @param count Number of nodes to create
|
||||
* @param baseNum Starting node number (default: 1)
|
||||
* @return A list of Node instances
|
||||
*/
|
||||
/** Creates a test [org.meshtastic.proto.MeshPacket] with default values. */
|
||||
fun createTestPacket(
|
||||
from: Int = 1,
|
||||
to: Int = 0xffffffff.toInt(),
|
||||
decoded: org.meshtastic.proto.Data? = null,
|
||||
relayNode: Int = 0,
|
||||
) = org.meshtastic.proto.MeshPacket(from = from, to = to, decoded = decoded, relay_node = relayNode)
|
||||
|
||||
/** Creates multiple test nodes with sequential IDs. */
|
||||
fun createTestNodes(count: Int, baseNum: Int = 1): List<Node> = (0 until count).map { i ->
|
||||
createTestNode(
|
||||
num = baseNum + i,
|
||||
|
|
@ -64,6 +79,32 @@ object TestDataFactory {
|
|||
shortName = "T$i",
|
||||
)
|
||||
}
|
||||
|
||||
/** Creates a test [MyNodeInfo] with default values. */
|
||||
fun createMyNodeInfo(
|
||||
myNodeNum: Int = 1,
|
||||
hasGPS: Boolean = false,
|
||||
model: String? = "TBEAM",
|
||||
firmwareVersion: String? = "2.5.0",
|
||||
hasWifi: Boolean = false,
|
||||
pioEnv: String? = null,
|
||||
) = MyNodeInfo(
|
||||
myNodeNum = myNodeNum,
|
||||
hasGPS = hasGPS,
|
||||
model = model,
|
||||
firmwareVersion = firmwareVersion,
|
||||
couldUpdate = false,
|
||||
shouldUpdate = false,
|
||||
currentPacketId = 1L,
|
||||
messageTimeoutMsec = 300000,
|
||||
minAppVersion = 1,
|
||||
maxChannels = 8,
|
||||
hasWifi = hasWifi,
|
||||
channelUtilization = 0f,
|
||||
airUtilTx = 0f,
|
||||
deviceId = "!$myNodeNum",
|
||||
pioEnv = pioEnv,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
/** Initializes platform-specific test context (e.g., Robolectric on Android). */
|
||||
expect fun setupTestContext()
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* 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.test.runTest
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
import org.meshtastic.proto.User
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
class FakeNodeRepositoryTest {
|
||||
|
||||
private val repository = FakeNodeRepository()
|
||||
|
||||
@Test
|
||||
fun `getNodes sorting by name`() = runTest {
|
||||
val nodes =
|
||||
listOf(
|
||||
Node(num = 1, user = User(long_name = "Charlie")),
|
||||
Node(num = 2, user = User(long_name = "Alice")),
|
||||
Node(num = 3, user = User(long_name = "Bob")),
|
||||
)
|
||||
repository.setNodes(nodes)
|
||||
|
||||
repository.getNodes(sort = NodeSortOption.ALPHABETICAL).test {
|
||||
val result = awaitItem()
|
||||
assertEquals("Alice", result[0].user.long_name)
|
||||
assertEquals("Bob", result[1].user.long_name)
|
||||
assertEquals("Charlie", result[2].user.long_name)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getUnknownNodes returns nodes with UNSET hw_model`() = runTest {
|
||||
val node1 = Node(num = 1, user = User(hw_model = org.meshtastic.proto.HardwareModel.UNSET))
|
||||
val node2 = Node(num = 2, user = User(hw_model = org.meshtastic.proto.HardwareModel.TLORA_V2))
|
||||
repository.setNodes(listOf(node1, node2))
|
||||
|
||||
val result = repository.getUnknownNodes()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(1, result[0].num)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getNodes filtering by onlyOnline`() = runTest {
|
||||
val node1 = Node(num = 1, lastHeard = 2000000000) // Online
|
||||
val node2 = Node(num = 2, lastHeard = 0) // Offline
|
||||
repository.setNodes(listOf(node1, node2))
|
||||
|
||||
repository.getNodes(onlyOnline = true).test {
|
||||
val result = awaitItem()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(1, result[0].num)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getNodes filtering by onlyDirect`() = runTest {
|
||||
val node1 = Node(num = 1, hopsAway = 0) // Direct
|
||||
val node2 = Node(num = 2, hopsAway = 1) // Indirect
|
||||
repository.setNodes(listOf(node1, node2))
|
||||
|
||||
repository.getNodes(onlyDirect = true).test {
|
||||
val result = awaitItem()
|
||||
assertEquals(1, result.size)
|
||||
assertEquals(1, result[0].num)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `insertMetadata updates node metadata`() = runTest {
|
||||
val nodeNum = 1234
|
||||
repository.upsert(Node(num = nodeNum))
|
||||
val metadata = org.meshtastic.proto.DeviceMetadata(firmware_version = "2.5.0")
|
||||
repository.insertMetadata(nodeNum, metadata)
|
||||
|
||||
val node = repository.nodeDBbyNum.value[nodeNum]
|
||||
assertEquals("2.5.0", node?.metadata?.firmware_version)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `deleteNodes removes multiple nodes`() = runTest {
|
||||
repository.setNodes(listOf(Node(num = 1), Node(num = 2), Node(num = 3)))
|
||||
repository.deleteNodes(listOf(1, 2))
|
||||
|
||||
assertEquals(1, repository.nodeDBbyNum.value.size)
|
||||
assertTrue(repository.nodeDBbyNum.value.containsKey(3))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reset clears all state`() = runTest {
|
||||
repository.setNodes(listOf(Node(num = 1)))
|
||||
repository.setMyId("my-id")
|
||||
repository.setNodeNotes(1, "note")
|
||||
|
||||
repository.reset()
|
||||
|
||||
assertTrue(repository.nodeDBbyNum.value.isEmpty())
|
||||
assertEquals(null, repository.myId.value)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `setNodeNotes persists notes`() = runTest {
|
||||
val nodeNum = 1234
|
||||
repository.upsert(Node(num = nodeNum))
|
||||
repository.setNodeNotes(nodeNum, "My Note")
|
||||
|
||||
val node = repository.nodeDBbyNum.value[nodeNum]
|
||||
assertEquals("My Note", node?.notes)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clearNodeDB preserves favorites`() = runTest {
|
||||
val node1 = Node(num = 1, isFavorite = true)
|
||||
val node2 = Node(num = 2, isFavorite = false)
|
||||
repository.setNodes(listOf(node1, node2))
|
||||
|
||||
repository.clearNodeDB(preserveFavorites = true)
|
||||
|
||||
assertEquals(1, repository.nodeDBbyNum.value.size)
|
||||
assertTrue(repository.nodeDBbyNum.value.containsKey(1))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.repository.Location
|
||||
|
||||
/** Creates a placeholder iOS [Location] for testing. */
|
||||
actual fun createLocation(latitude: Double, longitude: Double, altitude: Double): Location = Location()
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
actual fun setupTestContext() {}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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.repository.Location
|
||||
|
||||
/** Creates a placeholder JVM [Location] for testing. */
|
||||
actual fun createLocation(latitude: Double, longitude: Double, altitude: Double): Location = Location()
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* 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
|
||||
|
||||
@Suppress("EmptyFunctionBlock")
|
||||
actual fun setupTestContext() {}
|
||||
Loading…
Add table
Add a link
Reference in a new issue