[Wasm] Wasi kotlin.test implementation

This commit is contained in:
Igor Yakovlev
2023-07-31 14:28:54 +02:00
committed by Zalim Bashorov
parent 8cc0660693
commit 18a9c1916e
7 changed files with 193 additions and 77 deletions
@@ -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<String> = (d8Arguments().ifEmpty { nodeArguments() }).split(' ')
internal class TeamcityAdapterWithPromiseSupport : TeamcityAdapter() {
private var scheduleNextTaskAfter: Promise<JsAny?>? = 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<JsAny?>)
scheduleNextTaskAfter = result
} else {
scheduleNextTaskAfter = nextTask.then {
block() as? Promise<JsAny?>
}
}
}
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
}
)
}
}
@@ -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
}
@@ -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<JsAny?>? = null
private fun runOrScheduleNext(block: () -> Unit) {
if (scheduleNextTaskAfter == null) {
block()
} else {
scheduleNextTaskAfter = scheduleNextTaskAfter!!.finally(block)
}
}
private fun runOrScheduleNextWithResult(block: () -> Promise<JsAny?>?) {
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
}
@@ -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
}
@@ -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<String> = 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<String> {
val buffer = ByteArray(arraySize)
val result = mutableListOf<String>()
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
}
@@ -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