mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-10-03 09:04:32 -04:00
build: clear Kotlin warnings, time out hung tests, drop dead config (#7435)
This commit is contained in:
1 parent
b12ca979c8
commit
1b128ffc78
59 files changed
+499
-468
No files matched your search
@@ -44,9 +44,6 @@ ksp.incremental=false
|
||||
|
||||
# ── Android ──────────────────────────────────────────────────────────
|
||||
android.experimental.lint.analysisPerComponent=true
|
||||
# Disable unused build features to reduce build time
|
||||
android.defaults.buildfeatures.resvalues=false
|
||||
android.defaults.buildfeatures.shaders=false
|
||||
|
||||
# ── Misc ─────────────────────────────────────────────────────────────
|
||||
org.gradle.welcome=never
|
||||
@@ -7,4 +7,4 @@ applyTo: "build-logic/**/*.kt"
|
||||
- Prefer lazy Gradle configuration (`configureEach`, `withPlugin`, provider APIs).
|
||||
- Avoid `afterEvaluate` unless there is no viable lazy alternative.
|
||||
- Check `gradle/libs.versions.toml` for version catalog aliases before adding new ones.
|
||||
- 24 convention plugin ids are registered in `build-logic/convention/build.gradle.kts` — read that block rather than guessing. The ones module builds apply most often: `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.library.compose`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`.
|
||||
- 25 convention plugin ids are registered in `build-logic/convention/build.gradle.kts`. Read that block rather than guessing. The ones module builds apply most often: `meshtastic.kmp.feature`, `meshtastic.kmp.library`, `meshtastic.kmp.library.compose`, `meshtastic.kmp.jvm.android`, `meshtastic.koin`.
|
||||
@@ -157,6 +157,32 @@
|
||||
"depNameTemplate": "gradle",
|
||||
"datasourceTemplate": "gradle-version",
|
||||
"versioningTemplate": "gradle"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "ktlint's version is a bare [versions] entry that only Spotless reads, so the gradle manager cannot tie it to an artifact.",
|
||||
"managerFilePatterns": [
|
||||
"/^gradle/libs\\.versions\\.toml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"\\nktlint\\s*=\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "com.pinterest.ktlint:ktlint-cli",
|
||||
"datasourceTemplate": "maven",
|
||||
"versioningTemplate": "maven"
|
||||
},
|
||||
{
|
||||
"customType": "regex",
|
||||
"description": "ktfmt's version is a bare [versions] entry that only Spotless reads, so the gradle manager cannot tie it to an artifact.",
|
||||
"managerFilePatterns": [
|
||||
"/^gradle/libs\\.versions\\.toml$/"
|
||||
],
|
||||
"matchStrings": [
|
||||
"\\nktfmt\\s*=\\s*\"(?<currentValue>[^\"]+)\""
|
||||
],
|
||||
"depNameTemplate": "com.facebook:ktfmt",
|
||||
"datasourceTemplate": "maven",
|
||||
"versioningTemplate": "maven"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -133,15 +133,15 @@ The tiers are named here and the workflows carry the label versions.
|
||||
- `-Xmx4g` Gradle heap, `-Xmx6g` Kotlin daemon
|
||||
- VFS watching disabled, workers capped at 4
|
||||
- `org.gradle.isolated-projects=true` for better parallelism
|
||||
- Disables unused Android build features (`resvalues`, `shaders`)
|
||||
|
||||
### CI Conventions
|
||||
- **KMP Smoke Compile:** `./gradlew kmpSmokeCompile` is a lifecycle task (registered in `RootConventionPlugin`) that depends on `compileKotlinJvm` + `compileKotlinIosSimulatorArm64` for every KMP module in the hand-maintained `ALL_MODULES_FULL` list, plus `compileAndroidDeviceTest` for `:core:database` and `:core:model`. `scripts/check-module-list.py` fails the PR when that list drifts from `settings.gradle.kts`. CI runs it in `shard-core`.
|
||||
- **`maxParallelForks` CI logic:** `ProjectExtensions.kt` checks `project.findProperty("ci") == "true"` and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass `-Pci=true`.
|
||||
- **`maxParallelForks` CI logic:** `ProjectExtensions.kt` reads the `ci` Gradle property (`providers.gradleProperty("ci")`) and uses full available processors in CI (4 forks on std runners) vs. half locally. All CI invocations pass `-Pci=true`.
|
||||
- **Detekt report formats:** Detekt.kt checks `project.findProperty("ci") == "true"` and disables html, txt, md reports in CI; only xml + sarif are retained for GitHub annotations.
|
||||
- **Robolectric SDK caching:** The `gradle-setup` composite action caches `~/.m2/repository/org/robolectric` to prevent flaky `SocketException` on SDK downloads. Cache key is `robolectric-{os}-{arch}-{hash of gradle/libs.versions.toml}`, restoring from the `robolectric-{os}-{arch}-` prefix, so a catalog change that bumps Robolectric rolls the key without a hand edit.
|
||||
- **`mavenLocal()` gated:** Disabled by default to prevent CI cache poisoning. Pass `-PuseMavenLocal` for local JitPack testing.
|
||||
- **JUnit parallel execution:** Enabled project-wide with classes running sequentially (`junit.jupiter.execution.parallel.mode.classes.default=same_thread`) to avoid `Dispatchers.setMain()` races. Cross-module parallelism comes from Gradle forks (`maxParallelForks`).
|
||||
- **Test timeouts:** every Jupiter test and lifecycle method fails after 2 minutes (`junit.jupiter.execution.timeout.default`, `SEPARATE_THREAD` so code that ignores interrupts still fails by name), and every `Test` task stops after 15 minutes, which also covers the JUnit 4 host tests. Both live in `ProjectExtensions.kt`.
|
||||
- **Test retry:** Develocity plugin's native retry (`develocity.testRetry` on each Test task), configured in `ProjectExtensions.kt` (maxRetries=2, maxFailures=10). Screenshot tests opt out (maxRetries=0). The standalone `org.gradle.test-retry` plugin was removed.
|
||||
- **`fail-fast: false`:** Test sharding does not cancel other shards on failure.
|
||||
- **Explicit Gradle task paths:** Prefer `androidApp:lintFdroidDebug` over shorthand `lintDebug` in CI.
|
||||
|
||||
@@ -124,8 +124,6 @@ configure<ApplicationExtension> {
|
||||
)
|
||||
}
|
||||
ndk { abiFilters += listOf("armeabi-v7a", "arm64-v8a") }
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
// Disable ABI splits for bundle builds or when explicitly requested via Gradle property.
|
||||
|
||||
@@ -88,6 +88,9 @@ fun WaypointMarkers(
|
||||
val cleanName = waypoint.name.replace('\n', ' ').replace('\b', ' ')
|
||||
val title = if (waypoint.isLocked) "${convertIntToEmoji(LOCK)} $cleanName" else cleanName
|
||||
|
||||
// rememberComposeBitmapDescriptor shares its applier with its content lambda, so the Text drawn into the
|
||||
// icon makes this map-scope function infer as UI.
|
||||
@Suppress("COMPOSE_APPLIER_CALL_MISMATCH")
|
||||
Marker(
|
||||
state = markerState,
|
||||
icon = icon,
|
||||
|
||||
@@ -14,8 +14,11 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
|
||||
package org.meshtastic.app.map.offline.pmtiles
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
|
||||
|
||||
@@ -14,28 +14,19 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
plugins {
|
||||
alias(libs.plugins.android.test)
|
||||
alias(libs.plugins.meshtastic.android.test)
|
||||
alias(libs.plugins.androidx.baselineprofile)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.meshtastic.baselineprofile"
|
||||
compileSdk = 37
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
// Macrobenchmark / BaselineProfileRule require API 28+ on the test (device) side.
|
||||
// The generated profile is still installed on the app's real minSdk (26) via profileinstaller.
|
||||
minSdk = 28
|
||||
targetSdk = 37
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
// App module whose startup we profile/benchmark.
|
||||
@@ -48,8 +39,6 @@ android {
|
||||
productFlavors { create("google") { dimension = "marketplace" } }
|
||||
}
|
||||
|
||||
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }
|
||||
|
||||
baselineProfile {
|
||||
// Generate on an attached device/emulator. For hermetic CI, replace with a Gradle Managed
|
||||
// Device (see README.md) and set managedDevices + useConnectedDevices = false.
|
||||
|
||||
@@ -38,9 +38,6 @@ dependencies {
|
||||
// This allows the use of the 'libs' type-safe accessor in the Kotlin source of the plugins
|
||||
implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
|
||||
|
||||
// Self-updating embedded Gradle Kotlin version
|
||||
val gradleKotlinVersion = KotlinVersion.CURRENT.toString()
|
||||
|
||||
// ── Convention plugin compile dependencies ──────────────────────────────
|
||||
// These are standard compile-time dependencies used by our convention plugins.
|
||||
compileOnly(libs.android.gradleApiPlugin)
|
||||
@@ -137,13 +134,16 @@ gradlePlugin {
|
||||
}
|
||||
register("androidSecrets") {
|
||||
id = "meshtastic.android.secrets"
|
||||
implementationClass =
|
||||
"com.google.android.libraries.mapsplatform.secrets_gradle_plugin.SecretsPlugin"
|
||||
implementationClass = "com.google.android.libraries.mapsplatform.secrets_gradle_plugin.SecretsPlugin"
|
||||
}
|
||||
register("androidScreenshot") {
|
||||
id = "meshtastic.android.screenshot"
|
||||
implementationClass = "AndroidScreenshotConventionPlugin"
|
||||
}
|
||||
register("androidTest") {
|
||||
id = "meshtastic.android.test"
|
||||
implementationClass = "AndroidTestConventionPlugin"
|
||||
}
|
||||
register("androidApplicationCompose") {
|
||||
id = "meshtastic.android.application.compose"
|
||||
implementationClass = "AndroidApplicationComposeConventionPlugin"
|
||||
|
||||
@@ -46,11 +46,12 @@ class AboutLibrariesConventionPlugin : Plugin<Project> {
|
||||
|
||||
collect {
|
||||
configPath.set(licenseConfigDir)
|
||||
fetchRemoteLicense.set(isReleaseBuild && ghToken.isPresent)
|
||||
fetchRemoteFunding.set(isReleaseBuild && ghToken.isPresent)
|
||||
if (ghToken.isPresent) {
|
||||
gitHubApiToken.set(ghToken.get())
|
||||
}
|
||||
// Wired as providers, never read here: CI issues a new token per step, and a token read at
|
||||
// configuration time becomes a configuration cache input that no later step can reuse.
|
||||
val fetchRemote = ghToken.map { isReleaseBuild }.orElse(false)
|
||||
fetchRemoteLicense.set(fetchRemote)
|
||||
fetchRemoteFunding.set(fetchRemote)
|
||||
gitHubApiToken.set(ghToken)
|
||||
}
|
||||
export {
|
||||
excludeFields.set(listOf("generated"))
|
||||
|
||||
@@ -51,8 +51,6 @@ class AndroidLintConventionPlugin : Plugin<Project> {
|
||||
}
|
||||
|
||||
private fun Lint.configure(@Suppress("UnusedParameter") project: Project) {
|
||||
xmlReport = true
|
||||
sarifReport = true
|
||||
checkDependencies = true
|
||||
abortOnError = false
|
||||
disable += "GradleDependency"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
import com.android.build.api.dsl.TestExtension
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.apply
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.meshtastic.buildlogic.configureKotlinAndroidTest
|
||||
|
||||
class AndroidTestConventionPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
with(target) {
|
||||
apply(plugin = "com.android.test")
|
||||
|
||||
extensions.configure<TestExtension> { configureKotlinAndroidTest(this) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,19 +19,24 @@ package org.meshtastic.buildlogic
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
|
||||
import com.android.build.api.dsl.TestExtension
|
||||
import dev.mokkery.gradle.MokkeryGradleExtension
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.provider.Provider
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
import org.gradle.kotlin.dsl.findByType
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
|
||||
import org.jetbrains.kotlin.gradle.plugin.KotlinHierarchyTemplate
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
/** Configure base Kotlin with Android options */
|
||||
@@ -44,7 +49,6 @@ internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension) {
|
||||
compileSdk = compileSdkVersion
|
||||
|
||||
defaultConfig.minSdk = minSdkVersion
|
||||
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
if (this is ApplicationExtension) {
|
||||
defaultConfig.targetSdk = targetSdkVersion
|
||||
@@ -64,6 +68,19 @@ internal fun Project.configureKotlinAndroid(commonExtension: CommonExtension) {
|
||||
configureKotlin<KotlinAndroidProjectExtension>()
|
||||
}
|
||||
|
||||
/** Configure SDK and JVM levels for `com.android.test` modules; each module keeps its own minSdk. */
|
||||
internal fun Project.configureKotlinAndroidTest(testExtension: TestExtension) {
|
||||
testExtension.apply {
|
||||
compileSdk = configProperties.getProperty("COMPILE_SDK").toInt()
|
||||
defaultConfig.targetSdk = configProperties.getProperty("TARGET_SDK").toInt()
|
||||
|
||||
compileOptions.sourceCompatibility = JavaVersion.VERSION_21
|
||||
compileOptions.targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
configureKotlin<KotlinAndroidProjectExtension>()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the current build host can run the Kotlin/Native compiler.
|
||||
*
|
||||
@@ -216,14 +233,22 @@ internal fun Project.configureKotlinJvm() {
|
||||
configureKotlin<KotlinJvmProjectExtension>()
|
||||
}
|
||||
|
||||
/**
|
||||
* `-PwarningsAsErrors=true` turns every Kotlin warning into an error. Modules that configure Kotlin outside these
|
||||
* conventions (desktopApp, schema-strings) read it too.
|
||||
*/
|
||||
val Project.kotlinWarningsAsErrors: Provider<Boolean>
|
||||
get() = providers.gradleProperty("warningsAsErrors").map { it.toBoolean() }.orElse(false)
|
||||
|
||||
/** Compiler args shared across all Kotlin targets (JVM, Android, iOS, etc.). */
|
||||
private val SHARED_COMPILER_ARGS =
|
||||
listOf(
|
||||
"-Xexpect-actual-classes",
|
||||
"-Xskip-prerelease-check",
|
||||
// No -Xbackend-threads: parallel codegen races and crashes release builds (KT-83578).
|
||||
)
|
||||
|
||||
private const val SHARED_OPT_IN = "kotlinx.coroutines.ExperimentalCoroutinesApi"
|
||||
|
||||
private const val JDK_VERSION = 25
|
||||
|
||||
/** Configure base Kotlin options */
|
||||
@@ -233,14 +258,13 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
||||
|
||||
if (this is KotlinMultiplatformExtension) {
|
||||
targets.configureEach {
|
||||
val isJvmTarget = platformType.name == "jvm" || platformType.name == "androidJvm"
|
||||
compilations.configureEach {
|
||||
compileTaskProvider.configure {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
optIn.add(SHARED_OPT_IN)
|
||||
freeCompilerArgs.addAll(SHARED_COMPILER_ARGS)
|
||||
if (isJvmTarget) {
|
||||
freeCompilerArgs.add("-jvm-default=no-compatibility")
|
||||
if (this is KotlinJvmCompilerOptions) {
|
||||
jvmDefault.set(JvmDefaultMode.NO_COMPATIBILITY)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,19 +273,23 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() {
|
||||
}
|
||||
}
|
||||
|
||||
val warningsAsErrors = providers.gradleProperty("warningsAsErrors").map { it.toBoolean() }.getOrElse(false)
|
||||
val warningsAsErrors = kotlinWarningsAsErrors
|
||||
|
||||
// Every compilation, native and metadata included, so a gated build also fails on iosMain-only warnings.
|
||||
tasks.withType<KotlinCompilationTask<*>>().configureEach {
|
||||
compilerOptions.allWarningsAsErrors.set(warningsAsErrors)
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile>().configureEach {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_21)
|
||||
allWarningsAsErrors.set(warningsAsErrors)
|
||||
|
||||
// For non-KMP modules, configure compiler args here since they don't use targets.compilations.
|
||||
// KMP modules already set these via the targets block above — only jvmTarget/warnings needed here.
|
||||
// KMP modules already set these via the targets block above; only jvmTarget is needed here.
|
||||
if (T::class != KotlinMultiplatformExtension::class) {
|
||||
freeCompilerArgs.add("-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi")
|
||||
optIn.add(SHARED_OPT_IN)
|
||||
freeCompilerArgs.addAll(SHARED_COMPILER_ARGS)
|
||||
freeCompilerArgs.add("-jvm-default=no-compatibility")
|
||||
jvmDefault.set(JvmDefaultMode.NO_COMPATIBILITY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+15
-1
@@ -30,10 +30,12 @@ import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.withType
|
||||
import org.gradle.plugin.use.PluginDependency
|
||||
import java.io.FileInputStream
|
||||
import java.time.Duration
|
||||
import java.util.Properties
|
||||
|
||||
private const val MAX_TEST_RETRIES = 2
|
||||
private const val MAX_TEST_FAILURES = 10
|
||||
private const val TEST_TASK_TIMEOUT_MINUTES = 15L
|
||||
|
||||
val Project.libs
|
||||
get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
|
||||
@@ -74,6 +76,8 @@ internal fun Project.configureTestOptions() {
|
||||
project.dependencies.add(name, launcher)
|
||||
}
|
||||
|
||||
val isCi = providers.gradleProperty("ci").map { it.toBoolean() }.getOrElse(false)
|
||||
|
||||
tasks.withType<Test>().configureEach {
|
||||
// JUnit 5: activate JUnit Platform — but NOT for androidHostTest (Robolectric) tasks
|
||||
// in KMP modules. Those tasks run JUnit 4 natively; applying useJUnitPlatform()
|
||||
@@ -84,7 +88,6 @@ internal fun Project.configureTestOptions() {
|
||||
}
|
||||
// Parallelize unit tests at the Gradle fork level.
|
||||
// In CI, use all available processors; locally use half to keep the machine responsive.
|
||||
val isCi = project.findProperty("ci") == "true"
|
||||
maxParallelForks =
|
||||
if (isCi) {
|
||||
Runtime.getRuntime().availableProcessors().coerceAtLeast(1)
|
||||
@@ -99,12 +102,23 @@ internal fun Project.configureTestOptions() {
|
||||
systemProperty("java.awt.headless", "true")
|
||||
jvmArgs("-Dapple.awt.UIElement=true")
|
||||
|
||||
// JDK 24+ warns on every System.load from the class path (bundled SQLite, Skiko) unless native access is on.
|
||||
jvmArgs("--enable-native-access=ALL-UNNAMED")
|
||||
|
||||
// Numbers and units format in the OS locale, so the forked test JVMs are pinned to one: otherwise a
|
||||
// contributor whose machine defaults to de-DE gets "0,0°C" and fails every test that pins "0.0°C".
|
||||
// Locale-specific behaviour is asserted by tests that set the locale themselves.
|
||||
systemProperty("user.language", "en")
|
||||
systemProperty("user.country", "US")
|
||||
|
||||
// A hung test fails by name instead of running into the CI job timeout. SEPARATE_THREAD lets the timeout
|
||||
// fire even when the stuck code never checks for interruption.
|
||||
systemProperty("junit.jupiter.execution.timeout.default", "2 m")
|
||||
systemProperty("junit.jupiter.execution.timeout.thread.mode.default", "SEPARATE_THREAD")
|
||||
systemProperty("junit.jupiter.execution.timeout.mode", "disabled_on_debug")
|
||||
// Backstop for JUnit 4 host tests, which Jupiter's timeout does not reach, and for a wedged test JVM.
|
||||
timeout.set(Duration.ofMinutes(TEST_TASK_TIMEOUT_MINUTES))
|
||||
|
||||
// JUnit Jupiter parallel execution within each Gradle fork.
|
||||
// Classes run sequentially ("same_thread") because 19+ ViewModel test classes use
|
||||
// Dispatchers.setMain() — a JVM-global singleton that races when classes execute
|
||||
|
||||
@@ -15,21 +15,8 @@
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
# Gradle properties for the build-logic included build.
|
||||
# These need to be set separately because properties are not passed to included builds.
|
||||
# https://github.com/gradle/gradle/issues/2534
|
||||
# Project properties for the build-logic included build, which does not see the root's.
|
||||
# Gradle runtime properties (org.gradle.*) are read from the root build only and are
|
||||
# ignored here: https://docs.gradle.org/current/userguide/composite_builds.html
|
||||
|
||||
org.gradle.jvmargs=-Xmx2g -XX:+UseParallelGC -Dfile.encoding=UTF-8
|
||||
|
||||
# Parallelism & Caching
|
||||
org.gradle.parallel=true
|
||||
org.gradle.caching=true
|
||||
org.gradle.configuration-cache=true
|
||||
org.gradle.isolated-projects=true
|
||||
org.gradle.configureondemand=false
|
||||
|
||||
# Kotlin
|
||||
kotlin.code.style=official
|
||||
|
||||
# Housekeeping
|
||||
org.gradle.welcome=never
|
||||
+4
-5
@@ -16,7 +16,6 @@
|
||||
*/
|
||||
package org.meshtastic.core.ble
|
||||
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
@@ -146,8 +145,8 @@ class AndroidBluetoothRepositoryBondTest {
|
||||
// Make the device already bonded both at the adapter level (so isBonded is observable) and per-device
|
||||
// (so bond() hits the early BOND_BONDED guard at line 85 without ever calling createBond()).
|
||||
RobolectricBleBonding.primeBond(mac, bondState = BluetoothDevice.BOND_BONDED, createBondReturns = false)
|
||||
shadowOf(BluetoothAdapter.getDefaultAdapter())
|
||||
.setBondedDevices(setOf(BluetoothAdapter.getDefaultAdapter().getRemoteDevice(mac)))
|
||||
val adapter = RobolectricBleBonding.adapter
|
||||
shadowOf(adapter).setBondedDevices(setOf(adapter.getRemoteDevice(mac)))
|
||||
val repo = newRepository(UnconfinedTestDispatcher(testScheduler))
|
||||
|
||||
assertNull(launchBond(repo, mac).await(), "an already-bonded device should return without error")
|
||||
@@ -401,8 +400,8 @@ class AndroidBluetoothRepositoryBondTest {
|
||||
val bondedMac = "AA:BB:CC:DD:EE:06"
|
||||
val otherMac = "AA:BB:CC:DD:EE:07"
|
||||
RobolectricBleBonding.grantBluetoothConnectPermission()
|
||||
val bondedDevice = BluetoothAdapter.getDefaultAdapter().getRemoteDevice(bondedMac)
|
||||
shadowOf(BluetoothAdapter.getDefaultAdapter()).setBondedDevices(setOf(bondedDevice))
|
||||
val adapter = RobolectricBleBonding.adapter
|
||||
shadowOf(adapter).setBondedDevices(setOf(adapter.getRemoteDevice(bondedMac)))
|
||||
val repo = newRepository(UnconfinedTestDispatcher(testScheduler))
|
||||
|
||||
assertTrue(repo.isBonded(bondedMac))
|
||||
|
||||
@@ -25,6 +25,7 @@ import okio.ByteString
|
||||
* device public key: `my_node_num = crc32(config.security.public_key)` (NodeDB::createNewIdentity). Lets the app
|
||||
* recognize a pubkey-derived ("canonical") node number when a node reappears under a new num after a firmware upgrade.
|
||||
*/
|
||||
@OptIn(ExperimentalUnsignedTypes::class)
|
||||
object Crc32 {
|
||||
private const val POLYNOMIAL: UInt = 0xEDB88320u
|
||||
private const val INITIAL: UInt = 0xFFFFFFFFu
|
||||
|
||||
-3
@@ -467,7 +467,6 @@ class MeshConnectionManagerImplTest {
|
||||
admissionVersions += call.arg<Long>(1)
|
||||
seedAttempts++
|
||||
if (seedAttempts < 3) throw PacketQueueRejectedException("test passkey seed")
|
||||
Unit
|
||||
}
|
||||
everySuspend { commandSender.requestTelemetryForConnection(any(), any(), any(), any()) } calls
|
||||
{ call ->
|
||||
@@ -476,14 +475,12 @@ class MeshConnectionManagerImplTest {
|
||||
val attempts = telemetryAttempts.getOrElse(type) { 0 } + 1
|
||||
telemetryAttempts[type] = attempts
|
||||
if (attempts == 1) throw PacketQueueRejectedException("test telemetry request")
|
||||
Unit
|
||||
}
|
||||
everySuspend { historyManager.requestHistoryReplay(any(), any(), any(), any(), any()) } calls
|
||||
{ call ->
|
||||
admissionVersions += call.arg<Long>(4)
|
||||
historyAttempts++
|
||||
if (historyAttempts == 1) throw PacketQueueRejectedException("test history replay")
|
||||
Unit
|
||||
}
|
||||
every { nodeManager.myNodeNum } returns MutableStateFlow(123)
|
||||
every { mqttManager.startProxy(any(), any()) } returns Unit
|
||||
|
||||
@@ -37,6 +37,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -1263,6 +1264,7 @@ open class DatabaseManager(private val datastore: DatabaseDataStore, private val
|
||||
* only once it is about to invoke the callback — a call still waiting for its lane has performed no side effect and
|
||||
* is aborted at the cancellation check instead of running late.
|
||||
*/
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
private fun <T> launchDbBlock(
|
||||
admission: AdmittedDatabase,
|
||||
blockStarted: CompletableDeferred<Unit>,
|
||||
|
||||
+5
-5
@@ -152,7 +152,7 @@ abstract class CommonNodeInfoDaoTest {
|
||||
assertEquals(trusted, stored?.publicKey)
|
||||
assertEquals(trusted, stored?.user?.public_key)
|
||||
assertFalse(stored?.keyMatch ?: true)
|
||||
assertEquals(substitute, stored?.newPublicKey)
|
||||
assertEquals(substitute, stored.newPublicKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -207,7 +207,7 @@ abstract class CommonNodeInfoDaoTest {
|
||||
val stillFlagged = dao.getNodeByNum(1)?.node
|
||||
assertEquals(trusted, stillFlagged?.publicKey)
|
||||
assertFalse(stillFlagged?.keyMatch ?: true)
|
||||
assertEquals(substitute, stillFlagged?.newPublicKey)
|
||||
assertEquals(substitute, stillFlagged.newPublicKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -262,7 +262,7 @@ abstract class CommonNodeInfoDaoTest {
|
||||
val stored = dao.getNodeByNum(own)?.node
|
||||
assertEquals(after, stored?.publicKey)
|
||||
assertTrue(stored?.keyMatch ?: false)
|
||||
assertEquals(null, stored?.newPublicKey)
|
||||
assertEquals(null, stored.newPublicKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -299,7 +299,7 @@ abstract class CommonNodeInfoDaoTest {
|
||||
val remote = dao.getNodeByNum(1)?.node
|
||||
assertEquals(trusted, remote?.publicKey)
|
||||
assertTrue(remote?.keyMatch ?: false)
|
||||
assertEquals(null, remote?.newPublicKey)
|
||||
assertEquals(null, remote.newPublicKey)
|
||||
|
||||
// Nor may the local link write it over the connected radio's real key. A key of its own, or the new-node
|
||||
// guard would read this upsert as node 1 claiming a second number and never insert it.
|
||||
@@ -447,7 +447,7 @@ abstract class CommonNodeInfoDaoTest {
|
||||
val stored = dao.getNodeByNum(own)
|
||||
assertEquals(real, stored?.node?.publicKey)
|
||||
assertFalse(stored?.node?.keyMatch ?: true)
|
||||
assertTrue(stored!!.toModel().mismatchKey)
|
||||
assertTrue(stored.toModel().mismatchKey)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -90,8 +90,6 @@ core:domain
|
||||
├── core:model (domain models)
|
||||
├── org.meshtastic:protobufs (Meshtastic protobuf types, Maven)
|
||||
├── core:common
|
||||
├── core:database
|
||||
├── core:datastore
|
||||
└── core:resources
|
||||
```
|
||||
|
||||
@@ -110,8 +108,6 @@ graph TB
|
||||
:core:domain -.-> :core:repository
|
||||
:core:domain -.-> :core:model
|
||||
:core:domain -.-> :core:common
|
||||
:core:domain -.-> :core:database
|
||||
:core:domain -.-> :core:datastore
|
||||
:core:domain -.-> :core:resources
|
||||
:core:domain -.-> :core:testing
|
||||
|
||||
|
||||
@@ -30,8 +30,6 @@ kotlin {
|
||||
implementation(projects.core.model)
|
||||
implementation(libs.meshtastic.protobufs)
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.database)
|
||||
implementation(projects.core.datastore)
|
||||
implementation(projects.core.resources)
|
||||
|
||||
implementation(libs.kermit)
|
||||
|
||||
+4
-20
@@ -105,11 +105,7 @@ class EnsureRemoteAdminSessionUseCaseTest {
|
||||
val sessionManager = stubSessionManager(refreshFlow = refresh)
|
||||
val controller = mock<RadioController>(MockMode.autofill)
|
||||
// Simulate the radio responding by emitting on the refresh flow when the metadata request fires.
|
||||
everySuspend { controller.refreshMetadata(any()) } calls
|
||||
{
|
||||
refresh.tryEmit(destNum)
|
||||
Unit
|
||||
}
|
||||
everySuspend { controller.refreshMetadata(any()) } calls { refresh.tryEmit(destNum) }
|
||||
|
||||
val useCase =
|
||||
EnsureRemoteAdminSessionUseCase(sessionManager, controller, connectedRepo(), this.asServiceScope())
|
||||
@@ -167,11 +163,7 @@ class EnsureRemoteAdminSessionUseCaseTest {
|
||||
val sessionManager = stubSessionManager(refreshFlow = refresh)
|
||||
val controller = mock<RadioController>(MockMode.autofill)
|
||||
var dispatches = 0
|
||||
everySuspend { controller.refreshMetadata(any()) } calls
|
||||
{
|
||||
dispatches++
|
||||
Unit
|
||||
}
|
||||
everySuspend { controller.refreshMetadata(any()) } calls { dispatches++ }
|
||||
val useCase =
|
||||
EnsureRemoteAdminSessionUseCase(sessionManager, controller, connectedRepo(), this.asServiceScope())
|
||||
|
||||
@@ -195,11 +187,7 @@ class EnsureRemoteAdminSessionUseCaseTest {
|
||||
val sessionManager = stubSessionManager(refreshFlow = refresh)
|
||||
val controller = mock<RadioController>(MockMode.autofill)
|
||||
var dispatches = 0
|
||||
everySuspend { controller.refreshMetadata(any()) } calls
|
||||
{
|
||||
dispatches++
|
||||
Unit
|
||||
}
|
||||
everySuspend { controller.refreshMetadata(any()) } calls { dispatches++ }
|
||||
val useCase =
|
||||
EnsureRemoteAdminSessionUseCase(sessionManager, controller, connectedRepo(), this.asServiceScope())
|
||||
|
||||
@@ -224,11 +212,7 @@ class EnsureRemoteAdminSessionUseCaseTest {
|
||||
val sessionManager = stubSessionManager(refreshFlow = refresh)
|
||||
val controller = mock<RadioController>(MockMode.autofill)
|
||||
var dispatches = 0
|
||||
everySuspend { controller.refreshMetadata(any()) } calls
|
||||
{
|
||||
dispatches++
|
||||
Unit
|
||||
}
|
||||
everySuspend { controller.refreshMetadata(any()) } calls { dispatches++ }
|
||||
val connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Connected)
|
||||
val repository = mock<ServiceRepository>(MockMode.autofill)
|
||||
every { repository.connectionState } returns connectionState
|
||||
|
||||
@@ -22,6 +22,7 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// No withHostTest: commonTest reaches android.net.Uri and Bundle, which the host-test stubs do not implement.
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(projects.core.common)
|
||||
|
||||
+1
-1
@@ -169,7 +169,7 @@ class MockRadioTransport(
|
||||
}
|
||||
|
||||
data != null && data.portnum == PortNum.TEXT_MESSAGE_APP -> {
|
||||
if (packet?.want_ack == true) sendFakeAck(pr)
|
||||
if (packet.want_ack) sendFakeAck(pr)
|
||||
sendSimulatedReply(packet)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -607,7 +607,7 @@ internal fun effectiveCredentials(config: ModuleConfig.MQTTConfig?): Pair<String
|
||||
if (config?.address.isNullOrEmpty()) {
|
||||
DEFAULT_MQTT_USERNAME to DEFAULT_MQTT_PASSWORD
|
||||
} else {
|
||||
config?.username to config?.password
|
||||
config.username to config.password
|
||||
}
|
||||
|
||||
fun effectiveTlsEnabled(address: String, tlsEnabled: Boolean): Boolean =
|
||||
|
||||
+9
-3
@@ -20,8 +20,10 @@ import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.RemoteInput
|
||||
import androidx.core.os.BundleCompat
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import dev.mokkery.MockMode
|
||||
@@ -361,8 +363,10 @@ class MeshNotificationManagerImplConversationTest {
|
||||
advanceUntilIdle()
|
||||
|
||||
val posted = activeByTag("message").single().notification
|
||||
val alerting = posted.extras.getParcelableArray(Notification.EXTRA_MESSAGES)
|
||||
val historic = posted.extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES)
|
||||
val alerting =
|
||||
BundleCompat.getParcelableArray(posted.extras, Notification.EXTRA_MESSAGES, Parcelable::class.java)
|
||||
val historic =
|
||||
BundleCompat.getParcelableArray(posted.extras, Notification.EXTRA_HISTORIC_MESSAGES, Parcelable::class.java)
|
||||
assertEquals(1, alerting?.size, "only the unread message should be presented as new content")
|
||||
assertEquals(2, historic?.size, "read context should be carried as historic messages")
|
||||
assertEquals("new unread", posted.extras.getCharSequence(Notification.EXTRA_TEXT)?.toString())
|
||||
@@ -429,7 +433,9 @@ class MeshNotificationManagerImplConversationTest {
|
||||
val posted = activeByTag("message").single().notification
|
||||
// Empty primary channel resolves to its modem-preset display name, matching the in-app conversation list.
|
||||
assertEquals("LongFast", posted.extras.getCharSequence(Notification.EXTRA_CONVERSATION_TITLE)?.toString())
|
||||
assertNotNull(posted.extras.getParcelableArray(Notification.EXTRA_MESSAGES))
|
||||
assertNotNull(
|
||||
BundleCompat.getParcelableArray(posted.extras, Notification.EXTRA_MESSAGES, Parcelable::class.java),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+4
-20
@@ -219,16 +219,8 @@ class MeshServiceOrchestratorTest {
|
||||
val takEnabledFlow = MutableStateFlow(false)
|
||||
val takRunningFlow = MutableStateFlow(false)
|
||||
val lifecycleEvents = mutableListOf<String>()
|
||||
every { takServerManager.start(any()) } calls
|
||||
{
|
||||
lifecycleEvents += "start"
|
||||
Unit
|
||||
}
|
||||
every { takServerManager.stop() } calls
|
||||
{
|
||||
lifecycleEvents += "stop"
|
||||
Unit
|
||||
}
|
||||
every { takServerManager.start(any()) } calls { lifecycleEvents += "start" }
|
||||
every { takServerManager.stop() } calls { lifecycleEvents += "stop" }
|
||||
val orchestrator = createOrchestrator(takEnabledFlow = takEnabledFlow, takRunningFlow = takRunningFlow)
|
||||
|
||||
orchestrator.start()
|
||||
@@ -248,16 +240,8 @@ class MeshServiceOrchestratorTest {
|
||||
val takEnabledFlow = MutableStateFlow(true)
|
||||
val takRunningFlow = MutableStateFlow(false)
|
||||
val lifecycleEvents = mutableListOf<String>()
|
||||
every { takServerManager.start(any()) } calls
|
||||
{
|
||||
lifecycleEvents += "start"
|
||||
Unit
|
||||
}
|
||||
every { takServerManager.stop() } calls
|
||||
{
|
||||
lifecycleEvents += "stop"
|
||||
Unit
|
||||
}
|
||||
every { takServerManager.start(any()) } calls { lifecycleEvents += "start" }
|
||||
every { takServerManager.stop() } calls { lifecycleEvents += "stop" }
|
||||
val orchestrator = createOrchestrator(takEnabledFlow = takEnabledFlow, takRunningFlow = takRunningFlow)
|
||||
|
||||
orchestrator.start()
|
||||
|
||||
+4
-6
@@ -20,6 +20,7 @@ import android.Manifest
|
||||
import android.app.Application
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothDevice
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.content.Intent
|
||||
import android.os.Looper
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
@@ -49,12 +50,9 @@ object RobolectricBleBonding {
|
||||
private val application: Application
|
||||
get() = RuntimeEnvironment.getApplication()
|
||||
|
||||
/**
|
||||
* The default adapter Robolectric exposes; production resolves the same one via
|
||||
* [android.bluetooth.BluetoothManager].
|
||||
*/
|
||||
private val adapter: BluetoothAdapter
|
||||
get() = BluetoothAdapter.getDefaultAdapter()
|
||||
/** The adapter Robolectric exposes through [BluetoothManager], the same one production resolves. */
|
||||
val adapter: BluetoothAdapter
|
||||
get() = application.getSystemService(BluetoothManager::class.java).adapter
|
||||
|
||||
/** Grant the runtime permissions [ShadowBluetoothDevice.createBond] checks, so it returns instead of throwing. */
|
||||
fun grantBluetoothConnectPermission() {
|
||||
|
||||
+1
-1
@@ -126,6 +126,6 @@ class FakeBleServiceFailureInjectionTest {
|
||||
|
||||
assertFalse(subscribed, "onSubscription must not run for never-subscribe characteristic")
|
||||
assertNotNull(received, "Notifications must still flow through the bare SharedFlow")
|
||||
assertTrue(received!!.contentEquals(byteArrayOf(1, 2, 3)), "Notification payload must be exposed verbatim")
|
||||
assertTrue(received.contentEquals(byteArrayOf(1, 2, 3)), "Notification payload must be exposed verbatim")
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// No withHostTest: commonTest holds Compose UI tests, which NPE on the host-test stubs' null Build.FINGERPRINT.
|
||||
|
||||
// Required for CMP files/ resources (emoji-data.json) to be packaged as Android assets.
|
||||
// Without this, Res.readBytes() throws MissingResourceException at runtime.
|
||||
android { androidResources.enable = true }
|
||||
|
||||
@@ -47,7 +47,7 @@ fun SharedContactDialog(contact: Node?, onDismiss: () -> Unit, isOwnContact: Boo
|
||||
title = stringResource(Res.string.share_contact),
|
||||
uriString = uriString,
|
||||
onDismiss = onDismiss,
|
||||
subtitle = contact.user?.long_name,
|
||||
subtitle = contact.user.long_name,
|
||||
shareSubject = stringResource(Res.string.share_contact_subject),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.PlainTooltip
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TooltipAnchorPosition
|
||||
import androidx.compose.material3.TooltipBox
|
||||
import androidx.compose.material3.TooltipDefaults
|
||||
import androidx.compose.material3.rememberTooltipState
|
||||
@@ -193,7 +194,7 @@ fun QrDialog(
|
||||
// Copy stays an icon: it is the secondary of the three, and a tooltip carries the
|
||||
// label on desktop, where standards section 4 asks for one on an icon-only control.
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(TooltipAnchorPosition.Above),
|
||||
tooltip = { PlainTooltip { Text(stringResource(Res.string.copy)) } },
|
||||
state = rememberTooltipState(),
|
||||
) {
|
||||
|
||||
@@ -20,8 +20,10 @@ import org.gradle.api.file.FileSystemOperations
|
||||
import org.gradle.process.ExecOperations
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.compose.desktop.application.tasks.AbstractJPackageTask
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.meshtastic.buildlogic.configureGraphTasks
|
||||
import org.meshtastic.buildlogic.kotlinWarningsAsErrors
|
||||
import org.meshtastic.buildlogic.maplibreDesktopRuntime
|
||||
import org.meshtastic.buildlogic.resolveVersionInfo
|
||||
import java.io.File
|
||||
@@ -113,7 +115,8 @@ kotlin {
|
||||
}
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_25)
|
||||
freeCompilerArgs.add("-jvm-default=no-compatibility")
|
||||
jvmDefault.set(JvmDefaultMode.NO_COMPATIBILITY)
|
||||
allWarningsAsErrors.set(kotlinWarningsAsErrors)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -420,7 +423,6 @@ dependencies {
|
||||
implementation(libs.androidx.datastore)
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.sqlite.bundled)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
|
||||
implementation(libs.jna)
|
||||
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ class DesktopNotificationManagerTest {
|
||||
|
||||
assertTrue(dispatched, "Tray fallback acceptance should count as delivery-accepted")
|
||||
assertNotNull(fallback, "Expected fallback notification to be emitted")
|
||||
assertEquals("Fallback", fallback!!.title)
|
||||
assertEquals("Fallback", fallback.title)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -55,5 +55,4 @@ UI for port-37 Mesh Beacon join invitations (`ui/component/`):
|
||||
From `feature/discovery/build.gradle.kts` (`commonMain`):
|
||||
|
||||
- `core:common`, `core:data`, `core:database`, `core:di`, `core:model`, `core:navigation`, `core:network`, `core:prefs`, `core:repository`, `core:resources`, `core:service`, `core:ui`
|
||||
- `kotlinx.collections.immutable`
|
||||
- `org.meshtastic:protobufs` (Maven artifact)
|
||||
@@ -45,7 +45,6 @@ kotlin {
|
||||
implementation(projects.core.service)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.meshtastic.protobufs)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -771,7 +771,7 @@ class DiscoveryScanEngineTest {
|
||||
|
||||
val state = engine.scanState.value
|
||||
assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state")
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Success, (state as DiscoveryScanState.Complete).outcome)
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Success, state.outcome)
|
||||
assertEquals(1, radioController.neighborInfoRequests.size)
|
||||
assertEquals("complete", discoveryDao.sessions.values.single().completionStatus)
|
||||
}
|
||||
@@ -787,7 +787,7 @@ class DiscoveryScanEngineTest {
|
||||
|
||||
val state = engine.scanState.value
|
||||
assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state")
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Success, (state as DiscoveryScanState.Complete).outcome)
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Success, state.outcome)
|
||||
assertEquals(1, radioController.neighborInfoRequests.size)
|
||||
assertEquals("complete", discoveryDao.sessions.values.single().completionStatus)
|
||||
}
|
||||
@@ -809,7 +809,7 @@ class DiscoveryScanEngineTest {
|
||||
// none of which happened before the fix, because cancelScanInternal() cancelled this coroutine first.
|
||||
val state = engine.scanState.value
|
||||
assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state")
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Failed, (state as DiscoveryScanState.Complete).outcome)
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Failed, state.outcome)
|
||||
assertFalse(engine.isActive)
|
||||
assertNull(collectorRegistry.collector, "collector should be unregistered")
|
||||
|
||||
@@ -928,7 +928,7 @@ class DiscoveryScanEngineTest {
|
||||
|
||||
val state = engine.scanState.value
|
||||
assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state")
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Success, (state as DiscoveryScanState.Complete).outcome)
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Success, state.outcome)
|
||||
assertEquals(DiscoverySessionStatus.COMPLETE, discoveryDao.sessions.values.first().completionStatus)
|
||||
}
|
||||
|
||||
@@ -1002,7 +1002,7 @@ class DiscoveryScanEngineTest {
|
||||
|
||||
val state = engine.scanState.value
|
||||
assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state")
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Failed, (state as DiscoveryScanState.Complete).outcome)
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Failed, state.outcome)
|
||||
assertEquals(DiscoverySessionStatus.FAILED, discoveryDao.sessions.values.first().completionStatus)
|
||||
assertEquals(ChannelOption.LONG_FAST.modemPreset, radioController.lastLocalConfig?.lora?.modem_preset)
|
||||
assertTrue(
|
||||
@@ -1162,7 +1162,7 @@ class DiscoveryScanEngineTest {
|
||||
|
||||
val state = engine.scanState.value
|
||||
assertTrue(state is DiscoveryScanState.Complete, "expected Complete, was $state")
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Success, (state as DiscoveryScanState.Complete).outcome)
|
||||
assertEquals(DiscoveryScanState.CompletionOutcome.Success, state.outcome)
|
||||
assertEquals(DiscoverySessionStatus.COMPLETE, discoveryDao.sessions.values.first().completionStatus)
|
||||
assertEquals(ChannelOption.LONG_FAST.modemPreset, radioController.lastLocalConfig?.lora?.modem_preset)
|
||||
}
|
||||
|
||||
@@ -134,8 +134,7 @@ feature:docs
|
||||
├── core:common, core:navigation, core:resources, core:ui, core:di
|
||||
├── coil (image loading in Markdown)
|
||||
├── markdown-renderer-m3 (Compose Markdown rendering)
|
||||
├── compose.material3.adaptive, compose.material3.adaptive.navigation3
|
||||
└── kotlinx.collections.immutable
|
||||
└── compose.material3.adaptive, compose.material3.adaptive.navigation3
|
||||
```
|
||||
|
||||
<!--region graph-->
|
||||
|
||||
@@ -22,6 +22,7 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// No withHostTest: commonTest holds Compose UI tests, which NPE on the host-test stubs' null Build.FINGERPRINT.
|
||||
android {
|
||||
namespace = "org.meshtastic.feature.docs"
|
||||
androidResources.enable = true
|
||||
@@ -35,7 +36,6 @@ kotlin {
|
||||
implementation(projects.core.ui)
|
||||
implementation(projects.core.di)
|
||||
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive)
|
||||
implementation(libs.jetbrains.compose.material3.adaptive.navigation3)
|
||||
implementation(libs.coil)
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ class TranslationCascadeTest {
|
||||
assertIs<TranslationResult.Success>(success)
|
||||
assertIs<TranslationResult.ModelDownloadRequired>(download)
|
||||
assertIs<TranslationResult.Unavailable>(unavailable)
|
||||
assertEquals("es", (download as TranslationResult.ModelDownloadRequired).locale)
|
||||
assertEquals("es", download.locale)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,6 @@ kotlin {
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.coil)
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
implementation(libs.ktor.client.core)
|
||||
implementation(libs.ktor.network)
|
||||
implementation(libs.markdown.renderer)
|
||||
|
||||
+106
-105
@@ -35,115 +35,116 @@ import kotlin.test.assertTrue
|
||||
*/
|
||||
abstract class CommonMaintenanceVolumeTest {
|
||||
|
||||
private val manifestJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
/** The real maintenance-UF2 manifest, embedded verbatim — see UsbMaintenanceGateTest for why. */
|
||||
private val testManifest =
|
||||
Json { ignoreUnknownKeys = true }
|
||||
.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5",
|
||||
"otafixBase": "https://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.3-BP1.5",
|
||||
"erase": {
|
||||
"nrf52": {
|
||||
"6.1.1": {
|
||||
"fileName": "nrf_erase2.uf2",
|
||||
"sha256": "4b778a3def19854415db64cb51bfd29c15b11cc46006353dd518f62d09efe3fe",
|
||||
"expectedFirstTargetAddress": 155648
|
||||
},
|
||||
"7.3.0": {
|
||||
"fileName": "nrf_erase_sd7_3.uf2",
|
||||
"sha256": "13941bedce009e61255c37b1524d11ca604e88c38e7588bb8b391e2998da468f",
|
||||
"expectedFirstTargetAddress": 159744
|
||||
}
|
||||
},
|
||||
"nrf52Bootloader": {
|
||||
"fileName": "meshtastic_factory_erase.uf2",
|
||||
"sha256": "6ef3146505c40079ee9e7e692448e40a793dad636f55d1545063299d28908f0d",
|
||||
"expectedFamilyId": 1296388936
|
||||
},
|
||||
"rp2040": {
|
||||
"fileName": "pico_erase.uf2",
|
||||
"sha256": "08aa7d561e8b8bf2f9b061b3506fb4d8f135e832efe0f3ae978241db2da0c853"
|
||||
}
|
||||
manifestJson.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5",
|
||||
"otafixBase": "https://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.3-BP1.5",
|
||||
"erase": {
|
||||
"nrf52": {
|
||||
"6.1.1": {
|
||||
"fileName": "nrf_erase2.uf2",
|
||||
"sha256": "4b778a3def19854415db64cb51bfd29c15b11cc46006353dd518f62d09efe3fe",
|
||||
"expectedFirstTargetAddress": 155648
|
||||
},
|
||||
"otafixByBoardId": {
|
||||
"HT-n5262": {
|
||||
"otafixBoardSlug": "heltec_t114",
|
||||
"sha256": "ae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19"
|
||||
},
|
||||
"MinewSemi-MX25LE01": {
|
||||
"otafixBoardSlug": "minewsemi_mx25le01",
|
||||
"sha256": "e09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1b"
|
||||
},
|
||||
"TRACKER L1": {
|
||||
"otafixBoardSlug": "wio_tracker_l1",
|
||||
"sha256": "70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9e"
|
||||
},
|
||||
"WisBlock-RAK4631-Board": {
|
||||
"otafixBoardSlug": "wiscore_rak4631_board",
|
||||
"sha256": "8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305"
|
||||
},
|
||||
"WisMesh-Tag": {
|
||||
"otafixBoardSlug": "wismesh_tag",
|
||||
"sha256": "96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758"
|
||||
},
|
||||
"nRF52840-SeeedSenseCAPSolarP1-v1": {
|
||||
"otafixBoardSlug": "sensecap_solar_p1",
|
||||
"sha256": "9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5"
|
||||
},
|
||||
"nRF52840-SeeedXiao-v1": {
|
||||
"otafixBoardSlug": "xiao_nrf52840_ble",
|
||||
"sha256": "ff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67f"
|
||||
},
|
||||
"nRF52840-SeeedXiaoSense-v1": {
|
||||
"otafixBoardSlug": "xiao_nrf52840_ble_sense",
|
||||
"sha256": "fc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40da"
|
||||
},
|
||||
"nRF52840-T1000-E-v1": {
|
||||
"otafixBoardSlug": "t1000_e",
|
||||
"sha256": "5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4"
|
||||
},
|
||||
"nRF52840-TEcho-v1": {
|
||||
"otafixBoardSlug": "lilygo_techo",
|
||||
"sha256": "2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0"
|
||||
},
|
||||
"nRF52840-ThinkNode-M3-v1": {
|
||||
"otafixBoardSlug": "thinknode_m3",
|
||||
"sha256": "bf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708"
|
||||
},
|
||||
"nRF52840-ThinkNodeM1-v1": {
|
||||
"otafixBoardSlug": "thinknode_m1",
|
||||
"sha256": "aa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795"
|
||||
},
|
||||
"nRF52840-ThinkNodeM6-v1": {
|
||||
"otafixBoardSlug": "thinknode_m6",
|
||||
"sha256": "aaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4a"
|
||||
},
|
||||
"nRF52840-promicro": {
|
||||
"otafixBoardSlug": "promicro_nrf52840",
|
||||
"sha256": "46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794"
|
||||
}
|
||||
},
|
||||
"otafixSupportedTargets": [
|
||||
"rak4631",
|
||||
"rak_wismeshtag",
|
||||
"t-echo",
|
||||
"heltec-mesh-node-t114",
|
||||
"nrf52_promicro_diy_tcxo",
|
||||
"thinknode_m1",
|
||||
"thinknode_m3",
|
||||
"thinknode_m6",
|
||||
"tracker-t1000-e",
|
||||
"seeed_wio_tracker_L1",
|
||||
"seeed_wio_tracker_L1_eink",
|
||||
"seeed_solar_node",
|
||||
"seeed_xiao_nrf52840_kit"
|
||||
]
|
||||
"7.3.0": {
|
||||
"fileName": "nrf_erase_sd7_3.uf2",
|
||||
"sha256": "13941bedce009e61255c37b1524d11ca604e88c38e7588bb8b391e2998da468f",
|
||||
"expectedFirstTargetAddress": 159744
|
||||
}
|
||||
},
|
||||
"nrf52Bootloader": {
|
||||
"fileName": "meshtastic_factory_erase.uf2",
|
||||
"sha256": "6ef3146505c40079ee9e7e692448e40a793dad636f55d1545063299d28908f0d",
|
||||
"expectedFamilyId": 1296388936
|
||||
},
|
||||
"rp2040": {
|
||||
"fileName": "pico_erase.uf2",
|
||||
"sha256": "08aa7d561e8b8bf2f9b061b3506fb4d8f135e832efe0f3ae978241db2da0c853"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
},
|
||||
"otafixByBoardId": {
|
||||
"HT-n5262": {
|
||||
"otafixBoardSlug": "heltec_t114",
|
||||
"sha256": "ae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19"
|
||||
},
|
||||
"MinewSemi-MX25LE01": {
|
||||
"otafixBoardSlug": "minewsemi_mx25le01",
|
||||
"sha256": "e09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1b"
|
||||
},
|
||||
"TRACKER L1": {
|
||||
"otafixBoardSlug": "wio_tracker_l1",
|
||||
"sha256": "70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9e"
|
||||
},
|
||||
"WisBlock-RAK4631-Board": {
|
||||
"otafixBoardSlug": "wiscore_rak4631_board",
|
||||
"sha256": "8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305"
|
||||
},
|
||||
"WisMesh-Tag": {
|
||||
"otafixBoardSlug": "wismesh_tag",
|
||||
"sha256": "96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758"
|
||||
},
|
||||
"nRF52840-SeeedSenseCAPSolarP1-v1": {
|
||||
"otafixBoardSlug": "sensecap_solar_p1",
|
||||
"sha256": "9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5"
|
||||
},
|
||||
"nRF52840-SeeedXiao-v1": {
|
||||
"otafixBoardSlug": "xiao_nrf52840_ble",
|
||||
"sha256": "ff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67f"
|
||||
},
|
||||
"nRF52840-SeeedXiaoSense-v1": {
|
||||
"otafixBoardSlug": "xiao_nrf52840_ble_sense",
|
||||
"sha256": "fc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40da"
|
||||
},
|
||||
"nRF52840-T1000-E-v1": {
|
||||
"otafixBoardSlug": "t1000_e",
|
||||
"sha256": "5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4"
|
||||
},
|
||||
"nRF52840-TEcho-v1": {
|
||||
"otafixBoardSlug": "lilygo_techo",
|
||||
"sha256": "2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0"
|
||||
},
|
||||
"nRF52840-ThinkNode-M3-v1": {
|
||||
"otafixBoardSlug": "thinknode_m3",
|
||||
"sha256": "bf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708"
|
||||
},
|
||||
"nRF52840-ThinkNodeM1-v1": {
|
||||
"otafixBoardSlug": "thinknode_m1",
|
||||
"sha256": "aa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795"
|
||||
},
|
||||
"nRF52840-ThinkNodeM6-v1": {
|
||||
"otafixBoardSlug": "thinknode_m6",
|
||||
"sha256": "aaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4a"
|
||||
},
|
||||
"nRF52840-promicro": {
|
||||
"otafixBoardSlug": "promicro_nrf52840",
|
||||
"sha256": "46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794"
|
||||
}
|
||||
},
|
||||
"otafixSupportedTargets": [
|
||||
"rak4631",
|
||||
"rak_wismeshtag",
|
||||
"t-echo",
|
||||
"heltec-mesh-node-t114",
|
||||
"nrf52_promicro_diy_tcxo",
|
||||
"thinknode_m1",
|
||||
"thinknode_m3",
|
||||
"thinknode_m6",
|
||||
"tracker-t1000-e",
|
||||
"seeed_wio_tracker_L1",
|
||||
"seeed_wio_tracker_L1_eink",
|
||||
"seeed_solar_node",
|
||||
"seeed_xiao_nrf52840_kit"
|
||||
]
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
private val treeUri = CommonUri.parse("content://com.android.externalstorage.documents/tree/1234-5678%3A")
|
||||
|
||||
|
||||
+27
-26
@@ -38,33 +38,34 @@ import kotlin.test.assertTrue
|
||||
*/
|
||||
abstract class CommonUsbPassWriterTest {
|
||||
|
||||
private val manifestJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
private val manifest =
|
||||
Json { ignoreUnknownKeys = true }
|
||||
.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5",
|
||||
"otafixBase": "https://example.invalid/otafix",
|
||||
"erase": {
|
||||
"nrf52": {
|
||||
"6.1.1": { "fileName": "nrf_erase2.uf2", "sha256": "00", "expectedFirstTargetAddress": 155648 }
|
||||
},
|
||||
"nrf52Bootloader": {
|
||||
"fileName": "meshtastic_factory_erase.uf2",
|
||||
"sha256": "00",
|
||||
"expectedFamilyId": 1296388936
|
||||
},
|
||||
"rp2040": { "fileName": "pico_erase.uf2", "sha256": "00" }
|
||||
},
|
||||
"otafixByBoardId": {
|
||||
"WisBlock-RAK4631-Board": { "otafixBoardSlug": "wiscore_rak4631_board", "sha256": "00" }
|
||||
},
|
||||
"otafixSupportedTargets": ["rak4631"]
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
manifestJson.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5",
|
||||
"otafixBase": "https://example.invalid/otafix",
|
||||
"erase": {
|
||||
"nrf52": {
|
||||
"6.1.1": { "fileName": "nrf_erase2.uf2", "sha256": "00", "expectedFirstTargetAddress": 155648 }
|
||||
},
|
||||
"nrf52Bootloader": {
|
||||
"fileName": "meshtastic_factory_erase.uf2",
|
||||
"sha256": "00",
|
||||
"expectedFamilyId": 1296388936
|
||||
},
|
||||
"rp2040": { "fileName": "pico_erase.uf2", "sha256": "00" }
|
||||
},
|
||||
"otafixByBoardId": {
|
||||
"WisBlock-RAK4631-Board": { "otafixBoardSlug": "wiscore_rak4631_board", "sha256": "00" }
|
||||
},
|
||||
"otafixSupportedTargets": ["rak4631"]
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
private val treeUri = CommonUri.parse("content://com.android.externalstorage.documents/tree/1234-5678%3A")
|
||||
|
||||
|
||||
+131
-132
@@ -40,119 +40,120 @@ import kotlin.test.assertTrue
|
||||
*/
|
||||
class UsbMaintenanceGateTest {
|
||||
|
||||
private val manifestJson = Json { ignoreUnknownKeys = true }
|
||||
|
||||
/**
|
||||
* The real maintenance-UF2 manifest ([api/data/maintenanceUf2.json] in `meshtastic/api`, embedded verbatim), so
|
||||
* this test suite keeps exercising the exact board/digest table that ships, not a hand-trimmed fixture that could
|
||||
* drift from it silently.
|
||||
*/
|
||||
private val testManifest =
|
||||
Json { ignoreUnknownKeys = true }
|
||||
.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5",
|
||||
"otafixBase": "https://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.3-BP1.5",
|
||||
"erase": {
|
||||
"nrf52": {
|
||||
"6.1.1": {
|
||||
"fileName": "nrf_erase2.uf2",
|
||||
"sha256": "4b778a3def19854415db64cb51bfd29c15b11cc46006353dd518f62d09efe3fe",
|
||||
"expectedFirstTargetAddress": 155648
|
||||
},
|
||||
"7.3.0": {
|
||||
"fileName": "nrf_erase_sd7_3.uf2",
|
||||
"sha256": "13941bedce009e61255c37b1524d11ca604e88c38e7588bb8b391e2998da468f",
|
||||
"expectedFirstTargetAddress": 159744
|
||||
}
|
||||
},
|
||||
"nrf52Bootloader": {
|
||||
"fileName": "meshtastic_factory_erase.uf2",
|
||||
"sha256": "6ef3146505c40079ee9e7e692448e40a793dad636f55d1545063299d28908f0d",
|
||||
"expectedFamilyId": 1296388936
|
||||
},
|
||||
"rp2040": {
|
||||
"fileName": "pico_erase.uf2",
|
||||
"sha256": "08aa7d561e8b8bf2f9b061b3506fb4d8f135e832efe0f3ae978241db2da0c853"
|
||||
}
|
||||
manifestJson.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"manifestVersion": 1,
|
||||
"otafixReleaseTag": "0.9.2-OTAFIX2.3-BP1.5",
|
||||
"otafixBase": "https://github.com/meshtastic/Adafruit_nRF52_Bootloader_OTAFIX/releases/download/0.9.2-OTAFIX2.3-BP1.5",
|
||||
"erase": {
|
||||
"nrf52": {
|
||||
"6.1.1": {
|
||||
"fileName": "nrf_erase2.uf2",
|
||||
"sha256": "4b778a3def19854415db64cb51bfd29c15b11cc46006353dd518f62d09efe3fe",
|
||||
"expectedFirstTargetAddress": 155648
|
||||
},
|
||||
"otafixByBoardId": {
|
||||
"HT-n5262": {
|
||||
"otafixBoardSlug": "heltec_t114",
|
||||
"sha256": "ae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19"
|
||||
},
|
||||
"MinewSemi-MX25LE01": {
|
||||
"otafixBoardSlug": "minewsemi_mx25le01",
|
||||
"sha256": "e09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1b"
|
||||
},
|
||||
"TRACKER L1": {
|
||||
"otafixBoardSlug": "wio_tracker_l1",
|
||||
"sha256": "70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9e"
|
||||
},
|
||||
"WisBlock-RAK4631-Board": {
|
||||
"otafixBoardSlug": "wiscore_rak4631_board",
|
||||
"sha256": "8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305"
|
||||
},
|
||||
"WisMesh-Tag": {
|
||||
"otafixBoardSlug": "wismesh_tag",
|
||||
"sha256": "96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758"
|
||||
},
|
||||
"nRF52840-SeeedSenseCAPSolarP1-v1": {
|
||||
"otafixBoardSlug": "sensecap_solar_p1",
|
||||
"sha256": "9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5"
|
||||
},
|
||||
"nRF52840-SeeedXiao-v1": {
|
||||
"otafixBoardSlug": "xiao_nrf52840_ble",
|
||||
"sha256": "ff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67f"
|
||||
},
|
||||
"nRF52840-SeeedXiaoSense-v1": {
|
||||
"otafixBoardSlug": "xiao_nrf52840_ble_sense",
|
||||
"sha256": "fc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40da"
|
||||
},
|
||||
"nRF52840-T1000-E-v1": {
|
||||
"otafixBoardSlug": "t1000_e",
|
||||
"sha256": "5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4"
|
||||
},
|
||||
"nRF52840-TEcho-v1": {
|
||||
"otafixBoardSlug": "lilygo_techo",
|
||||
"sha256": "2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0"
|
||||
},
|
||||
"nRF52840-ThinkNode-M3-v1": {
|
||||
"otafixBoardSlug": "thinknode_m3",
|
||||
"sha256": "bf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708"
|
||||
},
|
||||
"nRF52840-ThinkNodeM1-v1": {
|
||||
"otafixBoardSlug": "thinknode_m1",
|
||||
"sha256": "aa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795"
|
||||
},
|
||||
"nRF52840-ThinkNodeM6-v1": {
|
||||
"otafixBoardSlug": "thinknode_m6",
|
||||
"sha256": "aaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4a"
|
||||
},
|
||||
"nRF52840-promicro": {
|
||||
"otafixBoardSlug": "promicro_nrf52840",
|
||||
"sha256": "46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794"
|
||||
}
|
||||
},
|
||||
"otafixSupportedTargets": [
|
||||
"rak4631",
|
||||
"rak_wismeshtag",
|
||||
"t-echo",
|
||||
"heltec-mesh-node-t114",
|
||||
"nrf52_promicro_diy_tcxo",
|
||||
"thinknode_m1",
|
||||
"thinknode_m3",
|
||||
"thinknode_m6",
|
||||
"tracker-t1000-e",
|
||||
"seeed_wio_tracker_L1",
|
||||
"seeed_wio_tracker_L1_eink",
|
||||
"seeed_solar_node",
|
||||
"seeed_xiao_nrf52840_kit"
|
||||
]
|
||||
"7.3.0": {
|
||||
"fileName": "nrf_erase_sd7_3.uf2",
|
||||
"sha256": "13941bedce009e61255c37b1524d11ca604e88c38e7588bb8b391e2998da468f",
|
||||
"expectedFirstTargetAddress": 159744
|
||||
}
|
||||
},
|
||||
"nrf52Bootloader": {
|
||||
"fileName": "meshtastic_factory_erase.uf2",
|
||||
"sha256": "6ef3146505c40079ee9e7e692448e40a793dad636f55d1545063299d28908f0d",
|
||||
"expectedFamilyId": 1296388936
|
||||
},
|
||||
"rp2040": {
|
||||
"fileName": "pico_erase.uf2",
|
||||
"sha256": "08aa7d561e8b8bf2f9b061b3506fb4d8f135e832efe0f3ae978241db2da0c853"
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
},
|
||||
"otafixByBoardId": {
|
||||
"HT-n5262": {
|
||||
"otafixBoardSlug": "heltec_t114",
|
||||
"sha256": "ae92d3577cb58dd9b43c9b61ffb9bfffda05b0eca4113a0ec42a37cd8be53b19"
|
||||
},
|
||||
"MinewSemi-MX25LE01": {
|
||||
"otafixBoardSlug": "minewsemi_mx25le01",
|
||||
"sha256": "e09564fd8dd03fc25d76dcb732a0214c79653da3b130240949b783254d3dfc1b"
|
||||
},
|
||||
"TRACKER L1": {
|
||||
"otafixBoardSlug": "wio_tracker_l1",
|
||||
"sha256": "70fbce0eda9d70d7bd8a4367057badf5ec310838bf3221370d45a56f04956b9e"
|
||||
},
|
||||
"WisBlock-RAK4631-Board": {
|
||||
"otafixBoardSlug": "wiscore_rak4631_board",
|
||||
"sha256": "8741bc677a3c24f28422c5ffb80761de7d98a127a3b0191ba6585bf57ce9f305"
|
||||
},
|
||||
"WisMesh-Tag": {
|
||||
"otafixBoardSlug": "wismesh_tag",
|
||||
"sha256": "96d42e1990e17251e8c625e98a1551cac12c6e29111bc2e59ab7c9fe6dec8758"
|
||||
},
|
||||
"nRF52840-SeeedSenseCAPSolarP1-v1": {
|
||||
"otafixBoardSlug": "sensecap_solar_p1",
|
||||
"sha256": "9b4bce48c1b4830617715c5619457bce6b21f3079803e35e13433de7701290f5"
|
||||
},
|
||||
"nRF52840-SeeedXiao-v1": {
|
||||
"otafixBoardSlug": "xiao_nrf52840_ble",
|
||||
"sha256": "ff8a0916e98cceb394fd66590bccc17f63612c11ff56b086ef88bd436c8df67f"
|
||||
},
|
||||
"nRF52840-SeeedXiaoSense-v1": {
|
||||
"otafixBoardSlug": "xiao_nrf52840_ble_sense",
|
||||
"sha256": "fc233d83a1011419625fcb50b49084578460c25bbc0270374ca176757a3c40da"
|
||||
},
|
||||
"nRF52840-T1000-E-v1": {
|
||||
"otafixBoardSlug": "t1000_e",
|
||||
"sha256": "5c065e11b8acd5b0cefa9295f98bca1512306cfa478856aa76a871124a904cc4"
|
||||
},
|
||||
"nRF52840-TEcho-v1": {
|
||||
"otafixBoardSlug": "lilygo_techo",
|
||||
"sha256": "2ddb36188ffe521c270bb2ce8441d742d0fe45325c57e4db6475bf63162a59b0"
|
||||
},
|
||||
"nRF52840-ThinkNode-M3-v1": {
|
||||
"otafixBoardSlug": "thinknode_m3",
|
||||
"sha256": "bf90979f2f6adc96ef6ca09c280b2ab7e66cb8ce2654fc80da9b20407bfb8708"
|
||||
},
|
||||
"nRF52840-ThinkNodeM1-v1": {
|
||||
"otafixBoardSlug": "thinknode_m1",
|
||||
"sha256": "aa0721b573c60e0b179274d5a5296bac7a8436faf339cfc03116ebe8a4375795"
|
||||
},
|
||||
"nRF52840-ThinkNodeM6-v1": {
|
||||
"otafixBoardSlug": "thinknode_m6",
|
||||
"sha256": "aaf94953a540a18f3e48f4cdec0c78290ad3c5f8740aea26fa3b3ce3632a8d4a"
|
||||
},
|
||||
"nRF52840-promicro": {
|
||||
"otafixBoardSlug": "promicro_nrf52840",
|
||||
"sha256": "46ef3440f151d6f2606075bcd1aa83db25a660da7d25b988aeb47ef350c98794"
|
||||
}
|
||||
},
|
||||
"otafixSupportedTargets": [
|
||||
"rak4631",
|
||||
"rak_wismeshtag",
|
||||
"t-echo",
|
||||
"heltec-mesh-node-t114",
|
||||
"nrf52_promicro_diy_tcxo",
|
||||
"thinknode_m1",
|
||||
"thinknode_m3",
|
||||
"thinknode_m6",
|
||||
"tracker-t1000-e",
|
||||
"seeed_wio_tracker_L1",
|
||||
"seeed_wio_tracker_L1_eink",
|
||||
"seeed_solar_node",
|
||||
"seeed_xiao_nrf52840_kit"
|
||||
]
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
private fun nrf(
|
||||
variant: SoftDeviceVariant? = SoftDeviceVariant.S140_6_1_1,
|
||||
@@ -212,20 +213,19 @@ class UsbMaintenanceGateTest {
|
||||
@Test
|
||||
fun `an unsafe erase filename refuses the image instead of throwing`() {
|
||||
val hostile =
|
||||
Json { ignoreUnknownKeys = true }
|
||||
.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"erase": {
|
||||
"nrf52": {
|
||||
"6.1.1": { "fileName": "../../etc/passwd", "sha256": "00" }
|
||||
},
|
||||
"rp2040": { "fileName": "sub/dir/pico_erase.uf2", "sha256": "00" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
manifestJson.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"erase": {
|
||||
"nrf52": {
|
||||
"6.1.1": { "fileName": "../../etc/passwd", "sha256": "00" }
|
||||
},
|
||||
"rp2040": { "fileName": "sub/dir/pico_erase.uf2", "sha256": "00" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
assertNull(eraseUf2For(hostile, nrf()), "A traversal fileName must resolve to null, not throw")
|
||||
assertNull(eraseUf2For(hostile, rp2040()), "A separator in fileName must resolve to null, not throw")
|
||||
@@ -238,19 +238,18 @@ class UsbMaintenanceGateTest {
|
||||
@Test
|
||||
fun `an unsafe otafix slug or tag refuses the image instead of throwing`() {
|
||||
val hostile =
|
||||
Json { ignoreUnknownKeys = true }
|
||||
.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"otafixReleaseTag": "../../../evil",
|
||||
"otafixBase": "https://example.invalid/releases",
|
||||
"otafixByBoardId": {
|
||||
"rak4631": { "otafixBoardSlug": "rak4631", "sha256": "00" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
manifestJson.decodeFromString<MaintenanceUf2Manifest>(
|
||||
"""
|
||||
{
|
||||
"otafixReleaseTag": "../../../evil",
|
||||
"otafixBase": "https://example.invalid/releases",
|
||||
"otafixByBoardId": {
|
||||
"rak4631": { "otafixBoardSlug": "rak4631", "sha256": "00" }
|
||||
}
|
||||
}
|
||||
"""
|
||||
.trimIndent(),
|
||||
)
|
||||
|
||||
assertNull(otafixUf2ForBoardId(hostile, "rak4631"), "A traversal release tag must resolve to null, not throw")
|
||||
}
|
||||
|
||||
-2
@@ -374,7 +374,6 @@ class Esp32OtaUpdateHandlerTest {
|
||||
}
|
||||
assertTrue(events.none { it.startsWith("start:") || it.startsWith("stream:") })
|
||||
assertIs<FirmwareUpdateState.Error>(states.lastOrNull())
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,7 +407,6 @@ class Esp32OtaUpdateHandlerTest {
|
||||
}
|
||||
assertTrue(events.none { it.startsWith("start:") || it.startsWith("stream:") })
|
||||
assertIs<FirmwareUpdateState.Error>(states.lastOrNull())
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+15
-11
@@ -23,6 +23,7 @@ import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.ExperimentalForInheritanceCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
@@ -429,26 +430,28 @@ class LegacyDfuTransportTest {
|
||||
*/
|
||||
var throwExceptionOnControlPointWrites: Boolean = false
|
||||
|
||||
override fun hasCharacteristic(c: BleCharacteristic) = delegate.hasCharacteristic(c)
|
||||
override fun hasCharacteristic(characteristic: BleCharacteristic) = delegate.hasCharacteristic(characteristic)
|
||||
|
||||
override fun observe(c: BleCharacteristic): Flow<ByteArray> = delegate.observe(c)
|
||||
override fun observe(characteristic: BleCharacteristic): Flow<ByteArray> = delegate.observe(characteristic)
|
||||
|
||||
override suspend fun read(c: BleCharacteristic): ByteArray = delegate.read(c)
|
||||
override suspend fun read(characteristic: BleCharacteristic): ByteArray = delegate.read(characteristic)
|
||||
|
||||
override fun preferredWriteType(c: BleCharacteristic): BleWriteType = delegate.preferredWriteType(c)
|
||||
override fun preferredWriteType(characteristic: BleCharacteristic): BleWriteType =
|
||||
delegate.preferredWriteType(characteristic)
|
||||
|
||||
override suspend fun write(c: BleCharacteristic, data: ByteArray, writeType: BleWriteType) {
|
||||
if (throwErrorOnControlPointWrites && c.uuid == LegacyDfuUuids.CONTROL_POINT) {
|
||||
override suspend fun write(characteristic: BleCharacteristic, data: ByteArray, writeType: BleWriteType) {
|
||||
val isControlPoint = characteristic.uuid == LegacyDfuUuids.CONTROL_POINT
|
||||
if (throwErrorOnControlPointWrites && isControlPoint) {
|
||||
throw AssertionError("Simulated assertion failure during control point write")
|
||||
}
|
||||
if (throwExceptionOnControlPointWrites && c.uuid == LegacyDfuUuids.CONTROL_POINT) {
|
||||
if (throwExceptionOnControlPointWrites && isControlPoint) {
|
||||
throw RuntimeException("Simulated link failure during control point write")
|
||||
}
|
||||
if (hangOnControlPointWrites && c.uuid == LegacyDfuUuids.CONTROL_POINT) {
|
||||
if (hangOnControlPointWrites && isControlPoint) {
|
||||
awaitCancellation()
|
||||
}
|
||||
delegate.write(c, data, writeType)
|
||||
val response = responder.onWrite(c.uuid, data) ?: return
|
||||
delegate.write(characteristic, data, writeType)
|
||||
val response = responder.onWrite(characteristic.uuid, data) ?: return
|
||||
response.forEach { delegate.emitNotification(LegacyDfuUuids.CONTROL_POINT, it) }
|
||||
}
|
||||
}
|
||||
@@ -644,6 +647,7 @@ class LegacyDfuTransportTest {
|
||||
* where the write throws before the watcher processes the emission), while `bleConnection.connectionState.value`
|
||||
* still reads as Disconnected for the write-catch classification.
|
||||
*/
|
||||
@OptIn(ExperimentalForInheritanceCoroutinesApi::class)
|
||||
private class DisconnectEmissionsSuppressedStateFlow(private val delegate: StateFlow<BleConnectionState>) :
|
||||
StateFlow<BleConnectionState> {
|
||||
override val value: BleConnectionState
|
||||
@@ -971,7 +975,7 @@ class LegacyDfuTransportTest {
|
||||
// parentJob stands in for the caller's scope; cancelling it must propagate through withTimeoutOrNull
|
||||
// (which only swallows its own TimeoutCancellationException, not parent cancellation) and out of abort.
|
||||
val parentJob = Job()
|
||||
val abortDeferred = async(parentJob) { env.transport.abort() }
|
||||
val abortDeferred = CoroutineScope(coroutineContext + parentJob).async { env.transport.abort() }
|
||||
|
||||
// Let abort reach the hanging RESET write.
|
||||
runCurrent()
|
||||
|
||||
+2
-2
@@ -21,12 +21,12 @@ import ch.poole.geo.pmtiles.HttpUrlConnectionChannel
|
||||
import ch.poole.geo.pmtiles.Reader
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.net.URI
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
actual class TerrainTileFetcher actual constructor(pmtilesUrl: String) : AutoCloseable {
|
||||
|
||||
private val reader: Reader = Reader(HttpUrlConnectionChannel(URL(pmtilesUrl)))
|
||||
private val reader: Reader = Reader(HttpUrlConnectionChannel(URI(pmtilesUrl).toURL()))
|
||||
|
||||
actual fun fetchTile(zoom: Int, x: Int, y: Int): ByteArray? {
|
||||
val raw = reader.getTile(zoom, x, y) ?: return null
|
||||
|
||||
+2
-2
@@ -21,12 +21,12 @@ import ch.poole.geo.pmtiles.HttpUrlConnectionChannel
|
||||
import ch.poole.geo.pmtiles.Reader
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.net.URL
|
||||
import java.net.URI
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
actual class TerrainTileFetcher actual constructor(pmtilesUrl: String) : AutoCloseable {
|
||||
|
||||
private val reader: Reader = Reader(HttpUrlConnectionChannel(URL(pmtilesUrl)))
|
||||
private val reader: Reader = Reader(HttpUrlConnectionChannel(URI(pmtilesUrl).toURL()))
|
||||
|
||||
actual fun fetchTile(zoom: Int, x: Int, y: Int): ByteArray? {
|
||||
val raw = reader.getTile(zoom, x, y) ?: return null
|
||||
|
||||
@@ -28,7 +28,6 @@ kotlin {
|
||||
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(libs.kotlinx.collections.immutable)
|
||||
// KML import parses through the same xmlutil the app already resolves for CoT XML.
|
||||
implementation(libs.xmlutil.core)
|
||||
implementation(projects.core.data)
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ class KmlGroundOverlayTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an overlay-only document is not "nothing mappable"`() {
|
||||
fun `an overlay-only document still counts as mappable`() {
|
||||
val result =
|
||||
KmlToGeoJson.convertDocument(
|
||||
"""
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
plugins { alias(libs.plugins.meshtastic.kmp.feature) }
|
||||
|
||||
kotlin {
|
||||
// No withHostTest: commonTest holds Compose UI tests, which NPE on the host-test stubs' null Build.FINGERPRINT.
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(projects.core.common)
|
||||
|
||||
@@ -22,6 +22,7 @@ plugins {
|
||||
}
|
||||
|
||||
kotlin {
|
||||
// No withHostTest: commonTest holds Compose UI tests, which NPE on the host-test stubs' null Build.FINGERPRINT.
|
||||
sourceSets {
|
||||
commonMain.dependencies {
|
||||
implementation(projects.core.common)
|
||||
|
||||
+4
-4
@@ -1125,7 +1125,7 @@ class RadioConfigViewModelTest {
|
||||
}
|
||||
every { processRadioResponseUseCase(any(), 123, any()) } calls
|
||||
{
|
||||
val pendingRequestIds = it.args[2] as Set<Int>
|
||||
val pendingRequestIds = it.arg<Set<Int>>(2)
|
||||
if (42 in pendingRequestIds) RadioResponseResult.Owner(owner) else null
|
||||
}
|
||||
|
||||
@@ -1287,8 +1287,8 @@ class RadioConfigViewModelTest {
|
||||
// Channel A (index 1) completed before channel B (index 2) threw.
|
||||
assertEquals(listOf(1, 2), writtenIndexes)
|
||||
assertNotNull(interrupted)
|
||||
assertEquals(1, interrupted!!.appliedWriteCount)
|
||||
assertEquals("A", interrupted!!.appliedSettings[1].name)
|
||||
assertEquals(1, interrupted.appliedWriteCount)
|
||||
assertEquals("A", interrupted.appliedSettings[1].name)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -1938,7 +1938,7 @@ class RadioConfigViewModelTest {
|
||||
var response: RadioResponseResult = RadioResponseResult.Error(maxRetransmit, Routing.Error.MAX_RETRANSMIT)
|
||||
every { processRadioResponseUseCase(any(), 456, any()) } calls
|
||||
{
|
||||
val pendingRequestIds = it.args[2] as Set<Int>
|
||||
val pendingRequestIds = it.arg<Set<Int>>(2)
|
||||
if (42 in pendingRequestIds) response else null
|
||||
}
|
||||
nodeRepository.setNodes(listOf(localNode, remoteNode))
|
||||
|
||||
+3
-3
@@ -1,6 +1,4 @@
|
||||
# --- Android build features ---
|
||||
android.defaults.buildfeatures.resvalues=false
|
||||
android.defaults.buildfeatures.shaders=false
|
||||
# --- Android ---
|
||||
# Lint analysis runs in its own worker, so neither org.gradle.jvmargs nor
|
||||
# kotlin.daemon.jvmargs applies to it. The default heap OOMs on a cold analysis.
|
||||
android.experimental.lint.heapSize=3G
|
||||
@@ -20,6 +18,8 @@ kotlin.daemon.useFallback=false
|
||||
# The Kotlin/Native compiler runs in its own JVM, governed by neither of the heaps
|
||||
# above. Its 3g default OOMs compiling every module for iOS from a cold cache.
|
||||
kotlin.native.jvmArgs=-Xmx5g -XX\:+UseG1GC -XX\:+HeapDumpOnOutOfMemoryError
|
||||
# Only macOS can run iosSimulatorArm64Test; this hides KGP's notice about it on Linux and Windows hosts.
|
||||
kotlin.native.ignoreDisabledTargets=true
|
||||
|
||||
# --- KSP ---
|
||||
# (incremental processing is default-on in KSP2; only the non-default flag is declared)
|
||||
|
||||
@@ -34,7 +34,7 @@ ktfmt = "0.61"
|
||||
kover = "0.9.10"
|
||||
mokkery = "3.5.0"
|
||||
junit5 = "6.1.3"
|
||||
junit-platform = "6.1.3" # aligned with junit5 — JUnit Platform uses 1.x scheme
|
||||
junit-platform = "6.1.3" # JUnit Platform shares the junit5 version since JUnit 6
|
||||
kotest = "6.2.5"
|
||||
konsist = "0.17.3"
|
||||
turbine = "1.2.1"
|
||||
@@ -44,8 +44,7 @@ compose-screenshot = "0.0.1-alpha16"
|
||||
|
||||
# Baseline Profiles / Macrobenchmark
|
||||
# `benchmark` drives both the androidx.benchmark macro lib AND the androidx.baselineprofile
|
||||
# Gradle plugin (same coordinates/version). Kept on the alpha track to stay compatible with
|
||||
# AGP 9.x (the stable 1.4.x line predates AGP 9 support).
|
||||
# Gradle plugin (same coordinates/version). The stable 1.4.x line predates AGP 9 support.
|
||||
benchmark = "1.5.0"
|
||||
profileinstaller = "1.4.1"
|
||||
androidx-uiautomator = "2.4.0"
|
||||
@@ -113,7 +112,6 @@ meshtastic-protobufs = "2.8.0.123-gadda936-SNAPSHOT"
|
||||
# Gradle Plugins
|
||||
ccud = "2.8.0"
|
||||
develocity = "4.6.0"
|
||||
foojay-resolver = "1.0.0"
|
||||
|
||||
|
||||
[libraries]
|
||||
@@ -381,7 +379,6 @@ dd-sdk-android = [
|
||||
# Android
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
android-kotlin-multiplatform-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
|
||||
android-test = { id = "com.android.test" }
|
||||
androidx-baselineprofile = { id = "androidx.baselineprofile", version.ref = "benchmark" }
|
||||
compose-screenshot = { id = "com.android.compose.screenshot", version.ref = "compose-screenshot" }
|
||||
|
||||
@@ -410,9 +407,6 @@ dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
|
||||
room = { id = "androidx.room3", version.ref = "room" }
|
||||
spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
|
||||
|
||||
develocity = { id = "com.gradle.develocity", version.ref = "develocity" }
|
||||
foojay-resolver = { id = "org.gradle.toolchains.foojay-resolver", version.ref = "foojay-resolver" }
|
||||
|
||||
# Meshtastic
|
||||
meshtastic-android-application = { id = "meshtastic.android.application" }
|
||||
meshtastic-android-application-compose = { id = "meshtastic.android.application.compose" }
|
||||
@@ -424,6 +418,7 @@ meshtastic-android-room = { id = "meshtastic.android.room" }
|
||||
meshtastic-android-screenshot = { id = "meshtastic.android.screenshot" }
|
||||
# Vendored google/secrets-gradle-plugin (Isolated-Projects-safe) — see build-logic/convention.
|
||||
meshtastic-android-secrets = { id = "meshtastic.android.secrets" }
|
||||
meshtastic-android-test = { id = "meshtastic.android.test" }
|
||||
meshtastic-detekt = { id = "meshtastic.detekt" }
|
||||
meshtastic-docs = { id = "meshtastic.docs" }
|
||||
meshtastic-koin = { id = "meshtastic.koin" }
|
||||
@@ -435,5 +430,4 @@ meshtastic-kmp-library = { id = "meshtastic.kmp.library" }
|
||||
meshtastic-kmp-library-compose = { id = "meshtastic.kmp.library.compose" }
|
||||
meshtastic-root = { id = "meshtastic.root" }
|
||||
meshtastic-spotless = { id = "meshtastic.spotless" }
|
||||
meshtastic-publishing = { id = "meshtastic.publishing" }
|
||||
meshtastic-aboutlibraries = { id = "meshtastic.aboutlibraries" }
|
||||
+4
-5
@@ -2,11 +2,10 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
# -bin: nothing here reads -all's docs/sources. Update sha256 with the URL on upgrades
|
||||
# (official .sha256 sits next to the distribution; renovate does both).
|
||||
# On 9.7.1 again after the #6786 pin-back. Both of that revert's blockers must stay dead
|
||||
# on any future 9.7+ bump: the CC fingerprint crashing on reload of its own same-key entry
|
||||
# (IsInIdeaSyncValueSource CNFE in the settings-plugin scope), and CMP's Windows packaging
|
||||
# tripping an Isolated Projects violation (':desktopApp' cannot access Project.layout on
|
||||
# ':'). The first only shows on a CC cache-key HIT, so one green run does not clear it.
|
||||
# Every bump must keep two failures dead: the CC fingerprint crashing on reload of its own
|
||||
# same-key entry (IsInIdeaSyncValueSource CNFE in the settings-plugin scope), and CMP's Windows
|
||||
# packaging tripping an Isolated Projects violation (':desktopApp' cannot access Project.layout
|
||||
# on ':'). The first only shows on a CC cache-key HIT, so one green run does not clear it.
|
||||
distributionSha256Sum=bafd5ce9cfaea0fbccfdc8439a1ac42fbd4cd9c89dc9a988228d8a2639a58e6c
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.8.0-bin.zip
|
||||
networkTimeout=30000
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.meshtastic.buildlogic.kotlinWarningsAsErrors
|
||||
|
||||
// Writes core/resources/.../values/schema_strings.xml: every label and description in the protobufs field-metadata
|
||||
// registry, keyed by schema path (`schema_lora_hop_limit`). A settings control that edits a schema field names that
|
||||
@@ -31,7 +32,10 @@ kotlin {
|
||||
languageVersion.set(JavaLanguageVersion.of(25))
|
||||
vendor.set(JvmVendorSpec.JETBRAINS)
|
||||
}
|
||||
compilerOptions { jvmTarget.set(JvmTarget.JVM_25) }
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_25)
|
||||
allWarningsAsErrors.set(kotlinWarningsAsErrors)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
// Captures the store-listing screenshots from the real debug app on a device or emulator:
|
||||
// ./gradlew :store-screenshots:connectedGoogleDebugAndroidTest (Play)
|
||||
@@ -24,25 +23,15 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
// adb pull /data/local/tmp/store-screenshots/<flavor>/. <dir>
|
||||
// .github/workflows/store-screenshots.yml does this for both flavors on one emulator.
|
||||
plugins {
|
||||
alias(libs.plugins.android.test)
|
||||
alias(libs.plugins.meshtastic.android.test)
|
||||
alias(libs.plugins.meshtastic.detekt)
|
||||
alias(libs.plugins.meshtastic.spotless)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "org.meshtastic.storescreenshots"
|
||||
compileSdk = 37
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 28
|
||||
targetSdk = 37
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
defaultConfig { minSdk = 28 }
|
||||
|
||||
targetProjectPath = ":androidApp"
|
||||
// Its own process, so relaunching the app between form factors does not end the run.
|
||||
@@ -68,8 +57,6 @@ tasks
|
||||
.named { it == "connectedFdroidDebugAndroidTest" }
|
||||
.configureEach { mustRunAfter("connectedGoogleDebugAndroidTest") }
|
||||
|
||||
kotlin { compilerOptions { jvmTarget.set(JvmTarget.JVM_21) } }
|
||||
|
||||
dependencies {
|
||||
implementation(libs.androidx.test.ext.junit)
|
||||
implementation(libs.androidx.test.runner)
|
||||
|
||||
Reference in new issue
Block a user