refactor(service): harden KMP service layer — database init, connection reliability, handler decomposition (#4992)

This commit is contained in:
James Rich 2026-04-04 13:07:44 -05:00 committed by GitHub
parent e111b61e4e
commit 6af3ad6f0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 3808 additions and 735 deletions

View file

@ -31,7 +31,7 @@ object Exceptions {
*/
fun report(exception: Throwable, tag: String? = null, message: String? = null) {
// Log locally first
Logger.e(exception) { "Exceptions.report: $tag $message" }
Logger.e(exception) { "Exceptions.report: ${tag ?: "no-tag"} ${message ?: "no-message"}" }
reporter?.invoke(exception, tag, message)
}
}
@ -47,6 +47,17 @@ fun ignoreException(silent: Boolean = false, inner: () -> Unit) {
}
}
/** Suspend-compatible variant of [ignoreException]. */
suspend fun ignoreExceptionSuspend(silent: Boolean = false, inner: suspend () -> Unit) {
try {
inner()
} catch (@Suppress("TooGenericExceptionCaught") ex: Exception) {
if (!silent) {
Logger.w(ex) { "Ignoring exception" }
}
}
}
/**
* Wraps and discards exceptions, but reports them to the crash reporter before logging. Use this for operations that
* should not crash the process but are still unexpected.

View file

@ -0,0 +1,147 @@
/*
* Copyright (c) 2026 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.meshtastic.core.common.util
import kotlinx.coroutines.test.runTest
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
class ExceptionsTest {
@AfterTest
fun tearDown() {
Exceptions.reporter = null
}
// ---------- Exceptions.report ----------
@Test
fun `report invokes configured reporter with all arguments`() {
var captured: Triple<Throwable, String?, String?>? = null
Exceptions.reporter = { ex, tag, msg -> captured = Triple(ex, tag, msg) }
val error = RuntimeException("boom")
Exceptions.report(error, tag = "MyTag", message = "context")
assertEquals(error, captured?.first)
assertEquals("MyTag", captured?.second)
assertEquals("context", captured?.third)
}
@Test
fun `report works with null tag and message`() {
var captured: Triple<Throwable, String?, String?>? = null
Exceptions.reporter = { ex, tag, msg -> captured = Triple(ex, tag, msg) }
Exceptions.report(RuntimeException("x"))
assertNull(captured?.second)
assertNull(captured?.third)
}
@Test
fun `report does not crash when no reporter is configured`() {
Exceptions.reporter = null
// Should not throw
Exceptions.report(RuntimeException("no reporter"))
}
// ---------- ignoreException ----------
@Test
fun `ignoreException swallows exceptions from inner block`() {
var reached = false
ignoreException { throw IllegalStateException("expected") }
reached = true
assertTrue(reached)
}
@Test
fun `ignoreException does not swallow when inner succeeds`() {
var executed = false
ignoreException { executed = true }
assertTrue(executed)
}
@Test
fun `ignoreException silent mode suppresses logging`() {
// Should not crash even in silent mode
ignoreException(silent = true) { throw RuntimeException("silent") }
}
@Test
fun `ignoreException non-silent mode logs but does not crash`() {
ignoreException(silent = false) { throw RuntimeException("logged") }
}
// ---------- ignoreExceptionSuspend ----------
@Test
fun `ignoreExceptionSuspend swallows exceptions`() = runTest {
var reached = false
ignoreExceptionSuspend { throw IllegalArgumentException("async boom") }
reached = true
assertTrue(reached)
}
@Test
fun `ignoreExceptionSuspend silent mode suppresses logging`() = runTest {
ignoreExceptionSuspend(silent = true) { throw RuntimeException("silent async") }
}
@Test
fun `ignoreExceptionSuspend executes block normally when no exception`() = runTest {
var executed = false
ignoreExceptionSuspend { executed = true }
assertTrue(executed)
}
// ---------- exceptionReporter ----------
@Test
fun `exceptionReporter reports exceptions to configured reporter`() {
var reportCalled = false
Exceptions.reporter = { _, _, _ -> reportCalled = true }
exceptionReporter { throw RuntimeException("reported") }
assertTrue(reportCalled)
}
@Test
fun `exceptionReporter does not invoke reporter when block succeeds`() {
var reportCalled = false
Exceptions.reporter = { _, _, _ -> reportCalled = true }
exceptionReporter {
// no exception
}
assertFalse(reportCalled)
}
@Test
fun `exceptionReporter works without configured reporter`() {
Exceptions.reporter = null
// Should not crash
exceptionReporter { throw RuntimeException("no reporter configured") }
}
}