From 18a9c1916e64411e00b0808772589cfc1fb6def7 Mon Sep 17 00:00:00 2001 From: Igor Yakovlev Date: Mon, 31 Jul 2023 14:28:54 +0200 Subject: [PATCH] [Wasm] Wasi kotlin.test implementation --- .../kotlin/kotlin/test/JasmineLikeAdapter.kt | 0 .../test/TeamcityAdapterWithPromiseSupport.kt | 63 +++++++++++++++ .../js/src/main/kotlin/kotlin/test/TestApi.kt | 27 +++++++ .../kotlin/kotlin/test/TeamcityAdapter.kt | 66 ++++------------ .../src/main/kotlin/kotlin/test/TestApi.kt | 26 +------ .../kotlin/kotlin/test/TeamcityAdapter.kt | 78 +++++++++++++++++++ .../src/main/kotlin/kotlin/test/TestApi.kt | 10 +++ 7 files changed, 193 insertions(+), 77 deletions(-) rename libraries/kotlin.test/wasm/{ => js}/src/main/kotlin/kotlin/test/JasmineLikeAdapter.kt (100%) create mode 100644 libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/TeamcityAdapterWithPromiseSupport.kt create mode 100644 libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/TestApi.kt create mode 100644 libraries/kotlin.test/wasm/wasi/src/main/kotlin/kotlin/test/TeamcityAdapter.kt create mode 100644 libraries/kotlin.test/wasm/wasi/src/main/kotlin/kotlin/test/TestApi.kt diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/JasmineLikeAdapter.kt b/libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/JasmineLikeAdapter.kt similarity index 100% rename from libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/JasmineLikeAdapter.kt rename to libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/JasmineLikeAdapter.kt diff --git a/libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/TeamcityAdapterWithPromiseSupport.kt b/libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/TeamcityAdapterWithPromiseSupport.kt new file mode 100644 index 00000000000..177fa056796 --- /dev/null +++ b/libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/TeamcityAdapterWithPromiseSupport.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +import kotlin.math.abs +import kotlin.js.* + +// Using 'globalThis.arguments' because 'arguments' can refer to current JS function arguments +@JsFun("() => globalThis.arguments?.join?.(' ') ?? ''") +private external fun d8Arguments(): String +@JsFun("() => (typeof process != 'undefined' && typeof process.argv != 'undefined') ? process.argv.slice(2).join(' ') : ''") +private external fun nodeArguments(): String + +internal fun getArguments(): List = (d8Arguments().ifEmpty { nodeArguments() }).split(' ') + +internal class TeamcityAdapterWithPromiseSupport : TeamcityAdapter() { + private var scheduleNextTaskAfter: Promise? = null + override fun runOrScheduleNext(block: () -> Unit) { + if (scheduleNextTaskAfter == null) { + block() + } else { + scheduleNextTaskAfter = scheduleNextTaskAfter!!.finally(block) + } + } + + override fun runOrScheduleNextWithResult(block: () -> Any?) { + val nextTask = scheduleNextTaskAfter + if (nextTask == null) { + val result = block() + if (result is Promise) + scheduleNextTaskAfter = result + } else { + scheduleNextTaskAfter = nextTask.then { + block() as? Promise + } + } + } + + override fun tryProcessResult(result: Any?, name: String): Any? { + if (result == null) return null + if (result !is Promise<*>) return null + + return result.then( + onFulfilled = { value -> + MessageType.Finished.report(name) + value + }, + onRejected = { e -> + val throwable = e.toThrowableOrNull() + if (throwable != null) { + MessageType.Failed.report(name, throwable) + } else { + MessageType.Failed.report(name, e.toString()) + } + MessageType.Finished.report(name) + null + } + ) + } +} \ No newline at end of file diff --git a/libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/TestApi.kt b/libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/TestApi.kt new file mode 100644 index 00000000000..c12ecc72668 --- /dev/null +++ b/libraries/kotlin.test/wasm/js/src/main/kotlin/kotlin/test/TestApi.kt @@ -0,0 +1,27 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +/** + * Overrides current framework adapter with a provided instance of [FrameworkAdapter]. Use in order to support custom test frameworks. + * + * If this function is not called, the test framework will be detected automatically. + * + */ +internal fun setAdapter(adapter: FrameworkAdapter) { + currentAdapter = adapter +} + +internal var currentAdapter: FrameworkAdapter? = null + +private fun isJasmine(): Boolean = + js("typeof describe === 'function' && typeof it === 'function'") + +internal fun adapter(): FrameworkAdapter { + val result = currentAdapter ?: if (isJasmine()) JasmineLikeAdapter() else TeamcityAdapterWithPromiseSupport() + currentAdapter = result + return result +} \ No newline at end of file diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TeamcityAdapter.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TeamcityAdapter.kt index 709e94a2905..b961c73cb0e 100644 --- a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TeamcityAdapter.kt +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TeamcityAdapter.kt @@ -6,38 +6,13 @@ package kotlin.test import kotlin.math.abs -import kotlin.js.* -// Using 'globalThis.arguments' because 'arguments' can refer to current JS function arguments -@JsFun("() => globalThis.arguments?.join?.(' ') ?? ''") -private external fun d8Arguments(): String -@JsFun("() => (typeof process != 'undefined' && typeof process.argv != 'undefined') ? process.argv.slice(2).join(' ') : ''") -private external fun nodeArguments(): String +internal open class TeamcityAdapter : FrameworkAdapter { + protected open fun runOrScheduleNext(block: () -> Unit) = block() + protected open fun runOrScheduleNextWithResult(block: () -> Any?) = block() + protected open fun tryProcessResult(result: Any?, name: String): Any? = null -internal class TeamcityAdapter : FrameworkAdapter { - - private var scheduleNextTaskAfter: Promise? = null - private fun runOrScheduleNext(block: () -> Unit) { - if (scheduleNextTaskAfter == null) { - block() - } else { - scheduleNextTaskAfter = scheduleNextTaskAfter!!.finally(block) - } - } - - private fun runOrScheduleNextWithResult(block: () -> Promise?) { - if (scheduleNextTaskAfter == null) { - val result = block() - if (result != null) - scheduleNextTaskAfter = result - } else { - scheduleNextTaskAfter = scheduleNextTaskAfter!!.then { - block() - } - } - } - - private enum class MessageType(val type: String) { + internal enum class MessageType(val type: String) { Started("testStarted"), Finished("testFinished"), Failed("testFailed"), @@ -61,11 +36,11 @@ internal class TeamcityAdapter : FrameworkAdapter { private val flowId: String = "flowId='wasmTcAdapter${abs(hashCode())}'" - private fun MessageType.report(name: String) { + internal fun MessageType.report(name: String) { println("##teamcity[$type name='${name.tcEscape()}' $flowId]") } - private fun MessageType.report(name: String, e: Throwable) { + internal fun MessageType.report(name: String, e: Throwable) { if (this == MessageType.Failed) { println("##teamcity[$type name='${name.tcEscape()}' message='${e.message.tcEscape()}' details='${e.stackTraceToString().tcEscape()}' $flowId]") } else { @@ -73,7 +48,7 @@ internal class TeamcityAdapter : FrameworkAdapter { } } - private fun MessageType.report(name: String, errorMessage: String) { + internal fun MessageType.report(name: String, errorMessage: String) { println("##teamcity[$type name='${name.tcEscape()}' message='${errorMessage.tcEscape()}' $flowId]") } @@ -82,8 +57,7 @@ internal class TeamcityAdapter : FrameworkAdapter { get() { var value = _testArguments if (value == null) { - val arguments = d8Arguments().ifEmpty { nodeArguments() } - value = FrameworkTestArguments.parse(arguments.split(' ')) + value = FrameworkTestArguments.parse(getArguments()) _testArguments = value } @@ -212,24 +186,10 @@ internal class TeamcityAdapter : FrameworkAdapter { } catch (e: Throwable) { MessageType.Failed.report(name, e) } - if (result is Promise<*>) { - return@runOrScheduleNextWithResult result.then( - onFulfilled = { value -> - MessageType.Finished.report(name) - value - }, - onRejected = { e -> - val throwable = e.toThrowableOrNull() - if (throwable != null) { - MessageType.Failed.report(name, throwable) - } else { - MessageType.Failed.report(name, e.toString()) - } - MessageType.Finished.report(name) - null - } - ) - } + + val processed = tryProcessResult(result, name) + if (processed != null) return@runOrScheduleNextWithResult processed + MessageType.Finished.report(name) return@runOrScheduleNextWithResult null } diff --git a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TestApi.kt b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TestApi.kt index 321fc068aa5..a197b42247f 100644 --- a/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TestApi.kt +++ b/libraries/kotlin.test/wasm/src/main/kotlin/kotlin/test/TestApi.kt @@ -5,17 +5,7 @@ package kotlin.test -import kotlin.js.JsExport - -/** - * Overrides current framework adapter with a provided instance of [FrameworkAdapter]. Use in order to support custom test frameworks. - * - * If this function is not called, the test framework will be detected automatically. - * - */ -internal fun setAdapter(adapter: FrameworkAdapter) { - currentAdapter = adapter -} +import kotlin.wasm.WasmExport /** * The functions below are used by the compiler to describe the tests structure, e.g. @@ -37,20 +27,8 @@ internal fun test(name: String, ignored: Boolean, testFn: () -> Any?) { adapter().test(name, ignored, testFn) } -internal var currentAdapter: FrameworkAdapter? = null - -private fun isJasmine(): Boolean = - js("typeof describe === 'function' && typeof it === 'function'") - -internal fun adapter(): FrameworkAdapter { - val result = currentAdapter ?: if (isJasmine()) JasmineLikeAdapter() else TeamcityAdapter() - currentAdapter = result - return result -} - // This is called from the js-launcher alongside wasm start function -@JsExport -@OptIn(kotlin.js.ExperimentalJsExport::class) +@WasmExport internal fun startUnitTests() { // This will be filled with the corresponding code during lowering } diff --git a/libraries/kotlin.test/wasm/wasi/src/main/kotlin/kotlin/test/TeamcityAdapter.kt b/libraries/kotlin.test/wasm/wasi/src/main/kotlin/kotlin/test/TeamcityAdapter.kt new file mode 100644 index 00000000000..d61e1d51b22 --- /dev/null +++ b/libraries/kotlin.test/wasm/wasi/src/main/kotlin/kotlin/test/TeamcityAdapter.kt @@ -0,0 +1,78 @@ +/* + * Copyright 2010-2023 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +import kotlin.wasm.WasmImport +import kotlin.wasm.unsafe.* + +/** + * Read command-line argument data. The size of the array should match that returned by + * `args_sizes_get`. Each argument is expected to be `\0` terminated. + */ +@WasmImport("wasi_snapshot_preview1", "args_get") +private external fun wasiRawArgsGet( + argvPtr: Int, + argvBuf: Int, +): Int + + +/** Return command-line argument data sizes. */ +@WasmImport("wasi_snapshot_preview1", "args_sizes_get") +private external fun wasiRawArgsSizesGet( + argumentNumberPtr: Int, + argumentStringSizePtr: Int, +): Int + +@OptIn(UnsafeWasmMemoryApi::class) +internal fun getArguments(): List = withScopedMemoryAllocator { allocator -> + val numberOfArgumentsPtr = allocator.allocate(4) + val sizeOfArgumentStringPtr = allocator.allocate(4) + val argNumRes = wasiRawArgsSizesGet( + argumentNumberPtr = numberOfArgumentsPtr.address.toInt(), + argumentStringSizePtr = sizeOfArgumentStringPtr.address.toInt() + ) + if (argNumRes != 0) { + throw IllegalStateException("Wasi error code $argNumRes") + } + + val argumentNumber = numberOfArgumentsPtr.loadInt() + if (argumentNumber <= 2) return emptyList() + + val argumentStringSize = sizeOfArgumentStringPtr.loadInt() + val stringBufferPtr = allocator.allocate(argumentStringSize) + + val argvPtr = allocator.allocate(argumentNumber * Int.SIZE_BYTES) + + val argRes = wasiRawArgsGet(argvPtr.address.toInt(), stringBufferPtr.address.toInt()) + if (argRes != 0) { + throw IllegalStateException("Wasi error code $argNumRes") + } + + val startAddress = (argvPtr + 2 * Int.SIZE_BYTES).loadInt().toUInt() + val endAddress = stringBufferPtr.address + argumentStringSize.toUInt() + decodeStrings(argumentStringSize, startAddress, endAddress) +} + +@OptIn(UnsafeWasmMemoryApi::class) +private fun decodeStrings(arraySize: Int, startAddress: UInt, endAddress: UInt): List { + val buffer = ByteArray(arraySize) + val result = mutableListOf() + + var currentPtr = Pointer(startAddress) + var currentIndex = 0 + while (currentPtr.address < endAddress) { + val byte = currentPtr.loadByte() + if (byte.toInt() == 0) { + result.add(buffer.decodeToString(0, currentIndex)) + currentIndex = 0 + } else { + buffer[currentIndex] = byte + currentIndex++ + } + currentPtr += 1 + } + return result +} \ No newline at end of file diff --git a/libraries/kotlin.test/wasm/wasi/src/main/kotlin/kotlin/test/TestApi.kt b/libraries/kotlin.test/wasm/wasi/src/main/kotlin/kotlin/test/TestApi.kt new file mode 100644 index 00000000000..6e3f8ac1456 --- /dev/null +++ b/libraries/kotlin.test/wasm/wasi/src/main/kotlin/kotlin/test/TestApi.kt @@ -0,0 +1,10 @@ +/* + * Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package kotlin.test + +private val currentAdapter: FrameworkAdapter by lazy { TeamcityAdapter() } + +internal fun adapter(): FrameworkAdapter = currentAdapter \ No newline at end of file