From 85d0f4f8433dbf4a65d5376caa645fc153f0e8bd Mon Sep 17 00:00:00 2001 From: vinceglb Date: Fri, 24 Jul 2026 12:31:40 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20Fix=20flaky=20Windows=20dialog?= =?UTF-8?q?=20thread=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../platform/windows/WindowsDialogExecutor.kt | 18 ++++++++---- .../WindowsDialogExecutorIntegrationTest.kt | 1 - .../windows/WindowsDialogExecutorTest.kt | 29 ++++++++++++++++--- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/filekit-dialogs/src/jvmMain/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutor.kt b/filekit-dialogs/src/jvmMain/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutor.kt index 804beec6..373b18d4 100644 --- a/filekit-dialogs/src/jvmMain/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutor.kt +++ b/filekit-dialogs/src/jvmMain/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutor.kt @@ -4,6 +4,7 @@ import com.sun.jna.platform.win32.Ole32 import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.withContext import java.util.concurrent.Executors +import java.util.concurrent.ThreadFactory internal interface WindowsComRuntime { fun initializeSta(): Int @@ -23,15 +24,21 @@ internal object JnaWindowsComRuntime : WindowsComRuntime { } } +internal object WindowsDialogThreadFactory : ThreadFactory { + override fun newThread(runnable: Runnable): Thread = Thread(runnable, THREAD_NAME).apply { + isDaemon = true + } + + private const val THREAD_NAME = "FileKit-Windows-Dialog" +} + internal class WindowsDialogExecutor( private val comRuntime: WindowsComRuntime, + threadFactory: ThreadFactory = WindowsDialogThreadFactory, ) : AutoCloseable { private val dispatcher = Executors - .newSingleThreadExecutor { runnable -> - Thread(runnable, THREAD_NAME).apply { - isDaemon = true - } - }.asCoroutineDispatcher() + .newSingleThreadExecutor(threadFactory) + .asCoroutineDispatcher() suspend fun execute(block: () -> T): T = withContext(dispatcher) { val initializationResult = comRuntime.initializeSta() @@ -53,7 +60,6 @@ internal class WindowsDialogExecutor( } private companion object { - const val THREAD_NAME = "FileKit-Windows-Dialog" const val S_OK = 0 const val S_FALSE = 1 } diff --git a/filekit-dialogs/src/jvmTest/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutorIntegrationTest.kt b/filekit-dialogs/src/jvmTest/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutorIntegrationTest.kt index e1f11856..2f0b0e20 100644 --- a/filekit-dialogs/src/jvmTest/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutorIntegrationTest.kt +++ b/filekit-dialogs/src/jvmTest/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutorIntegrationTest.kt @@ -56,7 +56,6 @@ class WindowsDialogExecutorIntegrationTest { } assertNotEquals(callerThread, dialogThread) - assertEquals("FileKit-Windows-Dialog", dialogThread.name) } finally { dialogExecutor.close() } diff --git a/filekit-dialogs/src/jvmTest/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutorTest.kt b/filekit-dialogs/src/jvmTest/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutorTest.kt index 9d149266..12404af0 100644 --- a/filekit-dialogs/src/jvmTest/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutorTest.kt +++ b/filekit-dialogs/src/jvmTest/kotlin/io/github/vinceglb/filekit/dialogs/platform/windows/WindowsDialogExecutorTest.kt @@ -7,11 +7,13 @@ import kotlinx.coroutines.async import kotlinx.coroutines.runBlocking import java.util.concurrent.CountDownLatch import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.ThreadFactory import java.util.concurrent.TimeUnit import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertFailsWith import kotlin.test.assertFalse +import kotlin.test.assertSame import kotlin.test.assertTrue @Suppress("ktlint:standard:function-naming", "FunctionName") @@ -49,15 +51,23 @@ class WindowsDialogExecutorTest { } @Test - fun WindowsDialogExecutor_execute_usesNamedDaemonThread() = runBlocking { + fun WindowsDialogThreadFactory_newThread_usesStableDaemonIdentity() { + val worker = WindowsDialogThreadFactory.newThread {} + + assertEquals("FileKit-Windows-Dialog", worker.name) + assertTrue(worker.isDaemon) + } + + @Test + fun WindowsDialogExecutor_execute_usesFactoryCreatedThread() = runBlocking { val comRuntime = FakeWindowsComRuntime(initializationResult = S_OK) - val executor = WindowsDialogExecutor(comRuntime) + val threadFactory = RecordingThreadFactory(WindowsDialogThreadFactory) + val executor = WindowsDialogExecutor(comRuntime, threadFactory) try { val worker = executor.execute { Thread.currentThread() } - assertEquals("FileKit-Windows-Dialog", worker.name) - assertTrue(worker.isDaemon) + assertSame(threadFactory.createdThread, worker) } finally { executor.close() } @@ -182,6 +192,17 @@ class WindowsDialogExecutorTest { } } + private class RecordingThreadFactory( + private val delegate: ThreadFactory, + ) : ThreadFactory { + lateinit var createdThread: Thread + private set + + override fun newThread(runnable: Runnable): Thread = delegate.newThread(runnable).also { + createdThread = it + } + } + private class FakeWindowsComRuntime( private val initializationResult: Int, ) : WindowsComRuntime {