1 /*
<lambda>null2 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3 */
4
5 package kotlinx.coroutines
6
7 import kotlin.js.*
8
9 public actual val isStressTest: Boolean = false
10 public actual val stressTestMultiplier: Int = 1
11 public actual val stressTestMultiplierSqrt: Int = 1
12
13 @Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE")
14 public actual typealias TestResult = Promise<Unit>
15
16 public actual val isNative = false
17
18 public actual open class TestBase actual constructor() {
19 public actual val isBoundByJsTestTimeout = true
20 private var actionIndex = 0
21 private var finished = false
22 private var error: Throwable? = null
23 private var lastTestPromise: Promise<*>? = null
24
25 /**
26 * Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not
27 * complete successfully even if this exception is consumed somewhere in the test.
28 */
29 @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
30 public actual fun error(message: Any, cause: Throwable? = null): Nothing {
31 if (cause != null) console.log(cause)
32 val exception = IllegalStateException(
33 if (cause == null) message.toString() else "$message; caused by $cause")
34 if (error == null) error = exception
35 throw exception
36 }
37
38 private fun printError(message: String, cause: Throwable) {
39 if (error == null) error = cause
40 println("$message: $cause")
41 console.log(cause)
42 }
43
44 /**
45 * Asserts that this invocation is `index`-th in the execution sequence (counting from one).
46 */
47 public actual fun expect(index: Int) {
48 val wasIndex = ++actionIndex
49 check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
50 }
51
52 /**
53 * Asserts that this line is never executed.
54 */
55 public actual fun expectUnreached() {
56 error("Should not be reached")
57 }
58
59 /**
60 * Asserts that this it the last action in the test. It must be invoked by any test that used [expect].
61 */
62 public actual fun finish(index: Int) {
63 expect(index)
64 check(!finished) { "Should call 'finish(...)' at most once" }
65 finished = true
66 }
67
68 /**
69 * Asserts that [finish] was invoked
70 */
71 public actual fun ensureFinished() {
72 require(finished) { "finish(...) should be caller prior to this check" }
73 }
74
75 public actual fun reset() {
76 check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" }
77 actionIndex = 0
78 finished = false
79 }
80
81 @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
82 public actual fun runTest(
83 expected: ((Throwable) -> Boolean)? = null,
84 unhandled: List<(Throwable) -> Boolean> = emptyList(),
85 block: suspend CoroutineScope.() -> Unit
86 ): TestResult {
87 var exCount = 0
88 var ex: Throwable? = null
89 /*
90 * This is an additional sanity check against `runTest` mis-usage on JS.
91 * The only way to write an async test on JS is to return Promise from the test function.
92 * _Just_ launching promise and returning `Unit` won't suffice as the underlying test framework
93 * won't be able to detect an asynchronous failure in a timely manner.
94 * We cannot detect such situations, but we can detect the most common erroneous pattern
95 * in our code base, an attempt to use multiple `runTest` in the same `@Test` method,
96 * which typically is a premise to the same error:
97 * ```
98 * @Test
99 * fun incorrectTestForJs() { // <- promise is not returned
100 * for (parameter in parameters) {
101 * runTest {
102 * runTestForParameter(parameter)
103 * }
104 * }
105 * }
106 * ```
107 */
108 if (lastTestPromise != null) {
109 error("Attempt to run multiple asynchronous test within one @Test method")
110 }
111 val result = GlobalScope.promise(block = block, context = CoroutineExceptionHandler { _, e ->
112 if (e is CancellationException) return@CoroutineExceptionHandler // are ignored
113 exCount++
114 when {
115 exCount > unhandled.size ->
116 printError("Too many unhandled exceptions $exCount, expected ${unhandled.size}, got: $e", e)
117 !unhandled[exCount - 1](e) ->
118 printError("Unhandled exception was unexpected: $e", e)
119 }
120 }).catch { e ->
121 ex = e
122 if (expected != null) {
123 if (!expected(e))
124 error("Unexpected exception", e)
125 } else
126 throw e
127 }.finally {
128 if (ex == null && expected != null) error("Exception was expected but none produced")
129 if (exCount < unhandled.size)
130 error("Too few unhandled exceptions $exCount, expected ${unhandled.size}")
131 error?.let { throw it }
132 check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" }
133 }
134 lastTestPromise = result
135 return result
136 }
137 }
138
finallynull139 private fun <T> Promise<T>.finally(block: () -> Unit): Promise<T> =
140 then(onFulfilled = { value -> block(); value }, onRejected = { ex -> block(); throw ex })
141