mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat: introduce Desktop target and expand Kotlin Multiplatform (KMP) architecture (#4761)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
f4364cff9a
commit
ac6bb5479b
386 changed files with 17089 additions and 4590 deletions
188
core/testing/README.md
Normal file
188
core/testing/README.md
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
# `:core:testing` — Shared Test Doubles and Utilities
|
||||
|
||||
## Purpose
|
||||
|
||||
The `:core:testing` module provides lightweight, reusable test doubles (fakes, builders, factories) and testing utilities for **all** KMP modules. This module **consolidates testing dependencies** into a single, well-controlled location to:
|
||||
|
||||
- **Reduce duplication**: Shared fakes (e.g., `FakeNodeRepository`, `FakeRadioController`) used across multiple modules.
|
||||
- **Keep dependency graph clean**: All test doubles and libraries are defined once; modules depend on `:core:testing` instead of scattered test deps.
|
||||
- **Enable KMP-wide test patterns**: Every module (`commonTest`, `androidUnitTest`, JVM tests) can reuse the same fakes.
|
||||
- **Maintain purity**: Core business logic modules (e.g., `core:domain`, `core:data`) depend on `:core:testing` via `commonTest`, avoiding test-code leakage into production.
|
||||
|
||||
## Dependency Strategy
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ core:testing │
|
||||
│ (only deps: core:model, │
|
||||
│ core:repository, test libs) │
|
||||
└──────────────┬──────────────────────┘
|
||||
↑
|
||||
│ (commonTest dependency)
|
||||
┌──────┴─────────────┬────────────────────┐
|
||||
│ │ │
|
||||
core:domain feature:messaging feature:node
|
||||
core:data feature:settings feature:firmware
|
||||
(etc.) (etc.)
|
||||
```
|
||||
|
||||
### Key Design Rules
|
||||
|
||||
1. **`:core:testing` has NO dependencies on heavy modules**: It only depends on:
|
||||
- `core:model` — Domain types (Node, User, etc.)
|
||||
- `core:repository` — Interfaces (NodeRepository, etc.)
|
||||
- Test libraries (`kotlin("test")`, `mockk`, `kotlinx.coroutines.test`, `turbine`, `junit`)
|
||||
|
||||
2. **No circular dependencies**: Modules that depend on `:core:testing` (in `commonTest`) cannot be dependencies of `:core:testing` itself.
|
||||
|
||||
3. **`:core:testing` is NOT part of the app bundle**: It's declared in `commonTest` sourceSet only, so it never appears in release APKs or final JARs.
|
||||
|
||||
## What's Included
|
||||
|
||||
### Test Doubles (Fakes)
|
||||
|
||||
#### `FakeRadioController`
|
||||
A no-op implementation of `RadioController` for unit tests. Tracks method calls and state changes.
|
||||
|
||||
```kotlin
|
||||
val radioController = FakeRadioController()
|
||||
radioController.setConnectionState(ConnectionState.Connected)
|
||||
assertEquals(1, radioController.sentPackets.size)
|
||||
```
|
||||
|
||||
#### `FakeNodeRepository`
|
||||
An in-memory implementation of `NodeRepository` for isolated testing.
|
||||
|
||||
```kotlin
|
||||
val nodeRepo = FakeNodeRepository()
|
||||
nodeRepo.setNodes(TestDataFactory.createTestNodes(5))
|
||||
assertEquals(5, nodeRepo.nodeDBbyNum.value.size)
|
||||
```
|
||||
|
||||
### Test Builders & Factories
|
||||
|
||||
#### `TestDataFactory`
|
||||
Factory methods for creating domain objects with sensible defaults.
|
||||
|
||||
```kotlin
|
||||
val node = TestDataFactory.createTestNode(num = 42, longName = "Alice")
|
||||
val nodes = TestDataFactory.createTestNodes(10)
|
||||
```
|
||||
|
||||
### Test Utilities
|
||||
|
||||
#### Flow collection helper
|
||||
```kotlin
|
||||
val emissions = flow { emit(1); emit(2) }.toList()
|
||||
assertEquals(listOf(1, 2), emissions)
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Testing a ViewModel (in `feature:messaging/src/commonTest`)
|
||||
|
||||
```kotlin
|
||||
class MessageViewModelTest {
|
||||
private val nodeRepository = FakeNodeRepository()
|
||||
|
||||
@Test
|
||||
fun testLoadsNodesCorrectly() = runTest {
|
||||
nodeRepository.setNodes(TestDataFactory.createTestNodes(3))
|
||||
val viewModel = createViewModel(nodeRepository)
|
||||
assertEquals(3, viewModel.nodeCount.value)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testing a UseCase (in `core:domain/src/commonTest`)
|
||||
|
||||
```kotlin
|
||||
class SendMessageUseCaseTest {
|
||||
private val radioController = FakeRadioController()
|
||||
|
||||
@Test
|
||||
fun testSendsPacket() = runTest {
|
||||
val useCase = SendMessageUseCase(radioController)
|
||||
useCase.sendMessage(testPacket)
|
||||
assertEquals(1, radioController.sentPackets.size)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Adding New Test Doubles
|
||||
|
||||
When adding a new fake to `:core:testing`:
|
||||
|
||||
1. **Implement the interface** from `core:model` or `core:repository`.
|
||||
2. **Track side effects** (e.g., `sentPackets`, `calledMethods`) for test assertions.
|
||||
3. **Provide test helpers** (e.g., `setNodes()`, `clear()`) to manipulate state.
|
||||
4. **Document with examples** in the class KDoc.
|
||||
|
||||
Example:
|
||||
|
||||
```kotlin
|
||||
/**
|
||||
* A test double for [SomeRepository].
|
||||
*/
|
||||
class FakeSomeRepository : SomeRepository {
|
||||
val callHistory = mutableListOf<String>()
|
||||
|
||||
override suspend fun doSomething(value: String) {
|
||||
callHistory.add(value)
|
||||
}
|
||||
|
||||
// Test helpers
|
||||
fun getCallCount() = callHistory.size
|
||||
fun clear() = callHistory.clear()
|
||||
}
|
||||
```
|
||||
|
||||
## Dependency Maintenance
|
||||
|
||||
### When adding a new module:
|
||||
- If it has `commonTest` tests, add `implementation(projects.core.testing)` to its `commonTest.dependencies`.
|
||||
- Do NOT add heavy modules (e.g., `core:database`) to `:core:testing`'s dependencies.
|
||||
|
||||
### When a test needs a mock:
|
||||
- Check `:core:testing` first for an existing fake.
|
||||
- If none exists, consider adding it there (if it's reusable) vs. using `mockk()` inline.
|
||||
|
||||
### When updating interfaces:
|
||||
- Update corresponding fakes in `:core:testing` to match new method signatures.
|
||||
- Keep fakes no-op; don't replicate business logic.
|
||||
|
||||
## Files
|
||||
|
||||
```
|
||||
core/testing/
|
||||
├── build.gradle.kts # Lightweight, minimal dependencies
|
||||
├── README.md # This file
|
||||
└── src/commonMain/kotlin/org/meshtastic/core/testing/
|
||||
├── FakeRadioController.kt # RadioController test double
|
||||
├── FakeNodeRepository.kt # NodeRepository test double
|
||||
└── TestDataFactory.kt # Builders and factories
|
||||
```
|
||||
|
||||
## See Also
|
||||
|
||||
- `AGENTS.md` §3B: KMP platform purity guidelines (relevant for test code).
|
||||
- `docs/kmp-status.md`: KMP module status and targets.
|
||||
- `.github/copilot-instructions.md`: Build and test commands.
|
||||
|
||||
45
core/testing/build.gradle.kts
Normal file
45
core/testing/build.gradle.kts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
plugins { alias(libs.plugins.meshtastic.kmp.library) }
|
||||
|
||||
kotlin {
|
||||
jvm()
|
||||
|
||||
@Suppress("UnstableApiUsage")
|
||||
android {
|
||||
namespace = "org.meshtastic.core.testing"
|
||||
androidResources.enable = false
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
// Core KMP models and contracts for creating test fakes
|
||||
// NOTE: Only api() core:model and core:repository to keep dependency graph clean.
|
||||
// Heavy modules (database, data, domain) should depend on core:testing, not vice versa.
|
||||
api(projects.core.model)
|
||||
api(projects.core.repository)
|
||||
|
||||
// Testing libraries - these are public API for all test consumers
|
||||
api(kotlin("test"))
|
||||
api(libs.mockk)
|
||||
api(libs.kotlinx.coroutines.test)
|
||||
api(libs.turbine)
|
||||
api(libs.junit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
|
||||
/**
|
||||
* A test double for message/packet repository operations.
|
||||
*
|
||||
* Tracks sent packets and provides test helpers for messaging scenarios.
|
||||
*/
|
||||
class FakePacketRepository {
|
||||
val sentPackets = mutableListOf<DataPacket>()
|
||||
private val _packetsFlow = MutableStateFlow<List<DataPacket>>(emptyList())
|
||||
val packetsFlow: Flow<List<DataPacket>> = _packetsFlow
|
||||
|
||||
suspend fun sendPacket(packet: DataPacket) {
|
||||
sentPackets.add(packet)
|
||||
_packetsFlow.value = sentPackets.toList()
|
||||
}
|
||||
|
||||
fun getPacketCount() = sentPackets.size
|
||||
|
||||
fun clear() {
|
||||
sentPackets.clear()
|
||||
_packetsFlow.value = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A test double for contact management operations.
|
||||
*
|
||||
* Maintains a list of contacts and provides helpers for contact-related tests.
|
||||
*/
|
||||
class FakeContactRepository {
|
||||
data class Contact(val userId: String, val name: String, val lastMessageTime: Long = 0)
|
||||
|
||||
private val contacts = mutableMapOf<String, Contact>()
|
||||
private val _contactsFlow = MutableStateFlow<List<Contact>>(emptyList())
|
||||
val contactsFlow: Flow<List<Contact>> = _contactsFlow
|
||||
|
||||
suspend fun addContact(contact: Contact) {
|
||||
contacts[contact.userId] = contact
|
||||
_contactsFlow.value = contacts.values.toList()
|
||||
}
|
||||
|
||||
suspend fun removeContact(userId: String) {
|
||||
contacts.remove(userId)
|
||||
_contactsFlow.value = contacts.values.toList()
|
||||
}
|
||||
|
||||
suspend fun getContact(userId: String): Contact? = contacts[userId]
|
||||
|
||||
suspend fun updateContactLastMessage(userId: String, time: Long) {
|
||||
contacts[userId]?.let { existing ->
|
||||
contacts[userId] = existing.copy(lastMessageTime = time)
|
||||
_contactsFlow.value = contacts.values.toList()
|
||||
}
|
||||
}
|
||||
|
||||
fun getContactCount() = contacts.size
|
||||
|
||||
fun getAllContacts() = contacts.values.toList()
|
||||
|
||||
fun clear() {
|
||||
contacts.clear()
|
||||
_contactsFlow.value = emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
/** Test helper for creating test contact objects. */
|
||||
fun createTestContact(
|
||||
userId: String = "!test001",
|
||||
name: String = "Test Contact",
|
||||
lastMessageTime: Long = 0,
|
||||
): FakeContactRepository.Contact =
|
||||
FakeContactRepository.Contact(userId = userId, name = name, lastMessageTime = lastMessageTime)
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* 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.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import org.meshtastic.core.model.MyNodeInfo
|
||||
import org.meshtastic.core.model.Node
|
||||
import org.meshtastic.core.model.NodeSortOption
|
||||
import org.meshtastic.core.repository.NodeRepository
|
||||
import org.meshtastic.proto.DeviceMetadata
|
||||
import org.meshtastic.proto.LocalStats
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
/**
|
||||
* A test double for [NodeRepository] that provides an in-memory implementation.
|
||||
*
|
||||
* Tracks node operations and exposes mutable state for assertions in tests.
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* val nodeRepository = FakeNodeRepository()
|
||||
* nodeRepository.setNodes(TestDataFactory.createTestNodes(3))
|
||||
* assertEquals(3, nodeRepository.nodeDBbyNum.value.size)
|
||||
* ```
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class FakeNodeRepository : NodeRepository {
|
||||
|
||||
private val _myNodeInfo = MutableStateFlow<MyNodeInfo?>(null)
|
||||
override val myNodeInfo: StateFlow<MyNodeInfo?> = _myNodeInfo
|
||||
|
||||
private val _ourNodeInfo = MutableStateFlow<Node?>(null)
|
||||
override val ourNodeInfo: StateFlow<Node?> = _ourNodeInfo
|
||||
|
||||
private val _myId = MutableStateFlow<String?>(null)
|
||||
override val myId: StateFlow<String?> = _myId
|
||||
|
||||
private val _localStats = MutableStateFlow(LocalStats())
|
||||
override val localStats: StateFlow<LocalStats> = _localStats
|
||||
|
||||
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 }
|
||||
override val totalNodeCount: Flow<Int> = _nodeDBbyNum.map { it.size }
|
||||
|
||||
override fun updateLocalStats(stats: LocalStats) {
|
||||
_localStats.value = stats
|
||||
}
|
||||
|
||||
override fun effectiveLogNodeId(nodeNum: Int): Flow<Int> = MutableStateFlow(0)
|
||||
|
||||
override fun getNode(userId: String): Node =
|
||||
_nodeDBbyNum.value.values.find { it.user.id == userId } ?: Node(num = 0, user = User(id = userId))
|
||||
|
||||
override fun getUser(nodeNum: Int): User = _nodeDBbyNum.value[nodeNum]?.user ?: User()
|
||||
|
||||
override fun getUser(userId: String): User = _nodeDBbyNum.value.values.find { it.user.id == userId }?.user ?: User()
|
||||
|
||||
override fun getNodes(
|
||||
sort: NodeSortOption,
|
||||
filter: String,
|
||||
includeUnknown: Boolean,
|
||||
onlyOnline: Boolean,
|
||||
onlyDirect: Boolean,
|
||||
): Flow<List<Node>> = _nodeDBbyNum.map { db ->
|
||||
db.values
|
||||
.toList()
|
||||
.let { nodes -> if (filter.isBlank()) nodes else nodes.filter { it.user.long_name.contains(filter) } }
|
||||
.sortedBy { it.num }
|
||||
}
|
||||
|
||||
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 clearNodeDB(preserveFavorites: Boolean) {
|
||||
_nodeDBbyNum.value = emptyMap()
|
||||
}
|
||||
|
||||
override suspend fun clearMyNodeInfo() {
|
||||
_myNodeInfo.value = null
|
||||
}
|
||||
|
||||
override suspend fun deleteNode(num: Int) {
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value - num
|
||||
}
|
||||
|
||||
override suspend fun deleteNodes(nodeNums: List<Int>) {
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value - nodeNums.toSet()
|
||||
}
|
||||
|
||||
override suspend fun setNodeNotes(num: Int, notes: String) = Unit
|
||||
|
||||
override suspend fun upsert(node: Node) {
|
||||
_nodeDBbyNum.value = _nodeDBbyNum.value + (node.num to node)
|
||||
}
|
||||
|
||||
override suspend fun installConfig(mi: MyNodeInfo, nodes: List<Node>) {
|
||||
_myNodeInfo.value = mi
|
||||
_nodeDBbyNum.value = nodes.associateBy { it.num }
|
||||
}
|
||||
|
||||
override suspend fun insertMetadata(nodeNum: Int, metadata: DeviceMetadata) = Unit
|
||||
|
||||
// --- Helper methods for testing ---
|
||||
|
||||
fun setNodes(nodes: List<Node>) {
|
||||
_nodeDBbyNum.value = nodes.associateBy { it.num }
|
||||
}
|
||||
|
||||
fun setMyId(id: String) {
|
||||
_myId.value = id
|
||||
}
|
||||
|
||||
fun setOurNode(node: Node?) {
|
||||
_ourNodeInfo.value = node
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.model.ConnectionState
|
||||
import org.meshtastic.core.model.DataPacket
|
||||
import org.meshtastic.core.model.RadioController
|
||||
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 {
|
||||
|
||||
// Mutable state flows so we can manipulate them in our tests
|
||||
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Connected)
|
||||
override val connectionState: StateFlow<ConnectionState> = _connectionState
|
||||
|
||||
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>()
|
||||
|
||||
override suspend fun sendMessage(packet: DataPacket) {
|
||||
sentPackets.add(packet)
|
||||
}
|
||||
|
||||
override fun clearClientNotification() {
|
||||
_clientNotification.value = null
|
||||
}
|
||||
|
||||
override suspend fun favoriteNode(nodeNum: Int) {
|
||||
favoritedNodes.add(nodeNum)
|
||||
}
|
||||
|
||||
override suspend fun sendSharedContact(nodeNum: Int) {
|
||||
sentSharedContacts.add(nodeNum)
|
||||
}
|
||||
|
||||
override suspend fun setLocalConfig(config: org.meshtastic.proto.Config) {}
|
||||
|
||||
override suspend fun setLocalChannel(channel: org.meshtastic.proto.Channel) {}
|
||||
|
||||
override suspend fun setOwner(destNum: Int, user: org.meshtastic.proto.User, packetId: Int) {}
|
||||
|
||||
override suspend fun setConfig(destNum: Int, config: org.meshtastic.proto.Config, packetId: Int) {}
|
||||
|
||||
override suspend fun setModuleConfig(destNum: Int, config: org.meshtastic.proto.ModuleConfig, packetId: Int) {}
|
||||
|
||||
override suspend fun setRemoteChannel(destNum: Int, channel: org.meshtastic.proto.Channel, packetId: Int) {}
|
||||
|
||||
override suspend fun setFixedPosition(destNum: Int, position: org.meshtastic.core.model.Position) {}
|
||||
|
||||
override suspend fun setRingtone(destNum: Int, ringtone: String) {}
|
||||
|
||||
override suspend fun setCannedMessages(destNum: Int, messages: String) {}
|
||||
|
||||
override suspend fun getOwner(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getConfig(destNum: Int, configType: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getModuleConfig(destNum: Int, moduleConfigType: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getChannel(destNum: Int, index: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getRingtone(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getCannedMessages(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun getDeviceConnectionStatus(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun reboot(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun rebootToDfu(nodeNum: Int) {}
|
||||
|
||||
override suspend fun requestRebootOta(requestId: Int, destNum: Int, mode: Int, hash: ByteArray?) {}
|
||||
|
||||
override suspend fun shutdown(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun factoryReset(destNum: Int, packetId: Int) {}
|
||||
|
||||
override suspend fun nodedbReset(destNum: Int, packetId: Int, preserveFavorites: Boolean) {}
|
||||
|
||||
override suspend fun removeByNodenum(packetId: Int, nodeNum: Int) {}
|
||||
|
||||
override suspend fun requestPosition(destNum: Int, currentPosition: org.meshtastic.core.model.Position) {}
|
||||
|
||||
override suspend fun requestUserInfo(destNum: Int) {}
|
||||
|
||||
override suspend fun requestTraceroute(requestId: Int, destNum: Int) {}
|
||||
|
||||
override suspend fun requestTelemetry(requestId: Int, destNum: Int, typeValue: Int) {}
|
||||
|
||||
override suspend fun requestNeighborInfo(requestId: Int, destNum: Int) {}
|
||||
|
||||
override suspend fun beginEditSettings(destNum: Int) {}
|
||||
|
||||
override suspend fun commitEditSettings(destNum: Int) {}
|
||||
|
||||
override fun getPacketId(): Int = 1
|
||||
|
||||
override fun startProvideLocation() {}
|
||||
|
||||
override fun stopProvideLocation() {}
|
||||
|
||||
override fun setDeviceAddress(address: String) {}
|
||||
|
||||
// --- Helper methods for testing ---
|
||||
|
||||
fun setConnectionState(state: ConnectionState) {
|
||||
_connectionState.value = state
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.model.Node
|
||||
import org.meshtastic.proto.User
|
||||
|
||||
/**
|
||||
* Factory for creating test domain objects.
|
||||
*
|
||||
* Provides sensible defaults that can be overridden for specific test needs.
|
||||
*/
|
||||
@Suppress("MagicNumber") // test data padding
|
||||
object TestDataFactory {
|
||||
|
||||
/**
|
||||
* Creates a test [Node] with default values.
|
||||
*
|
||||
* @param num Node number (default: 1)
|
||||
* @param userId User ID in hex format (default: "!test0001")
|
||||
* @param longName User long name (default: "Test User")
|
||||
* @param shortName User short name (default: "T")
|
||||
* @param lastHeard Last heard timestamp in seconds (default: 0)
|
||||
* @return A Node instance with provided or default values
|
||||
*/
|
||||
fun createTestNode(
|
||||
num: Int = 1,
|
||||
userId: String = "!test0001",
|
||||
longName: String = "Test User",
|
||||
shortName: String = "T",
|
||||
lastHeard: Int = 0,
|
||||
): 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
fun createTestNodes(count: Int, baseNum: Int = 1): List<Node> = (0 until count).map { i ->
|
||||
createTestNode(
|
||||
num = baseNum + i,
|
||||
userId = "!test${(baseNum + i).toString().padStart(4, '0')}",
|
||||
longName = "Test User $i",
|
||||
shortName = "T$i",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collects all emissions from a Flow into a list.
|
||||
*
|
||||
* Useful for asserting on Flow values in tests.
|
||||
*
|
||||
* Example:
|
||||
* ```kotlin
|
||||
* val values = flow { emit(1); emit(2) }.toList()
|
||||
* assertEquals(listOf(1, 2), values)
|
||||
* ```
|
||||
*/
|
||||
suspend inline fun <T> Flow<T>.toList(): List<T> {
|
||||
val result = mutableListOf<T>()
|
||||
collect { result.add(it) }
|
||||
return result
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue