From 1b128ffc787180d1b81d899a4069dc3cb269f392 Mon Sep 17 00:00:00 2001 From: James Rich <2199651+jamesarich@users.noreply.github.com> Date: Tue, 29 Sep 2026 10:48:55 +0000 Subject: [PATCH] build: clear Kotlin warnings, time out hung tests, drop dead config (#7435) --- .github/ci-gradle.properties | 3 - .../instructions/build-logic.instructions.md | 2 +- .github/renovate.json | 26 ++ .skills/testing-ci/SKILL.md | 4 +- androidApp/build.gradle.kts | 2 - .../app/map/component/WaypointMarkers.kt | 3 + .../app/map/offline/pmtiles/VectorTile.kt | 3 + baselineprofile/build.gradle.kts | 13 +- build-logic/convention/build.gradle.kts | 10 +- .../kotlin/AboutLibrariesConventionPlugin.kt | 11 +- .../kotlin/AndroidLintConventionPlugin.kt | 2 - .../kotlin/AndroidTestConventionPlugin.kt | 32 +++ .../meshtastic/buildlogic/KotlinAndroid.kt | 50 +++- .../buildlogic/ProjectExtensions.kt | 16 +- build-logic/gradle.properties | 19 +- .../ble/AndroidBluetoothRepositoryBondTest.kt | 9 +- .../org/meshtastic/core/common/util/Crc32.kt | 1 + .../manager/MeshConnectionManagerImplTest.kt | 3 - .../core/database/DatabaseManager.kt | 2 + .../database/dao/CommonNodeInfoDaoTest.kt | 10 +- core/domain/README.md | 4 - core/domain/build.gradle.kts | 2 - .../EnsureRemoteAdminSessionUseCaseTest.kt | 24 +- core/navigation/build.gradle.kts | 1 + .../core/network/radio/MockRadioTransport.kt | 2 +- .../network/repository/MQTTRepositoryImpl.kt | 2 +- ...NotificationManagerImplConversationTest.kt | 12 +- .../service/MeshServiceOrchestratorTest.kt | 24 +- .../core/testing/RobolectricBleBonding.kt | 10 +- .../FakeBleServiceFailureInjectionTest.kt | 2 +- core/ui/build.gradle.kts | 2 + .../core/ui/component/ContactSharing.kt | 2 +- .../meshtastic/core/ui/component/QrDialog.kt | 3 +- desktopApp/build.gradle.kts | 6 +- .../DesktopNotificationManagerTest.kt | 2 +- feature/discovery/README.md | 1 - feature/discovery/build.gradle.kts | 1 - .../discovery/DiscoveryScanEngineTest.kt | 12 +- feature/docs/README.md | 3 +- feature/docs/build.gradle.kts | 2 +- .../translation/TranslationCascadeTest.kt | 2 +- feature/firmware/build.gradle.kts | 1 - .../firmware/CommonMaintenanceVolumeTest.kt | 211 +++++++------- .../firmware/CommonUsbPassWriterTest.kt | 53 ++-- .../firmware/UsbMaintenanceGateTest.kt | 263 +++++++++--------- .../firmware/ota/Esp32OtaUpdateHandlerTest.kt | 2 - .../ota/dfu/LegacyDfuTransportTest.kt | 26 +- .../map/terrain/TerrainTileFetcher.android.kt | 4 +- .../map/terrain/TerrainTileFetcher.jvm.kt | 4 +- feature/map/build.gradle.kts | 1 - .../feature/map/kml/KmlGroundOverlayTest.kt | 2 +- feature/messaging/build.gradle.kts | 1 + feature/settings/build.gradle.kts | 1 + .../radio/RadioConfigViewModelTest.kt | 8 +- gradle.properties | 6 +- gradle/libs.versions.toml | 12 +- gradle/wrapper/gradle-wrapper.properties | 9 +- schema-strings/build.gradle.kts | 6 +- store-screenshots/build.gradle.kts | 17 +- 59 files changed, 499 insertions(+), 468 deletions(-) create mode 100644 build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties index d3c3fdf36d..cd638cd83e 100644 --- a/.github/ci-gradle.properties +++ b/.github/ci-gradle.properties @@ -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 diff --git a/.github/instructions/build-logic.instructions.md b/.github/instructions/build-logic.instructions.md index 548bf6fd57..66a1ade4f8 100644 --- a/.github/instructions/build-logic.instructions.md +++ b/.github/instructions/build-logic.instructions.md @@ -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`. diff --git a/.github/renovate.json b/.github/renovate.json index 9012e88722..bc9a6b3bb3 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -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*\"(?[^\"]+)\"" + ], + "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*\"(?[^\"]+)\"" + ], + "depNameTemplate": "com.facebook:ktfmt", + "datasourceTemplate": "maven", + "versioningTemplate": "maven" } ] } diff --git a/.skills/testing-ci/SKILL.md b/.skills/testing-ci/SKILL.md index bee796c76c..8d27d0aa7d 100644 --- a/.skills/testing-ci/SKILL.md +++ b/.skills/testing-ci/SKILL.md @@ -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. diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index a5f92525a5..c29c3c2429 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -124,8 +124,6 @@ configure { ) } 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. diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt index 0bb8cb9118..4e61583265 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/component/WaypointMarkers.kt @@ -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, diff --git a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/VectorTile.kt b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/VectorTile.kt index 424e15961a..4355b34553 100644 --- a/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/VectorTile.kt +++ b/androidApp/src/google/kotlin/org/meshtastic/app/map/offline/pmtiles/VectorTile.kt @@ -14,8 +14,11 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ +@file:OptIn(ExperimentalSerializationApi::class) + package org.meshtastic.app.map.offline.pmtiles +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.protobuf.ProtoNumber diff --git a/baselineprofile/build.gradle.kts b/baselineprofile/build.gradle.kts index c2cb86d866..dc1bebb433 100644 --- a/baselineprofile/build.gradle.kts +++ b/baselineprofile/build.gradle.kts @@ -14,28 +14,19 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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. diff --git a/build-logic/convention/build.gradle.kts b/build-logic/convention/build.gradle.kts index b48ff5d8a3..c7c21ca9ba 100644 --- a/build-logic/convention/build.gradle.kts +++ b/build-logic/convention/build.gradle.kts @@ -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" diff --git a/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt index 7d197a54bd..d4f6ec2446 100644 --- a/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AboutLibrariesConventionPlugin.kt @@ -46,11 +46,12 @@ class AboutLibrariesConventionPlugin : Plugin { 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")) diff --git a/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt index cf8292711d..486b8785e1 100644 --- a/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidLintConventionPlugin.kt @@ -51,8 +51,6 @@ class AndroidLintConventionPlugin : Plugin { } private fun Lint.configure(@Suppress("UnusedParameter") project: Project) { - xmlReport = true - sarifReport = true checkDependencies = true abortOnError = false disable += "GradleDependency" diff --git a/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt new file mode 100644 index 0000000000..81f7d0ba87 --- /dev/null +++ b/build-logic/convention/src/main/kotlin/AndroidTestConventionPlugin.kt @@ -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 . + */ +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 { + override fun apply(target: Project) { + with(target) { + apply(plugin = "com.android.test") + + extensions.configure { configureKotlinAndroidTest(this) } + } + } +} diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt index 686b8d0bc6..4ff165ce14 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/KotlinAndroid.kt @@ -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() } +/** 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() +} + /** * Whether the current build host can run the Kotlin/Native compiler. * @@ -216,14 +233,22 @@ internal fun Project.configureKotlinJvm() { configureKotlin() } +/** + * `-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 + 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 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 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>().configureEach { + compilerOptions.allWarningsAsErrors.set(warningsAsErrors) + } tasks.withType().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) } } } diff --git a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt index fd20d891ef..9ed1cb0cca 100644 --- a/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt +++ b/build-logic/convention/src/main/kotlin/org/meshtastic/buildlogic/ProjectExtensions.kt @@ -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().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().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 diff --git a/build-logic/gradle.properties b/build-logic/gradle.properties index 7c9329c6a7..2fdf5c3e5b 100644 --- a/build-logic/gradle.properties +++ b/build-logic/gradle.properties @@ -15,21 +15,8 @@ # along with this program. If not, see . # -# 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 diff --git a/core/ble/src/androidHostTest/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepositoryBondTest.kt b/core/ble/src/androidHostTest/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepositoryBondTest.kt index 5b0fa17513..55d79ba635 100644 --- a/core/ble/src/androidHostTest/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepositoryBondTest.kt +++ b/core/ble/src/androidHostTest/kotlin/org/meshtastic/core/ble/AndroidBluetoothRepositoryBondTest.kt @@ -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)) diff --git a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/Crc32.kt b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/Crc32.kt index 64d2a9d046..d0fbca7f00 100644 --- a/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/Crc32.kt +++ b/core/common/src/commonMain/kotlin/org/meshtastic/core/common/util/Crc32.kt @@ -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 diff --git a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt index 8c817a7e64..8f9e6948bb 100644 --- a/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt +++ b/core/data/src/commonTest/kotlin/org/meshtastic/core/data/manager/MeshConnectionManagerImplTest.kt @@ -467,7 +467,6 @@ class MeshConnectionManagerImplTest { admissionVersions += call.arg(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(4) historyAttempts++ if (historyAttempts == 1) throw PacketQueueRejectedException("test history replay") - Unit } every { nodeManager.myNodeNum } returns MutableStateFlow(123) every { mqttManager.startProxy(any(), any()) } returns Unit diff --git a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt index 7591b6671b..98bf1c9350 100644 --- a/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt +++ b/core/database/src/commonMain/kotlin/org/meshtastic/core/database/DatabaseManager.kt @@ -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 launchDbBlock( admission: AdmittedDatabase, blockStarted: CompletableDeferred, diff --git a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt index 5f4204c681..a7ded6fc51 100644 --- a/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt +++ b/core/database/src/commonTest/kotlin/org/meshtastic/core/database/dao/CommonNodeInfoDaoTest.kt @@ -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 diff --git a/core/domain/README.md b/core/domain/README.md index 596653c7f7..67bef1d9fc 100644 --- a/core/domain/README.md +++ b/core/domain/README.md @@ -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 diff --git a/core/domain/build.gradle.kts b/core/domain/build.gradle.kts index ef45d55ff0..e259b4021d 100644 --- a/core/domain/build.gradle.kts +++ b/core/domain/build.gradle.kts @@ -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) diff --git a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCaseTest.kt b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCaseTest.kt index f7d865118a..9756319e84 100644 --- a/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCaseTest.kt +++ b/core/domain/src/commonTest/kotlin/org/meshtastic/core/domain/usecase/session/EnsureRemoteAdminSessionUseCaseTest.kt @@ -105,11 +105,7 @@ class EnsureRemoteAdminSessionUseCaseTest { val sessionManager = stubSessionManager(refreshFlow = refresh) val controller = mock(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(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(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(MockMode.autofill) var dispatches = 0 - everySuspend { controller.refreshMetadata(any()) } calls - { - dispatches++ - Unit - } + everySuspend { controller.refreshMetadata(any()) } calls { dispatches++ } val connectionState = MutableStateFlow(ConnectionState.Connected) val repository = mock(MockMode.autofill) every { repository.connectionState } returns connectionState diff --git a/core/navigation/build.gradle.kts b/core/navigation/build.gradle.kts index d06be78e49..1741f6247a 100644 --- a/core/navigation/build.gradle.kts +++ b/core/navigation/build.gradle.kts @@ -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) diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt index c15ed536b5..134c768580 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/radio/MockRadioTransport.kt @@ -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) } diff --git a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt index 7408732831..a701a81ce8 100644 --- a/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt +++ b/core/network/src/commonMain/kotlin/org/meshtastic/core/network/repository/MQTTRepositoryImpl.kt @@ -607,7 +607,7 @@ internal fun effectiveCredentials(config: ModuleConfig.MQTTConfig?): Pair() - 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() - 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() diff --git a/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/RobolectricBleBonding.kt b/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/RobolectricBleBonding.kt index 6a7c1bf697..3f1446b493 100644 --- a/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/RobolectricBleBonding.kt +++ b/core/testing/src/androidMain/kotlin/org/meshtastic/core/testing/RobolectricBleBonding.kt @@ -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() { diff --git a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeBleServiceFailureInjectionTest.kt b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeBleServiceFailureInjectionTest.kt index aa5f1e002f..a3cebc0a12 100644 --- a/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeBleServiceFailureInjectionTest.kt +++ b/core/testing/src/commonTest/kotlin/org/meshtastic/core/testing/FakeBleServiceFailureInjectionTest.kt @@ -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") } } diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index 979eb3291d..5cbe2e65c7 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -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 } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt index 1a8e8ca2bf..c6f03b7f62 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/ContactSharing.kt @@ -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), ) } diff --git a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt index 77967b720e..52244ca9d0 100644 --- a/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt +++ b/core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/QrDialog.kt @@ -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(), ) { diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index fb67018258..640b57d8bf 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -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) diff --git a/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt index ea2447c45b..ae924e77d8 100644 --- a/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt +++ b/desktopApp/src/test/kotlin/org/meshtastic/desktop/notification/DesktopNotificationManagerTest.kt @@ -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 diff --git a/feature/discovery/README.md b/feature/discovery/README.md index 4197965d71..df0e6748c3 100644 --- a/feature/discovery/README.md +++ b/feature/discovery/README.md @@ -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) diff --git a/feature/discovery/build.gradle.kts b/feature/discovery/build.gradle.kts index 8cdc85d3da..45d52fc24f 100644 --- a/feature/discovery/build.gradle.kts +++ b/feature/discovery/build.gradle.kts @@ -45,7 +45,6 @@ kotlin { implementation(projects.core.service) implementation(projects.core.ui) - implementation(libs.kotlinx.collections.immutable) implementation(libs.meshtastic.protobufs) } } diff --git a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt index 2aa0dc5e03..1c6b02af7a 100644 --- a/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt +++ b/feature/discovery/src/commonTest/kotlin/org/meshtastic/feature/discovery/DiscoveryScanEngineTest.kt @@ -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) } diff --git a/feature/docs/README.md b/feature/docs/README.md index 9014dd8112..0a14352522 100644 --- a/feature/docs/README.md +++ b/feature/docs/README.md @@ -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 ``` diff --git a/feature/docs/build.gradle.kts b/feature/docs/build.gradle.kts index bed5fdd299..4a834d7d73 100644 --- a/feature/docs/build.gradle.kts +++ b/feature/docs/build.gradle.kts @@ -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) diff --git a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/translation/TranslationCascadeTest.kt b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/translation/TranslationCascadeTest.kt index 980545baa4..a95ff456fd 100644 --- a/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/translation/TranslationCascadeTest.kt +++ b/feature/docs/src/commonTest/kotlin/org/meshtastic/feature/docs/translation/TranslationCascadeTest.kt @@ -96,7 +96,7 @@ class TranslationCascadeTest { assertIs(success) assertIs(download) assertIs(unavailable) - assertEquals("es", (download as TranslationResult.ModelDownloadRequired).locale) + assertEquals("es", download.locale) } } diff --git a/feature/firmware/build.gradle.kts b/feature/firmware/build.gradle.kts index fc0b74e0d4..96aa3c2776 100644 --- a/feature/firmware/build.gradle.kts +++ b/feature/firmware/build.gradle.kts @@ -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) diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonMaintenanceVolumeTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonMaintenanceVolumeTest.kt index 834bbf8f06..75a6646e84 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonMaintenanceVolumeTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonMaintenanceVolumeTest.kt @@ -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( - """ - { - "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( + """ + { + "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") diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonUsbPassWriterTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonUsbPassWriterTest.kt index cc9d89e9fc..ec4985cb83 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonUsbPassWriterTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/CommonUsbPassWriterTest.kt @@ -38,33 +38,34 @@ import kotlin.test.assertTrue */ abstract class CommonUsbPassWriterTest { + private val manifestJson = Json { ignoreUnknownKeys = true } + private val manifest = - Json { ignoreUnknownKeys = true } - .decodeFromString( - """ - { - "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( + """ + { + "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") diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt index 02cedb6a6f..c3ef7bcaae 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/UsbMaintenanceGateTest.kt @@ -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( - """ - { - "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( + """ + { + "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( - """ - { - "erase": { - "nrf52": { - "6.1.1": { "fileName": "../../etc/passwd", "sha256": "00" } - }, - "rp2040": { "fileName": "sub/dir/pico_erase.uf2", "sha256": "00" } - } - } - """ - .trimIndent(), - ) + manifestJson.decodeFromString( + """ + { + "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( - """ - { - "otafixReleaseTag": "../../../evil", - "otafixBase": "https://example.invalid/releases", - "otafixByBoardId": { - "rak4631": { "otafixBoardSlug": "rak4631", "sha256": "00" } - } - } - """ - .trimIndent(), - ) + manifestJson.decodeFromString( + """ + { + "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") } diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt index 670427e7e0..b662cb1b88 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/Esp32OtaUpdateHandlerTest.kt @@ -374,7 +374,6 @@ class Esp32OtaUpdateHandlerTest { } assertTrue(events.none { it.startsWith("start:") || it.startsWith("stream:") }) assertIs(states.lastOrNull()) - Unit } } @@ -408,7 +407,6 @@ class Esp32OtaUpdateHandlerTest { } assertTrue(events.none { it.startsWith("start:") || it.startsWith("stream:") }) assertIs(states.lastOrNull()) - Unit } } diff --git a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransportTest.kt b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransportTest.kt index 5ffcba6b86..d7338bcb7d 100644 --- a/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransportTest.kt +++ b/feature/firmware/src/commonTest/kotlin/org/meshtastic/feature/firmware/ota/dfu/LegacyDfuTransportTest.kt @@ -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 = delegate.observe(c) + override fun observe(characteristic: BleCharacteristic): Flow = 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) : StateFlow { 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() diff --git a/feature/map-terrain/src/androidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.android.kt b/feature/map-terrain/src/androidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.android.kt index 629bc76942..393805b1d4 100644 --- a/feature/map-terrain/src/androidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.android.kt +++ b/feature/map-terrain/src/androidMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.android.kt @@ -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 diff --git a/feature/map-terrain/src/jvmMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvm.kt b/feature/map-terrain/src/jvmMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvm.kt index 629bc76942..393805b1d4 100644 --- a/feature/map-terrain/src/jvmMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvm.kt +++ b/feature/map-terrain/src/jvmMain/kotlin/org/meshtastic/feature/map/terrain/TerrainTileFetcher.jvm.kt @@ -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 diff --git a/feature/map/build.gradle.kts b/feature/map/build.gradle.kts index a865d82149..3ecd29e4b3 100644 --- a/feature/map/build.gradle.kts +++ b/feature/map/build.gradle.kts @@ -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) diff --git a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/kml/KmlGroundOverlayTest.kt b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/kml/KmlGroundOverlayTest.kt index bb819d3c7d..4763fb400f 100644 --- a/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/kml/KmlGroundOverlayTest.kt +++ b/feature/map/src/commonTest/kotlin/org/meshtastic/feature/map/kml/KmlGroundOverlayTest.kt @@ -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( """ diff --git a/feature/messaging/build.gradle.kts b/feature/messaging/build.gradle.kts index 4a5c66230b..e14a002936 100644 --- a/feature/messaging/build.gradle.kts +++ b/feature/messaging/build.gradle.kts @@ -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) diff --git a/feature/settings/build.gradle.kts b/feature/settings/build.gradle.kts index 23d9f73631..770eadd5e3 100644 --- a/feature/settings/build.gradle.kts +++ b/feature/settings/build.gradle.kts @@ -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) diff --git a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt index 07a18e97b2..86cc656063 100644 --- a/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt +++ b/feature/settings/src/commonTest/kotlin/org/meshtastic/feature/settings/radio/RadioConfigViewModelTest.kt @@ -1125,7 +1125,7 @@ class RadioConfigViewModelTest { } every { processRadioResponseUseCase(any(), 123, any()) } calls { - val pendingRequestIds = it.args[2] as Set + val pendingRequestIds = it.arg>(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 + val pendingRequestIds = it.arg>(2) if (42 in pendingRequestIds) response else null } nodeRepository.setNodes(listOf(localNode, remoteNode)) diff --git a/gradle.properties b/gradle.properties index 82bbb91b96..4d134cf593 100644 --- a/gradle.properties +++ b/gradle.properties @@ -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) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9bee7af52f..dc27ab57b6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0aaafeeff1..5e1b8f8c88 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -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 diff --git a/schema-strings/build.gradle.kts b/schema-strings/build.gradle.kts index af230b4b0f..de6f3803c3 100644 --- a/schema-strings/build.gradle.kts +++ b/schema-strings/build.gradle.kts @@ -15,6 +15,7 @@ * along with this program. If not, see . */ 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 { diff --git a/store-screenshots/build.gradle.kts b/store-screenshots/build.gradle.kts index e25abfa7ab..136d43ffd4 100644 --- a/store-screenshots/build.gradle.kts +++ b/store-screenshots/build.gradle.kts @@ -14,7 +14,6 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ -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//. // .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)