From 91dafe8a048569efea8324d1a4a9eb48e4408f40 Mon Sep 17 00:00:00 2001 From: darken Date: Sun, 2 Aug 2026 20:52:05 +0200 Subject: [PATCH] test(debug): Stop leaked recorders in the recorder-module test harness The realtime harness cancelled its module scope but never stopped the recorder, and cancelling a scope does not uninstall a running recorder's globally installed FileLogger. A test that started a recording therefore left one writing into every test that followed, and an assertion failing before the explicit stop did the same. The harness now stops the module in a nested finally and fails its own test if a file logger survived, removing the straggler afterwards so a single leak cannot cascade. The tracked-recording test gets the same finally treatment. --- .../core/RecorderModuleDiagnosticsTest.kt | 36 ++++++++++++++----- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/app/src/test/java/eu/darken/capod/common/debug/recording/core/RecorderModuleDiagnosticsTest.kt b/app/src/test/java/eu/darken/capod/common/debug/recording/core/RecorderModuleDiagnosticsTest.kt index 6afefebc..9a78bd36 100644 --- a/app/src/test/java/eu/darken/capod/common/debug/recording/core/RecorderModuleDiagnosticsTest.kt +++ b/app/src/test/java/eu/darken/capod/common/debug/recording/core/RecorderModuleDiagnosticsTest.kt @@ -39,6 +39,7 @@ import org.robolectric.annotation.Config import testhelpers.BaseTest import testhelpers.TestApplication import testhelpers.coroutine.TestDispatcherProvider +import java.io.File import java.util.concurrent.CopyOnWriteArrayList import kotlin.system.measureTimeMillis @@ -95,12 +96,25 @@ class RecorderModuleDiagnosticsTest : BaseTest() { block: suspend (RecorderModule) -> Unit, ) { val moduleScope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + val fileLoggersBefore = Logging.loggers.filterIsInstance() + var module: RecorderModule? = null try { - val module = buildModule(moduleScope, upgradeDiagnostics, TestDispatcherProvider(Dispatchers.IO)) - module.headerReadTimeoutMs = headerTimeoutMs - runBlocking { block(module) } + try { + module = buildModule(moduleScope, upgradeDiagnostics, TestDispatcherProvider(Dispatchers.IO)) + .apply { headerReadTimeoutMs = headerTimeoutMs } + runBlocking { block(module) } + } finally { + // Stop before cancelling: scope cancellation does NOT uninstall a running + // recorder's global FileLogger. + module?.let { runBlocking { it.stopRecorder() } } + } } finally { moduleScope.cancel() + // A leaked logger must fail THIS test, not poison later ones. Remove stragglers after + // asserting so one failure can't cascade. + val leaked = Logging.loggers.filterIsInstance() - fileLoggersBefore.toSet() + leaked.forEach { Logging.remove(it) } + leaked shouldBe emptyList() } } @@ -111,11 +125,17 @@ class RecorderModuleDiagnosticsTest : BaseTest() { val module = buildModule(backgroundScope, diagnostics) - val logDir = module.startRecorder() - logDir.exists() shouldBe true - module.state.first { it.isRecording }.currentLogDir shouldBe logDir - - module.stopRecorder().shouldNotBeNull() + // Stopped in a finally: an assertion failing mid-test must not leave a live recorder whose + // globally installed FileLogger then writes into every later test. + var stopped: File? = null + try { + val logDir = module.startRecorder() + logDir.exists() shouldBe true + module.state.first { it.isRecording }.currentLogDir shouldBe logDir + } finally { + stopped = module.stopRecorder() + } + stopped.shouldNotBeNull() } /**