From 09e1dcee70c66d147469652f6df8f012da76b7a9 Mon Sep 17 00:00:00 2001 From: Zach Klippenstein Date: Wed, 16 Jul 2025 12:02:52 -0700 Subject: [PATCH] WIP Android-specific tests for WorkStealingDispatcher. Validates behavior with `Dispatchers.Main` and `.immediate`. This can't be merged as-is, since it requires the tests to live in the android source set but that's in a different module so it can't see WSD. Need to land https://github.com/square/workflow-kotlin/pull/1370 first. --- ...tealingDispatcherAndroidDispatchersTest.kt | 96 +++++++++++++++++++ .../workflow1/internal/Synchronization.kt | 4 +- .../internal/WorkStealingDispatcher.kt | 2 +- .../workflow1/internal/Synchronization.jvm.kt | 4 +- 4 files changed, 101 insertions(+), 5 deletions(-) create mode 100644 workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/WorkStealingDispatcherAndroidDispatchersTest.kt diff --git a/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/WorkStealingDispatcherAndroidDispatchersTest.kt b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/WorkStealingDispatcherAndroidDispatchersTest.kt new file mode 100644 index 0000000000..838ee765fd --- /dev/null +++ b/workflow-runtime-android/src/androidTest/java/com/squareup/workflow1/android/WorkStealingDispatcherAndroidDispatchersTest.kt @@ -0,0 +1,96 @@ +package com.squareup.workflow1.android + +import com.squareup.workflow1.internal.WorkStealingDispatcher +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext +import kotlinx.coroutines.yield +import org.junit.Test +import kotlin.test.assertEquals + +class WorkStealingDispatcherAndroidDispatchersTest { + + @Test fun dispatch_runsImmediatelyWhenDelegateIsMainImmediate_onMainThread() = runTest { + val dispatcher = WorkStealingDispatcher(Dispatchers.Main.immediate) + + runOnMainThread { + expect(0) + dispatcher.dispatch { + expect(1) + } + expect(2) + } + } + + @Test fun dispatchNested_enqueuesWhenDelegateIsMainImmediate_onMainThread() = runTest { + val dispatcher = WorkStealingDispatcher(Dispatchers.Main.immediate) + + runOnMainThread { + expect(0) + dispatcher.dispatch { + expect(1) + + // This dispatch should get enqueued to Unconfined's threadlocal queue. + dispatcher.dispatch { + expect(3) + } + + expect(2) + } + expect(4) + } + } + + @Test fun dispatch_queues_whenDelegateisMain_onMainThread() = runTest { + val dispatcher = WorkStealingDispatcher(Dispatchers.Main) + + runOnMainThread { + expect(0) + dispatcher.dispatch { + expect(2) + } + expect(1) + + yield() + expect(3) + } + } + + @Test fun dispatch_runsMultipleTasksInOrder_whenDelegateIsMain_onMainThread() = runTest { + val dispatcher = WorkStealingDispatcher(Dispatchers.Main) + + runOnMainThread { + expect(0) + dispatcher.dispatch { + expect(3) + } + expect(1) + dispatcher.dispatch { + expect(4) + } + expect(2) + + yield() + expect(5) + } + } + + private suspend fun runOnMainThread(block: suspend CoroutineScope.() -> Unit) { + withContext(Dispatchers.Main, block) + } + + private fun CoroutineDispatcher.dispatch(block: () -> Unit) { + dispatch(this) { block() } + } + + private val expectLock = Any() + private var current = 0 + private fun expect(expected: Int) { + synchronized(expectLock) { + assertEquals(expected, current, "Expected to be at step $expected but was at $current") + current++ + } + } +} diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Synchronization.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Synchronization.kt index fd98cb9c54..7817341703 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Synchronization.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/Synchronization.kt @@ -1,5 +1,5 @@ package com.squareup.workflow1.internal -internal expect class Lock() +public expect class Lock() -internal expect inline fun Lock.withLock(block: () -> R): R +public expect inline fun Lock.withLock(block: () -> R): R diff --git a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkStealingDispatcher.kt b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkStealingDispatcher.kt index c7f23b38db..b8a80c42a8 100644 --- a/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkStealingDispatcher.kt +++ b/workflow-runtime/src/commonMain/kotlin/com/squareup/workflow1/internal/WorkStealingDispatcher.kt @@ -38,7 +38,7 @@ import kotlin.coroutines.resume * delegate scheduling behavior to. This can either be a confined or unconfined dispatcher, and its * behavior will be preserved transparently. */ -internal open class WorkStealingDispatcher protected constructor( +public open class WorkStealingDispatcher protected constructor( private val delegateInterceptor: ContinuationInterceptor, lock: Lock?, queue: LinkedHashSet? diff --git a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/Synchronization.jvm.kt b/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/Synchronization.jvm.kt index e84a031233..b6af394428 100644 --- a/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/Synchronization.jvm.kt +++ b/workflow-runtime/src/jvmMain/kotlin/com/squareup/workflow1/internal/Synchronization.jvm.kt @@ -1,5 +1,5 @@ package com.squareup.workflow1.internal -internal actual typealias Lock = Any +public actual typealias Lock = Any -internal actual inline fun Lock.withLock(block: () -> R): R = synchronized(this, block) +public actual inline fun Lock.withLock(block: () -> R): R = synchronized(this, block)