diff --git a/buildSrc/src/main/kotlin/SourceSets.kt b/buildSrc/src/main/kotlin/SourceSets.kt index 340fd65cd0..e99f0cd6e6 100644 --- a/buildSrc/src/main/kotlin/SourceSets.kt +++ b/buildSrc/src/main/kotlin/SourceSets.kt @@ -40,16 +40,27 @@ fun KotlinSourceSet.configureDirectoryPaths() { fun NamedDomainObjectContainer.groupSourceSets( groupName: String, reverseDependencies: List, - dependencies: List + dependencies: List, + alreadyExist: Boolean = false ) { + fun KotlinSourceSet.configureSourceSet(suffix: String) { + for (dep in dependencies) { + dependsOn(get(dep + suffix)) + } + for (revDep in reverseDependencies) { + get(revDep + suffix).dependsOn(this) + } + } + val sourceSetSuffixes = listOf("Main", "Test") for (suffix in sourceSetSuffixes) { - register(groupName + suffix) { - for (dep in dependencies) { - dependsOn(get(dep + suffix)) + if (alreadyExist) { + getByName(groupName + suffix) { + configureSourceSet(suffix) } - for (revDep in reverseDependencies) { - get(revDep + suffix).dependsOn(this) + } else { + register(groupName + suffix) { + configureSourceSet(suffix) } } } diff --git a/buildSrc/src/main/kotlin/kotlin-multiplatform-conventions.gradle.kts b/buildSrc/src/main/kotlin/kotlin-multiplatform-conventions.gradle.kts index fd2d3fdbad..afb02c2389 100644 --- a/buildSrc/src/main/kotlin/kotlin-multiplatform-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/kotlin-multiplatform-conventions.gradle.kts @@ -63,6 +63,18 @@ kotlin { api("org.jetbrains.kotlinx:atomicfu-wasm-js:${version("atomicfu")}") } } + @OptIn(org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl::class) + wasmWasi { + nodejs() + compilations["main"]?.dependencies { + api("org.jetbrains.kotlinx:atomicfu-wasm-wasi:${version("atomicfu")}") + } + compilations.configureEach { + compilerOptions.configure { + optIn.add("kotlin.wasm.internal.InternalWasmApi") + } + } + } applyDefaultHierarchyTemplate() sourceSets { commonTest { @@ -101,15 +113,27 @@ kotlin { api("org.jetbrains.kotlin:kotlin-test-wasm-js:${version("kotlin")}") } } - groupSourceSets("jsAndWasmShared", listOf("js", "wasmJs"), listOf("common")) + val wasmWasiMain by getting { + } + val wasmWasiTest by getting { + dependencies { + api("org.jetbrains.kotlin:kotlin-test-wasm-wasi:${version("kotlin")}") + } + } + groupSourceSets("jsAndWasmShared", listOf("wasmWasi"), listOf("common")) + groupSourceSets("jsAndWasmJsShared", listOf("js", "wasmJs"), listOf("jsAndWasmShared")) + groupSourceSets("jsAndWasmShared", listOf("jsAndWasmJsShared"), emptyList(), alreadyExist = true) } } -// Disable intermediate sourceSet compilation because we do not need js-wasmJs artifact +// Disable intermediate sourceSet compilation because we do not need js-wasm common artifact tasks.configureEach { if (name == "compileJsAndWasmSharedMainKotlinMetadata") { enabled = false } + if (name == "compileJsAndWasmJsSharedMainKotlinMetadata") { + enabled = false + } } tasks.named("jvmTest", Test::class) { diff --git a/integration-testing/smokeTest/build.gradle b/integration-testing/smokeTest/build.gradle index 16c86638a5..65d09dfa32 100644 --- a/integration-testing/smokeTest/build.gradle +++ b/integration-testing/smokeTest/build.gradle @@ -48,6 +48,11 @@ kotlin { implementation kotlin('test-wasm-js') } } + wasmWasiTest { + dependencies { + implementation kotlin('test-wasm-wasi') + } + } jvmTest { dependencies { implementation kotlin('test') diff --git a/kotlinx-coroutines-core/build.gradle.kts b/kotlinx-coroutines-core/build.gradle.kts index 0c6f65cabd..68a1f8419b 100644 --- a/kotlinx-coroutines-core/build.gradle.kts +++ b/kotlinx-coroutines-core/build.gradle.kts @@ -18,10 +18,13 @@ apply(plugin = "pub-conventions") Configure source sets structure for kotlinx-coroutines-core: TARGETS SOURCE SETS - ------- ---------------------------------------------- - wasmJs \----------> jsAndWasmShared --------------------+ - js / | - V + ------------------------------------------------------------ + wasmJs \------> jsAndWasmJsShared ----+ + js / | + V + wasmWasi --------------------> jsAndWasmShared ----------+ + | + V jvmCore\ --------> jvm ---------> concurrent -------> common jdk8 / ^ | diff --git a/kotlinx-coroutines-core/js/src/CoroutineContext.kt b/kotlinx-coroutines-core/js/src/CoroutineContext.kt index 3156dca4f4..83f6d8e8a3 100644 --- a/kotlinx-coroutines-core/js/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/js/src/CoroutineContext.kt @@ -1,8 +1,10 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package kotlinx.coroutines import kotlinx.browser.* -import kotlinx.coroutines.internal.* -import kotlin.coroutines.* private external val navigator: dynamic private const val UNDEFINED = "undefined" @@ -28,30 +30,3 @@ private fun isJsdom() = jsTypeOf(navigator) != UNDEFINED && jsTypeOf(navigator.userAgent) != UNDEFINED && jsTypeOf(navigator.userAgent.match) != UNDEFINED && navigator.userAgent.match("\\bjsdom\\b") - -@PublishedApi // Used from kotlinx-coroutines-test via suppress, not part of ABI -internal actual val DefaultDelay: Delay - get() = Dispatchers.Default as Delay - -public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { - val combined = coroutineContext + context - return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null) - combined + Dispatchers.Default else combined -} - -public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext { - return this + addedContext -} - -// No debugging facilities on JS -internal actual inline fun withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = block() -internal actual inline fun withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T = block() -internal actual fun Continuation<*>.toDebugString(): String = toString() -internal actual val CoroutineContext.coroutineName: String? get() = null // not supported on JS - -internal actual class UndispatchedCoroutine actual constructor( - context: CoroutineContext, - uCont: Continuation -) : ScopeCoroutine(context, uCont) { - override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont)) -} diff --git a/kotlinx-coroutines-core/jsAndWasmShared/src/internal/JSDispatcher.kt b/kotlinx-coroutines-core/jsAndWasmJsShared/src/internal/JSDispatcher.kt similarity index 100% rename from kotlinx-coroutines-core/jsAndWasmShared/src/internal/JSDispatcher.kt rename to kotlinx-coroutines-core/jsAndWasmJsShared/src/internal/JSDispatcher.kt diff --git a/kotlinx-coroutines-core/jsAndWasmShared/test/MessageQueueTest.kt b/kotlinx-coroutines-core/jsAndWasmJsShared/test/MessageQueueTest.kt similarity index 100% rename from kotlinx-coroutines-core/jsAndWasmShared/test/MessageQueueTest.kt rename to kotlinx-coroutines-core/jsAndWasmJsShared/test/MessageQueueTest.kt diff --git a/kotlinx-coroutines-core/jsAndWasmShared/test/SetTimeoutDispatcherTest.kt b/kotlinx-coroutines-core/jsAndWasmJsShared/test/SetTimeoutDispatcherTest.kt similarity index 100% rename from kotlinx-coroutines-core/jsAndWasmShared/test/SetTimeoutDispatcherTest.kt rename to kotlinx-coroutines-core/jsAndWasmJsShared/test/SetTimeoutDispatcherTest.kt diff --git a/kotlinx-coroutines-core/jsAndWasmShared/src/CoroutineContext.kt b/kotlinx-coroutines-core/jsAndWasmShared/src/CoroutineContext.kt new file mode 100644 index 0000000000..d9dd6ef4aa --- /dev/null +++ b/kotlinx-coroutines-core/jsAndWasmShared/src/CoroutineContext.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.internal.ScopeCoroutine +import kotlin.coroutines.* + +@PublishedApi // Used from kotlinx-coroutines-test via suppress, not part of ABI +internal actual val DefaultDelay: Delay + get() = Dispatchers.Default as Delay + +public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { + val combined = coroutineContext + context + return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null) + combined + Dispatchers.Default else combined +} + +public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext { + return this + addedContext +} + +// No debugging facilities on Wasm and JS +internal actual inline fun withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = block() +internal actual inline fun withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T = block() +internal actual fun Continuation<*>.toDebugString(): String = toString() +internal actual val CoroutineContext.coroutineName: String? get() = null // not supported on Wasm and JS + +internal actual class UndispatchedCoroutine actual constructor( + context: CoroutineContext, + uCont: Continuation +) : ScopeCoroutine(context, uCont) { + override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont)) +} diff --git a/kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt b/kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt index f4db72a687..7c8e72ae19 100644 --- a/kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt +++ b/kotlinx-coroutines-core/wasmJs/src/CoroutineContext.kt @@ -1,8 +1,10 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + package kotlinx.coroutines -import kotlinx.coroutines.internal.* import org.w3c.dom.* -import kotlin.coroutines.* internal external interface JsProcess : JsAny { fun nextTick(handler: () -> Unit) @@ -18,30 +20,3 @@ internal actual fun createDefaultDispatcher(): CoroutineDispatcher = tryGetProcess()?.let(::NodeDispatcher) ?: tryGetWindow()?.let(::WindowDispatcher) ?: SetTimeoutDispatcher - -@PublishedApi // Used from kotlinx-coroutines-test via suppress, not part of ABI -internal actual val DefaultDelay: Delay - get() = Dispatchers.Default as Delay - -public actual fun CoroutineScope.newCoroutineContext(context: CoroutineContext): CoroutineContext { - val combined = coroutineContext + context - return if (combined !== Dispatchers.Default && combined[ContinuationInterceptor] == null) - combined + Dispatchers.Default else combined -} - -public actual fun CoroutineContext.newCoroutineContext(addedContext: CoroutineContext): CoroutineContext { - return this + addedContext -} - -// No debugging facilities on Wasm -internal actual inline fun withCoroutineContext(context: CoroutineContext, countOrElement: Any?, block: () -> T): T = block() -internal actual inline fun withContinuationContext(continuation: Continuation<*>, countOrElement: Any?, block: () -> T): T = block() -internal actual fun Continuation<*>.toDebugString(): String = toString() -internal actual val CoroutineContext.coroutineName: String? get() = null // not supported on Wasm - -internal actual class UndispatchedCoroutine actual constructor( - context: CoroutineContext, - uCont: Continuation -) : ScopeCoroutine(context, uCont) { - override fun afterResume(state: Any?) = uCont.resumeWith(recoverResult(state, uCont)) -} diff --git a/kotlinx-coroutines-core/wasmWasi/src/CoroutineContext.kt b/kotlinx-coroutines-core/wasmWasi/src/CoroutineContext.kt new file mode 100644 index 0000000000..a019139019 --- /dev/null +++ b/kotlinx-coroutines-core/wasmWasi/src/CoroutineContext.kt @@ -0,0 +1,7 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +internal actual fun createDefaultDispatcher(): CoroutineDispatcher = WasiDispatcher diff --git a/kotlinx-coroutines-core/wasmWasi/src/Debug.kt b/kotlinx-coroutines-core/wasmWasi/src/Debug.kt new file mode 100644 index 0000000000..bdf0793c2b --- /dev/null +++ b/kotlinx-coroutines-core/wasmWasi/src/Debug.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +internal actual val DEBUG: Boolean = false + +internal actual val Any.hexAddress: String + get() = this.hashCode().toString() + +internal actual val Any.classSimpleName: String get() = this::class.simpleName ?: "Unknown" + +internal actual inline fun assert(value: () -> Boolean) {} \ No newline at end of file diff --git a/kotlinx-coroutines-core/wasmWasi/src/EventLoopException.kt b/kotlinx-coroutines-core/wasmWasi/src/EventLoopException.kt new file mode 100644 index 0000000000..f60b8b5655 --- /dev/null +++ b/kotlinx-coroutines-core/wasmWasi/src/EventLoopException.kt @@ -0,0 +1,11 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +/** + * Thrown when multiply exception were thrown in event loop. + * @see runEventLoop + */ +public class EventLoopException(public val causes: List) : Throwable("Multiple exceptions were thrown in the event loop.") \ No newline at end of file diff --git a/kotlinx-coroutines-core/wasmWasi/src/WasiDispatcher.kt b/kotlinx-coroutines-core/wasmWasi/src/WasiDispatcher.kt new file mode 100644 index 0000000000..3ce6c1eafc --- /dev/null +++ b/kotlinx-coroutines-core/wasmWasi/src/WasiDispatcher.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines + +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* + +internal object WasiDispatcher: CoroutineDispatcher(), Delay { + override fun limitedParallelism(parallelism: Int): CoroutineDispatcher { + parallelism.checkParallelism() + return this + } + + override fun dispatch(context: CoroutineContext, block: Runnable) { + registerEvent(0) { block.run() } + } + + override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle { + val event = registerEvent(delayToNanos(timeMillis)) { block.run() } + return DisposableHandle { event.cancel() } + } + + override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation) { + val event = registerEvent(delayToNanos(timeMillis)) { + with(continuation) { resumeUndispatched(Unit) } + } + continuation.invokeOnCancellation(handler = { event.cancel() }) + } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/wasmWasi/src/internal/CoroutineExceptionHandlerImpl.kt b/kotlinx-coroutines-core/wasmWasi/src/internal/CoroutineExceptionHandlerImpl.kt new file mode 100644 index 0000000000..80b7d3a97b --- /dev/null +++ b/kotlinx-coroutines-core/wasmWasi/src/internal/CoroutineExceptionHandlerImpl.kt @@ -0,0 +1,9 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +internal actual fun propagateExceptionFinalResort(exception: Throwable) { + println(exception.toString()) +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/wasmWasi/src/internal/CoroutineRunner.kt b/kotlinx-coroutines-core/wasmWasi/src/internal/CoroutineRunner.kt new file mode 100644 index 0000000000..7106284f2f --- /dev/null +++ b/kotlinx-coroutines-core/wasmWasi/src/internal/CoroutineRunner.kt @@ -0,0 +1,17 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import kotlin.coroutines.* + +@InternalCoroutinesApi +public fun runTestCoroutine(context: CoroutineContext, block: suspend CoroutineScope.() -> Unit) { + val newContext = GlobalScope.newCoroutineContext(context) + val coroutine = object: AbstractCoroutine(newContext, true, true) {} + coroutine.start(CoroutineStart.DEFAULT, coroutine, block) + runEventLoop() + if (coroutine.isCancelled) throw coroutine.getCancellationException().let { it.cause ?: it } +} \ No newline at end of file diff --git a/kotlinx-coroutines-core/wasmWasi/src/internal/EventLoop.kt b/kotlinx-coroutines-core/wasmWasi/src/internal/EventLoop.kt new file mode 100644 index 0000000000..c741888708 --- /dev/null +++ b/kotlinx-coroutines-core/wasmWasi/src/internal/EventLoop.kt @@ -0,0 +1,205 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.internal + +import kotlinx.coroutines.* +import kotlin.math.min +import kotlin.wasm.WasmImport +import kotlin.wasm.unsafe.MemoryAllocator +import kotlin.wasm.unsafe.Pointer +import kotlin.wasm.unsafe.UnsafeWasmMemoryApi +import kotlin.wasm.unsafe.withScopedMemoryAllocator + +@WasmImport("wasi_snapshot_preview1", "poll_oneoff") +private external fun wasiPollOneOff(ptrToSubscription: Int, eventPtr: Int, nsubscriptions: Int, resultPtr: Int): Int + +@WasmImport("wasi_snapshot_preview1", "clock_time_get") +private external fun wasiRawClockTimeGet(clockId: Int, precision: Long, resultPtr: Int): Int + +private const val CLOCKID_MONOTONIC = 1 + +internal class Event internal constructor(internal var callback: (() -> Unit)?, internal val absoluteTimeout: Long) { + fun cancel() { + if (callback == null) return + callback = null + nextCycleNearestEventAbsoluteTime = 0 + nextCycleContainTimedEvent = true + } + + companion object { + fun createCancelled(): Event = Event(null, 0) + } +} + +private var currentCycleEvents = mutableListOf() +private var nextCycleEvents = mutableListOf() +private var thrownExceptions = mutableListOf() +private var nextCycleNearestEventAbsoluteTime: Long = Long.MAX_VALUE +private var nextCycleContainTimedEvent = false + +@OptIn(UnsafeWasmMemoryApi::class) +private fun initializeSubscriptionPtr(allocator: MemoryAllocator): Pointer { + val ptrToSubscription = allocator.allocate(48) + //userdata + ptrToSubscription.storeLong(0) + //uint8_t tag; + (ptrToSubscription + 8).storeByte(0) //EVENTTYPE_CLOCK + //__wasi_clockid_t id; + (ptrToSubscription + 16).storeInt(CLOCKID_MONOTONIC) //CLOCKID_MONOTONIC + //__wasi_timestamp_t timeout; + //(ptrToSubscription + 24).storeLong(timeout) + //__wasi_timestamp_t precision; + (ptrToSubscription + 32).storeLong(0) + //__wasi_subclockflags_t + (ptrToSubscription + 40).storeShort(0) //ABSOLUTE_TIME=1/RELATIVE=0 + + return ptrToSubscription +} + +@OptIn(UnsafeWasmMemoryApi::class) +private fun clockTimeGet(ptrTo8Bytes: Pointer): Long { + val returnCode = wasiRawClockTimeGet( + clockId = CLOCKID_MONOTONIC, + precision = 1, + resultPtr = ptrTo8Bytes.address.toInt() + ) + check(returnCode == 0) + return ptrTo8Bytes.loadLong() +} + +@OptIn(UnsafeWasmMemoryApi::class) +private fun sleep(timeout: Long, ptrTo32Bytes: Pointer, ptrTo8Bytes: Pointer, ptrToSubscription: Pointer) { + //__wasi_timestamp_t timeout; + (ptrToSubscription + 24).storeLong(timeout) + + val returnCode = wasiPollOneOff( + ptrToSubscription = ptrToSubscription.address.toInt(), + eventPtr = ptrTo32Bytes.address.toInt(), + nsubscriptions = 1, + resultPtr = ptrTo8Bytes.address.toInt() + ) + + check(returnCode == 0) +} + +@OptIn(UnsafeWasmMemoryApi::class) +private fun sleepAndGetTime( + absoluteTime: Long, + ptrTo32Bytes: Pointer, + ptrTo8Bytes: Pointer, + ptrToSubscription: Pointer +): Long { + var currentTime = clockTimeGet(ptrTo8Bytes) + val sleepTimeout = absoluteTime - currentTime + + if (sleepTimeout > 0) { + sleep( + timeout = sleepTimeout, + ptrTo32Bytes = ptrTo32Bytes, + ptrTo8Bytes = ptrTo8Bytes, + ptrToSubscription = ptrToSubscription + ) + currentTime += sleepTimeout + } + + return currentTime +} + +private fun runEventCycleSimple() { + for (currentEvent in currentCycleEvents) { + try { + currentEvent.callback?.invoke() + } catch (e: Throwable) { + thrownExceptions.add(e) + } + } +} + +private fun runEventCycle(currentTime: Long) { + for (currentEvent in currentCycleEvents) { + val callback = currentEvent.callback ?: continue + val eventAbsoluteTime = currentEvent.absoluteTimeout + if (currentTime >= eventAbsoluteTime) { + try { + callback() + } catch (e: Throwable) { + thrownExceptions.add(e) + } + } else { + nextCycleEvents.add(currentEvent) + nextCycleNearestEventAbsoluteTime = min(eventAbsoluteTime, nextCycleNearestEventAbsoluteTime) + nextCycleContainTimedEvent = eventAbsoluteTime > 0 + } + } +} + +@OptIn(UnsafeWasmMemoryApi::class) +internal fun runEventLoop() { + if (nextCycleEvents.isEmpty()) return + + withScopedMemoryAllocator { allocator -> + val ptrTo32Bytes = allocator.allocate(32) + val ptrTo8Bytes = allocator.allocate(8) + val ptrToSubscription = initializeSubscriptionPtr(allocator) + + while (nextCycleEvents.isNotEmpty()) { + val currentNextEventTime = nextCycleNearestEventAbsoluteTime + + val buffer = currentCycleEvents + currentCycleEvents = nextCycleEvents + nextCycleEvents = buffer + nextCycleEvents.clear() + nextCycleNearestEventAbsoluteTime = Long.MAX_VALUE + + if (nextCycleContainTimedEvent) { + nextCycleContainTimedEvent = false + + val currentTime = sleepAndGetTime( + absoluteTime = currentNextEventTime, + ptrTo32Bytes = ptrTo32Bytes, + ptrTo8Bytes = ptrTo8Bytes, + ptrToSubscription = ptrToSubscription + ) + runEventCycle(currentTime = currentTime) + } else { + runEventCycleSimple() + } + } + } + + if (thrownExceptions.isNotEmpty()) { + val exceptionToThrow = thrownExceptions.singleOrNull() ?: EventLoopException(thrownExceptions.toList()) + thrownExceptions.clear() + throw exceptionToThrow + } +} + +/* Register new event with specified timeout in nanoseconds */ +@OptIn(UnsafeWasmMemoryApi::class) +internal fun registerEvent(timeout: Long, callback: () -> Unit): Event { + if (kotlin.wasm.internal.onExportedFunctionExit == null) { + kotlin.wasm.internal.onExportedFunctionExit = ::runEventLoop + } + + require(timeout >= 0L) { "Timeout cannot be negative" } + val taskAbsoluteTime: Long + if (timeout > 0L) { + val absoluteCurrentTime = withScopedMemoryAllocator { allocator -> + clockTimeGet(ptrTo8Bytes = allocator.allocate(8)) + } + taskAbsoluteTime = absoluteCurrentTime + timeout + if (taskAbsoluteTime <= 0) return Event.createCancelled() + nextCycleNearestEventAbsoluteTime = min(taskAbsoluteTime, nextCycleNearestEventAbsoluteTime) + nextCycleContainTimedEvent = true + + } else { + taskAbsoluteTime = 0 + nextCycleNearestEventAbsoluteTime = 0 + } + + val event = Event(callback, taskAbsoluteTime) + nextCycleEvents.add(event) + return event +} \ No newline at end of file diff --git a/kotlinx-coroutines-test/common/test/RunTestTest.kt b/kotlinx-coroutines-test/common/test/RunTestTest.kt index 799bcae269..720f1eca7a 100644 --- a/kotlinx-coroutines-test/common/test/RunTestTest.kt +++ b/kotlinx-coroutines-test/common/test/RunTestTest.kt @@ -135,6 +135,8 @@ class RunTestTest { @Test @NoJs @NoNative + @NoWasmWasi + @NoWasmJs fun testListingActiveCoroutinesOnTimeout(): TestResult { val name1 = "GoodUniqueName" val name2 = "BadUniqueName" diff --git a/kotlinx-coroutines-test/wasmWasi/src/TestBuilders.kt b/kotlinx-coroutines-test/wasmWasi/src/TestBuilders.kt new file mode 100644 index 0000000000..b06f3874bc --- /dev/null +++ b/kotlinx-coroutines-test/wasmWasi/src/TestBuilders.kt @@ -0,0 +1,19 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test + +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* +import kotlin.coroutines.* + +@Suppress("ACTUAL_WITHOUT_EXPECT") +public actual typealias TestResult = Unit + +internal actual fun systemPropertyImpl(name: String): String? = null + +internal actual fun createTestResult(testProcedure: suspend CoroutineScope.() -> Unit) = + runTestCoroutine(EmptyCoroutineContext, testProcedure) + +internal actual fun dumpCoroutines() { } \ No newline at end of file diff --git a/kotlinx-coroutines-test/wasmWasi/src/internal/TestMainDispatcher.kt b/kotlinx-coroutines-test/wasmWasi/src/internal/TestMainDispatcher.kt new file mode 100644 index 0000000000..915955c190 --- /dev/null +++ b/kotlinx-coroutines-test/wasmWasi/src/internal/TestMainDispatcher.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ + +package kotlinx.coroutines.test.internal +import kotlinx.coroutines.* + +@Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") +internal actual fun Dispatchers.getTestMainDispatcher(): TestMainDispatcher = + when (val mainDispatcher = Main) { + is TestMainDispatcher -> mainDispatcher + else -> TestMainDispatcher(mainDispatcher).also { injectMain(it) } + } \ No newline at end of file diff --git a/kotlinx-coroutines-test/wasmWasi/test/Helpers.kt b/kotlinx-coroutines-test/wasmWasi/test/Helpers.kt new file mode 100644 index 0000000000..dbd3c55e25 --- /dev/null +++ b/kotlinx-coroutines-test/wasmWasi/test/Helpers.kt @@ -0,0 +1,13 @@ +/* + * Copyright 2016-2024 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. + */ +package kotlinx.coroutines.test + +actual fun testResultChain(block: () -> TestResult, after: (Result) -> TestResult): TestResult { + try { + block() + after(Result.success(Unit)) + } catch (e: Throwable) { + after(Result.failure(e)) + } +} diff --git a/test-utils/build.gradle.kts b/test-utils/build.gradle.kts index 66074fbc60..3461d5292b 100644 --- a/test-utils/build.gradle.kts +++ b/test-utils/build.gradle.kts @@ -24,5 +24,10 @@ kotlin { api("org.jetbrains.kotlin:kotlin-test-wasm-js:${version("kotlin")}") } } + val wasmWasiMain by getting { + dependencies { + api("org.jetbrains.kotlin:kotlin-test-wasm-wasi:${version("kotlin")}") + } + } } } diff --git a/test-utils/common/src/TestBase.common.kt b/test-utils/common/src/TestBase.common.kt index 5c0cba4eec..5c63ea2196 100644 --- a/test-utils/common/src/TestBase.common.kt +++ b/test-utils/common/src/TestBase.common.kt @@ -206,6 +206,12 @@ expect annotation class NoJs() @OptionalExpectation expect annotation class NoNative() +@OptionalExpectation +expect annotation class NoWasmJs() + +@OptionalExpectation +expect annotation class NoWasmWasi() + expect val isStressTest: Boolean expect val stressTestMultiplier: Int expect val stressTestMultiplierSqrt: Int diff --git a/test-utils/wasmJs/src/TestBase.kt b/test-utils/wasmJs/src/TestBase.kt index 0edd291b8e..bada7d9c6d 100644 --- a/test-utils/wasmJs/src/TestBase.kt +++ b/test-utils/wasmJs/src/TestBase.kt @@ -6,7 +6,7 @@ import kotlinx.coroutines.* actual val VERBOSE = false -actual typealias NoJs = Ignore +actual typealias NoWasmJs = Ignore actual val isStressTest: Boolean = false actual val stressTestMultiplier: Int = 1 diff --git a/test-utils/wasmWasi/src/TestBase.kt b/test-utils/wasmWasi/src/TestBase.kt new file mode 100644 index 0000000000..e22e59a753 --- /dev/null +++ b/test-utils/wasmWasi/src/TestBase.kt @@ -0,0 +1,68 @@ +package kotlinx.coroutines.testing + +import kotlin.test.* +import kotlinx.coroutines.* +import kotlinx.coroutines.internal.* + +actual val VERBOSE = false + +actual typealias NoWasmWasi = Ignore + +actual val isStressTest: Boolean = false +actual val stressTestMultiplier: Int = 1 +actual val stressTestMultiplierSqrt: Int = 1 + +actual typealias TestResult = Unit + +internal actual fun lastResortReportException(error: Throwable) { + println(error) +} + +actual open class TestBase( + private val errorCatching: ErrorCatching.Impl +): OrderedExecutionTestBase(), ErrorCatching by errorCatching { + + actual constructor(): this(errorCatching = ErrorCatching.Impl()) + + actual fun println(message: Any?) { + kotlin.io.println(message) + } + + public actual fun runTest( + expected: ((Throwable) -> Boolean)?, + unhandled: List<(Throwable) -> Boolean>, + block: suspend CoroutineScope.() -> Unit + ): TestResult { + var exCount = 0 + var ex: Throwable? = null + try { + runTestCoroutine(block = block, context = CoroutineExceptionHandler { _, e -> + if (e is CancellationException) return@CoroutineExceptionHandler // are ignored + exCount++ + when { + exCount > unhandled.size -> + error("Too many unhandled exceptions $exCount, expected ${unhandled.size}, got: $e", e) + !unhandled[exCount - 1](e) -> + error("Unhandled exception was unexpected: $e", e) + } + }) + } catch (e: Throwable) { + ex = e + if (expected != null) { + if (!expected(e)) + error("Unexpected exception: $e", e) + } else + throw e + } finally { + if (ex == null && expected != null) kotlin.error("Exception was expected but none produced") + } + if (exCount < unhandled.size) + kotlin.error("Too few unhandled exceptions $exCount, expected ${unhandled.size}") + } +} + +actual val isNative = false + +actual val isBoundByJsTestTimeout = true + +actual val isJavaAndWindows: Boolean get() = false