Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
8bdd673
test(coverage): Record current project test coverage baseline
jamesarich Mar 19, 2026
6b9a8c4
conductor(plan): Mark task 'Record current project test coverage' as …
jamesarich Mar 19, 2026
6d9ad46
conductor(checkpoint): Checkpoint end of Phase 1
jamesarich Mar 19, 2026
30a32b5
conductor(plan): Mark phase 'Baseline Measurement' as complete
jamesarich Mar 19, 2026
79e0592
test(node): Refactor MetricsViewModelTest and extract interfaces for …
jamesarich Mar 19, 2026
dc31138
conductor(plan): Mark task 'Refactor MetricsViewModelTest' as complete
jamesarich Mar 19, 2026
b45697b
test(messaging): Clean up imports in MessageViewModelTest
jamesarich Mar 19, 2026
835a860
conductor(plan): Mark task 'Refactor MessageViewModelTest' as complete
jamesarich Mar 19, 2026
33e10fc
test(map, intro): Refactor IntroViewModelTest and BaseMapViewModelTes…
jamesarich Mar 19, 2026
aad11d0
test(node): Add commonTest for NodeDetailViewModel
jamesarich Mar 19, 2026
61b9595
conductor(checkpoint): Checkpoint end of Phase 2
jamesarich Mar 19, 2026
7ab0f37
conductor(plan): Mark phase 'Feature ViewModel Migration' as complete
jamesarich Mar 19, 2026
2c8fd6a
test(network): Add Kotest property-based tests for StreamFrameCodec
jamesarich Mar 19, 2026
438d018
conductor(plan): Mark task 'Add Kotest tests for StreamFrameCodec' as…
jamesarich Mar 19, 2026
7d56c3f
test(data): Add Kotest property-based tests for PacketHandler
jamesarich Mar 19, 2026
edf0926
conductor(plan): Mark task 'Add Kotest tests for PacketHandler' as co…
jamesarich Mar 19, 2026
2fd68d6
test(network): Add property-based tests for StreamInterface
jamesarich Mar 19, 2026
8f354c2
conductor(plan): Mark task 'Add Kotest tests for Transport' as complete
jamesarich Mar 19, 2026
cb71c85
conductor(checkpoint): Checkpoint end of Phase 3
jamesarich Mar 19, 2026
a69139c
conductor(plan): Mark phase 'Property-Based Parsing Tests' as complete
jamesarich Mar 19, 2026
7b81513
test(domain, node): Fill testing gaps for UseCases and migrate to com…
jamesarich Mar 19, 2026
a83df15
conductor(plan): Mark task 'Identify and fill testing gaps' as complete
jamesarich Mar 19, 2026
5735aa1
conductor(checkpoint): Checkpoint end of Phase 4
jamesarich Mar 19, 2026
02fa96f
conductor(plan): Mark phase 'Domain Logic Gap Fill' as complete
jamesarich Mar 19, 2026
e3fe4ba
test(coverage): Record final project test coverage metrics
jamesarich Mar 19, 2026
b585dca
conductor(plan): Mark measurement tasks as complete
jamesarich Mar 19, 2026
e321cf0
conductor(checkpoint): Checkpoint end of Phase 5
jamesarich Mar 19, 2026
9f8e265
conductor(plan): Mark phase 'Phase 5: Final Measurement & Verificatio…
jamesarich Mar 19, 2026
b2c9d3e
docs(testing): update guidelines for turbine, kotest, and mokkery
jamesarich Mar 19, 2026
1380938
conductor(plan): Mark task 'Review previous steps and update project …
jamesarich Mar 19, 2026
d950e5e
conductor(checkpoint): Checkpoint end of Phase 6
jamesarich Mar 19, 2026
464b161
conductor(plan): Mark phase 'Phase 6: Documentation and Wrap-up' as c…
jamesarich Mar 19, 2026
44a95b6
chore(conductor): Mark track 'Expand Testing Coverage' as complete
jamesarich Mar 19, 2026
5eaa9c0
docs(conductor): Synchronize docs for track 'Expand Testing Coverage'
jamesarich Mar 19, 2026
82064b6
chore(conductor): Archive track 'expand testing'
jamesarich Mar 19, 2026
93ceedb
spotless
jamesarich Mar 19, 2026
b8f36da
test: refactor unit tests and fix DI bindings
jamesarich Mar 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/reusable-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -256,3 +256,4 @@ jobs:
path: |
**/build/outputs/androidTest-results
retention-days: 7
if-no-files-found: ignore
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `Turbine` for Flow testing, `Kotest` for property-based testing, and `Mokkery` for mocking. Use `core:testing` shared fakes.
- **Build-logic conventions:** In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative.

### C. Namespacing
Expand Down
2 changes: 1 addition & 1 deletion GEMINI.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Meshtastic-Android is a Kotlin Multiplatform (KMP) application for off-grid, dec
- **JetBrains fork aliases:** Version catalog aliases for JetBrains-forked AndroidX artifacts use the `jetbrains-*` prefix (e.g., `jetbrains-lifecycle-runtime-compose`, `jetbrains-navigation3-ui`). Plain `androidx-*` aliases are true Google AndroidX artifacts. Never mix them up in `commonMain`.
- **Compose Multiplatform:** Version catalog aliases for Compose Multiplatform artifacts use the `compose-multiplatform-*` prefix (e.g., `compose-multiplatform-material3`, `compose-multiplatform-foundation`). Never use plain `androidx.compose` dependencies in common Main.
- **Room KMP:** Always use `factory = { MeshtasticDatabaseConstructor.initialize() }` in `Room.databaseBuilder` and `inMemoryDatabaseBuilder`. DAOs and Entities reside in `commonMain`.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `core:testing` shared fakes.
- **Testing:** Write ViewModel and business logic tests in `commonTest`. Use `Turbine` for Flow testing, `Kotest` for property-based testing, and `Mokkery` for mocking. Use `core:testing` shared fakes.
- **Build-logic conventions:** In `build-logic/convention`, prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs). Avoid `afterEvaluate` in convention plugins unless there is no viable lazy alternative.

### C. Namespacing
Expand Down
32 changes: 32 additions & 0 deletions conductor/archive/expand_testing_20260318/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Implementation Plan: Expand Testing Coverage

## Phase 1: Baseline Measurement [checkpoint: 6d9ad46]
- [x] Task: Execute `./gradlew koverLog` and record current project test coverage. 8bdd673a1
- [x] Task: Conductor - User Manual Verification 'Phase 1: Baseline Measurement' (Protocol in workflow.md) 6d9ad468c

## Phase 2: Feature ViewModel Migration to Turbine [checkpoint: 61b9595]
- [x] Task: Refactor `MetricsViewModelTest` to use `Turbine` and `Mokkery` in `commonTest`. 79e059286
- [x] Task: Refactor `MessageViewModelTest` to use `Turbine` and `Mokkery` in `commonTest`. b45697b53
- [x] Task: Refactor `RadioConfigViewModelTest` to use `Turbine` and `Mokkery` in `commonTest`. 33e10fc6c
- [x] Task: Refactor `NodeListViewModelTest` to use `Turbine` and `Mokkery` in `commonTest`. 33e10fc6c
- [x] Task: Refactor remaining `feature` ViewModels to use `Turbine` and `Mokkery`. 33e10fc6c
- [x] Task: Conductor - User Manual Verification 'Phase 2: Feature ViewModel Migration to Turbine' (Protocol in workflow.md) 61b959506

## Phase 3: Property-Based Parsing Tests (Kotest) [checkpoint: cb71c85]
- [x] Task: Add `Kotest` property-based tests for `StreamFrameCodec` in `core:network`. 2c8fd6a8f
- [x] Task: Add `Kotest` property-based tests for `PacketHandler` implementations in `core:data`. 7d56c3fef
- [x] Task: Add `Kotest` property-based tests for `TcpTransport` and/or `SerialTransport` in `core:network`. 2fd68d67e
- [x] Task: Conductor - User Manual Verification 'Phase 3: Property-Based Parsing Tests (Kotest)' (Protocol in workflow.md) cb71c8588

## Phase 4: Domain Logic Gap Fill [checkpoint: 5735aa1]
- [x] Task: Identify and fill testing gaps in `core:domain` use cases not fully covered during the initial Mokkery migration. 7b815130f
- [x] Task: Conductor - User Manual Verification 'Phase 4: Domain Logic Gap Fill' (Protocol in workflow.md) 5735aa148

## Phase 5: Final Measurement & Verification [checkpoint: e321cf0]
- [x] Task: Execute full test suite (`./gradlew test`) to ensure stability. 02fa96f37
- [x] Task: Execute `./gradlew koverLog` to generate and document the final coverage metrics. e3fe4ba1e
- [x] Task: Conductor - User Manual Verification 'Phase 5: Final Measurement & Verification' (Protocol in workflow.md) e321cf0

## Phase 6: Documentation and Wrap-up [checkpoint: d950e5e]
- [x] Task: Review previous steps and update project documentation (e.g., `README.md`, testing guides). b2c9d3e
- [x] Task: Conductor - User Manual Verification 'Phase 6: Documentation and Wrap-up' (Protocol in workflow.md) d950e5e
2 changes: 1 addition & 1 deletion conductor/tech-stack.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@
- **Coroutines Testing:** Use `kotlinx-coroutines-test` for virtual time management in asynchronous flows.
- **Mocking Strategy:** Avoid JVM-specific mocking libraries. Prefer `Mokkery` or `Mockative` for multiplatform-compatible mocking interfaces, alongside handwritten fakes in `core:testing`.
- **Flow Assertions:** Use `Turbine` for testing multiplatform `Flow` emissions and state updates.
- **Property-Based Testing:** Consider evaluating `Kotest` for multiplatform data-driven and property-based testing scenarios if standard `kotlin.test` becomes insufficient.
- **Property-Based Testing:** Use `Kotest` for multiplatform data-driven and property-based testing scenarios.
4 changes: 0 additions & 4 deletions conductor/tracks.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,3 @@

This file tracks all major tracks for the project. Each track has its own detailed plan in its respective folder.

Comment on lines 3 to 4
---
- [ ] **Track: Expand Testing Coverage**
*Link: [./tracks/expand_testing_20260318/](./tracks/expand_testing_20260318/)*

32 changes: 0 additions & 32 deletions conductor/tracks/expand_testing_20260318/plan.md

This file was deleted.

2 changes: 2 additions & 0 deletions core/data/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ kotlin {
commonTest.dependencies {
implementation(kotlin("test"))
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotest.assertions)
implementation(libs.kotest.property)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import org.koin.core.annotation.Single
import org.meshtastic.core.common.util.NumberFormatter
import org.meshtastic.core.common.util.handledLaunch
import org.meshtastic.core.common.util.nowMillis
import org.meshtastic.core.data.repository.TracerouteSnapshotRepository
import org.meshtastic.core.model.Node
import org.meshtastic.core.model.fullRouteDiscovery
import org.meshtastic.core.model.getFullTracerouteResponse
Expand All @@ -34,6 +33,7 @@ import org.meshtastic.core.repository.NodeManager
import org.meshtastic.core.repository.NodeRepository
import org.meshtastic.core.repository.ServiceRepository
import org.meshtastic.core.repository.TracerouteHandler
import org.meshtastic.core.repository.TracerouteSnapshotRepository
import org.meshtastic.proto.MeshPacket

@Single
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,22 +27,23 @@ import org.koin.core.annotation.Single
import org.meshtastic.core.database.DatabaseProvider
import org.meshtastic.core.database.entity.TracerouteNodePositionEntity
import org.meshtastic.core.di.CoroutineDispatchers
import org.meshtastic.core.repository.TracerouteSnapshotRepository
import org.meshtastic.proto.Position

@Single
class TracerouteSnapshotRepository(
class TracerouteSnapshotRepositoryImpl(
private val dbManager: DatabaseProvider,
private val dispatchers: CoroutineDispatchers,
) {
) : TracerouteSnapshotRepository {

fun getSnapshotPositions(logUuid: String): Flow<Map<Int, Position>> = dbManager.currentDb
override fun getSnapshotPositions(logUuid: String): Flow<Map<Int, Position>> = dbManager.currentDb
.flatMapLatest { it.tracerouteNodePositionDao().getByLogUuid(logUuid) }
.distinctUntilChanged()
.mapLatest { list -> list.associate { it.nodeNum to it.position } }
.flowOn(dispatchers.io)
.conflate()

suspend fun upsertSnapshotPositions(logUuid: String, requestId: Int, positions: Map<Int, Position>) =
override suspend fun upsertSnapshotPositions(logUuid: String, requestId: Int, positions: Map<Int, Position>) =
withContext(dispatchers.io) {
val dao = dbManager.currentDb.value.tracerouteNodePositionDao()
dao.deleteByLogUuid(logUuid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import dev.mokkery.every
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verifySuspend
import io.kotest.property.Arb
import io.kotest.property.arbitrary.int
import io.kotest.property.checkAll
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
Expand All @@ -39,6 +42,7 @@ import org.meshtastic.proto.QueueStatus
import org.meshtastic.proto.ToRadio
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertNotNull

class PacketHandlerImplTest {

Expand Down Expand Up @@ -70,13 +74,16 @@ class PacketHandlerImplTest {
handler.start(testScope)
}

@Test
fun testInitialization() {
assertNotNull(handler)
}

@Test
fun `sendToRadio with ToRadio sends immediately`() {
val toRadio = ToRadio(packet = MeshPacket(id = 123))

handler.sendToRadio(toRadio)

// No explicit assertion here in original test, but we could verify call
}

@Test
Expand Down Expand Up @@ -107,6 +114,17 @@ class PacketHandlerImplTest {
testScheduler.runCurrent()
}

@Test
fun `handleQueueStatus property test`() = runTest(testDispatcher) {
checkAll(Arb.int(0, 10), Arb.int(0, 32), Arb.int(0, 100000)) { res, free, packetId ->
val status = QueueStatus(res = res, free = free, mesh_packet_id = packetId)

// Ensure it doesn't crash on any input
handler.handleQueueStatus(status)
testScheduler.runCurrent()
}
}

@Test
fun `outgoing packets are logged with NODE_NUM_LOCAL`() = runTest(testDispatcher) {
val packet = MeshPacket(id = 123, decoded = Data(portnum = PortNum.TEXT_MESSAGE_APP))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* 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.domain.usecase.settings

import dev.mokkery.answering.returns
import dev.mokkery.every
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verify
import org.meshtastic.core.common.UiPreferences
import kotlin.test.BeforeTest
import kotlin.test.Test

class SetLocaleUseCaseTest {

private val uiPreferences: UiPreferences = mock()
private lateinit var useCase: SetLocaleUseCase

@BeforeTest
fun setUp() {
useCase = SetLocaleUseCase(uiPreferences)
}

@Test
fun `invoke calls setLocale on uiPreferences`() {
every { uiPreferences.setLocale(any()) } returns Unit
useCase("en")
verify { uiPreferences.setLocale("en") }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* 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.domain.usecase.settings

import dev.mokkery.answering.returns
import dev.mokkery.every
import dev.mokkery.matcher.any
import dev.mokkery.mock
import dev.mokkery.verify
import org.meshtastic.core.repository.NotificationPrefs
import kotlin.test.BeforeTest
import kotlin.test.Test

class SetNotificationSettingsUseCaseTest {

private val notificationPrefs: NotificationPrefs = mock()
private lateinit var useCase: SetNotificationSettingsUseCase

@BeforeTest
fun setUp() {
useCase = SetNotificationSettingsUseCase(notificationPrefs)
}

@Test
fun `setMessagesEnabled calls notificationPrefs`() {
every { notificationPrefs.setMessagesEnabled(any()) } returns Unit
useCase.setMessagesEnabled(true)
verify { notificationPrefs.setMessagesEnabled(true) }
}

@Test
fun `setNodeEventsEnabled calls notificationPrefs`() {
every { notificationPrefs.setNodeEventsEnabled(any()) } returns Unit
useCase.setNodeEventsEnabled(false)
verify { notificationPrefs.setNodeEventsEnabled(false) }
}

@Test
fun `setLowBatteryEnabled calls notificationPrefs`() {
every { notificationPrefs.setLowBatteryEnabled(any()) } returns Unit
useCase.setLowBatteryEnabled(true)
verify { notificationPrefs.setLowBatteryEnabled(true) }
}
}
6 changes: 5 additions & 1 deletion core/network/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ kotlin {
implementation(libs.okhttp3.logging.interceptor)
}

commonTest.dependencies { implementation(libs.kotlinx.coroutines.test) }
commonTest.dependencies {
implementation(libs.kotlinx.coroutines.test)
implementation(libs.kotest.assertions)
implementation(libs.kotest.property)
}
}
}
Loading
Loading