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 416fc0a6f47..51d39383288 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 @@ -7,6 +7,7 @@ package kotlin.test import kotlin.test.FrameworkAdapter import kotlin.math.abs +import kotlin.js.* @JsFun("() => (typeof arguments !== 'undefined' && typeof arguments.join !== 'undefined') ? arguments.join(' ') : '' ") private external fun d8Arguments(): String @@ -14,6 +15,28 @@ private external fun d8Arguments(): String private external fun nodeArguments(): String 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() as Dynamic? + } + } + } + private enum class MessageType(val type: String) { Started("testStarted"), Finished("testFinished"), @@ -50,25 +73,40 @@ internal class TeamcityAdapter : FrameworkAdapter { } } + private fun MessageType.report(name: String, errorMessage: String) { + println("##teamcity[$type name='${name.tcEscape()}' message='${errorMessage.tcEscape()}' $flowId]") + } + + private val testArguments: FrameworkTestArguments by lazy { val arguments = d8Arguments().takeIf { it.isNotEmpty() } ?: nodeArguments() FrameworkTestArguments.parse(arguments.split(' ')) } private fun runSuite(name: String, suiteFn: () -> Unit) { - MessageType.SuiteStarted.report(name) + runOrScheduleNext { + MessageType.SuiteStarted.report(name) + } try { suiteFn() - MessageType.SuiteFinished.report(name) + runOrScheduleNext { + MessageType.SuiteFinished.report(name) + } } catch (e: Throwable) { - MessageType.SuiteFinished.report(name, e) + runOrScheduleNext { + MessageType.SuiteFinished.report(name, e) + } } } private fun runIgnoredSuite(name: String, suiteFn: () -> Unit) { - MessageType.SuiteStarted.report(name) + runOrScheduleNext { + MessageType.SuiteStarted.report(name) + } suiteFn() - MessageType.SuiteFinished.report(name) + runOrScheduleNext { + MessageType.SuiteFinished.report(name) + } } private var isUnderIgnoredSuit: Boolean = false @@ -117,45 +155,77 @@ internal class TeamcityAdapter : FrameworkAdapter { runSuite(name, suiteFn) } } else { - when (testArguments.ignoredTestSuites) { - IgnoredTestSuitesReporting.reportAsIgnoredTest -> { - MessageType.Ignored.report(name) - } - IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored -> { - var oldIsUnderIgnoredSuit = isUnderIgnoredSuit - isUnderIgnoredSuit = true - try { - runIgnoredSuite(name, suiteFn) - } finally { - isUnderIgnoredSuit = oldIsUnderIgnoredSuit + runOrScheduleNext { + when (testArguments.ignoredTestSuites) { + IgnoredTestSuitesReporting.reportAsIgnoredTest -> { + MessageType.Ignored.report(name) } + + IgnoredTestSuitesReporting.reportAllInnerTestsAsIgnored -> { + var oldIsUnderIgnoredSuit = isUnderIgnoredSuit + isUnderIgnoredSuit = true + try { + runIgnoredSuite(name, suiteFn) + } finally { + isUnderIgnoredSuit = oldIsUnderIgnoredSuit + } + } + + IgnoredTestSuitesReporting.skip -> {} } - IgnoredTestSuitesReporting.skip -> { } } } } override fun test(name: String, ignored: Boolean, testFn: () -> Any?) { if (isUnderIgnoredSuit) { - MessageType.Ignored.report(name) + runOrScheduleNext { + MessageType.Ignored.report(name) + } return } if (ignored) { - MessageType.Ignored.report(name) + runOrScheduleNext { + MessageType.Ignored.report(name) + } return } enterIfIncluded(name, false) { - try { + runOrScheduleNextWithResult { MessageType.Started.report(name) - if (!testArguments.dryRun) { - testFn() + + val result = try { + if (!testArguments.dryRun) { + testFn() + } else { + null + } + } catch (e: Throwable) { + MessageType.Failed.report(name, e) } - } 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 + } + ) + } + MessageType.Finished.report(name) + return@runOrScheduleNextWithResult null } - MessageType.Finished.report(name) } } } diff --git a/libraries/stdlib/wasm/src/kotlin/Dynamic.kt b/libraries/stdlib/wasm/src/kotlin/Dynamic.kt index 263789b64c9..8fab39a52ab 100644 --- a/libraries/stdlib/wasm/src/kotlin/Dynamic.kt +++ b/libraries/stdlib/wasm/src/kotlin/Dynamic.kt @@ -201,4 +201,36 @@ fun Nothing?.unsafeCast(): Dynamic? = null /** * Reinterprets Dynamic type value as a value of the specified type [T] without any actual type checking. */ -fun Dynamic.unsafeCast(): T = this as T \ No newline at end of file +fun Dynamic.unsafeCast(): T = this as T + +@JsFun("e => { throw e; }") +private external fun jsThrow(e: Dynamic) + +@JsFun("""(f) => { + let result = null; + try { + f(); + } catch (e) { + result = e; + } + return result; +}""") +private external fun jsCatch(f: () -> Unit): Dynamic? + +/** + * For a Dynamic value caught in JS, returns the corresponding [Throwable] + * if it was thrown from Kotlin, or null otherwise. + */ +public fun Dynamic.toThrowableOrNull(): Throwable? { + val thisAny: Any = this + if (thisAny is Throwable) return thisAny + var result: Throwable? = null + jsCatch { + try { + jsThrow(this) + } catch (e: Throwable) { + result = e + } + } + return result +} diff --git a/libraries/stdlib/wasm/src/kotlin/js/Promise.kt b/libraries/stdlib/wasm/src/kotlin/js/Promise.kt new file mode 100644 index 00000000000..a04fc9eb4c8 --- /dev/null +++ b/libraries/stdlib/wasm/src/kotlin/js/Promise.kt @@ -0,0 +1,24 @@ +/* + * 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.js + +/** + * Exposes the JavaScript [Promise object](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise) to Kotlin. + */ +public external class Promise(executor: (resolve: (Dynamic?) -> Unit, reject: (Dynamic) -> Unit) -> Unit) { + public open fun then(onFulfilled: (Dynamic?) -> Dynamic?): Promise + public open fun then(onFulfilled: (Dynamic?) -> Dynamic?, onRejected: (Dynamic) -> Dynamic?): Promise + public open fun catch(onRejected: (Dynamic) -> Dynamic?): Promise + public open fun finally(onFinally: () -> Unit): Promise + + public companion object { + public fun reject(e: Dynamic): Promise + public fun resolve(e: Dynamic): Promise + public fun resolve(e: Promise): Promise + } +} + + diff --git a/libraries/stdlib/wasm/src/kotlin/w3cSupport.kt b/libraries/stdlib/wasm/src/kotlin/w3cSupport.kt index 61a497cd9e4..bbbac2886a3 100644 --- a/libraries/stdlib/wasm/src/kotlin/w3cSupport.kt +++ b/libraries/stdlib/wasm/src/kotlin/w3cSupport.kt @@ -5,8 +5,6 @@ package kotlin.js -external class Promise - @PublishedApi internal val undefined: Nothing? = null diff --git a/libraries/stdlib/wasm/test/js/AsyncTest.kt b/libraries/stdlib/wasm/test/js/AsyncTest.kt new file mode 100644 index 00000000000..e8f6563e709 --- /dev/null +++ b/libraries/stdlib/wasm/test/js/AsyncTest.kt @@ -0,0 +1,63 @@ +package test.js + +import kotlin.js.* +import kotlin.test.* + +@JsFun("async () => 'foo'") +internal external fun jsAsyncFoo(): Promise + +@JsFun("() => 'foo'") +internal external fun jsFoo(): Dynamic + +var state: Dynamic? = null + +class MyThrowable : Throwable() + +class AsyncTest { + @Test + fun test1(): Promise { + var thenExecuted = false + val p = jsAsyncFoo().then { value -> + state = value + thenExecuted = true + null + } + assertFalse(thenExecuted) + return p + } + + @Test + fun test2(): Promise { + assertEquals(state, jsFoo()) + var thenExecuted = false + val p = jsAsyncFoo().then { + assertEquals(state, jsFoo()) + thenExecuted = true + null + }.then { + assertEquals(state, jsFoo()) + thenExecuted = true + null + } + assertFalse(thenExecuted) + return p + } + + @Test + fun testJsValueToThrowableOrNull1(): Promise { + val p = jsAsyncFoo().then { + throw MyThrowable() + null + }.catch { e -> + assert(e.toThrowableOrNull() is MyThrowable) + null + } + return p + } + + @Test + fun testJsValueToThrowableOrNull2() { + val e = MyThrowable() + assertEquals((e as Dynamic).toThrowableOrNull(), e) + } +} \ No newline at end of file