blob: c930c20030d597280dc9a28796ce5284da86d961 [file] [log] [blame]
Roman Elizarova7db8ec2017-12-21 22:45:12 +03001/*
Roman Elizarov1f74a2d2018-06-29 19:19:45 +03002 * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
Roman Elizarova7db8ec2017-12-21 22:45:12 +03003 */
4
Roman Elizarov0950dfa2018-07-13 10:33:25 +03005package kotlinx.coroutines
Roman Elizarov8bff72b2017-12-20 12:55:38 +03006
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +03007import kotlin.js.*
8
Nikita Koval8fa07b52019-09-27 18:26:03 +03009public actual val isStressTest: Boolean = false
10public actual val stressTestMultiplier: Int = 1
Steve Elliottca095be2022-07-25 14:26:10 +000011public actual val stressTestMultiplierSqrt: Int = 1
Roman Elizarov8bff72b2017-12-20 12:55:38 +030012
Vsevolod Tolstopyatova3429f72021-07-16 16:02:36 +030013@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE")
14public actual typealias TestResult = Promise<Unit>
15
Steve Elliottca095be2022-07-25 14:26:10 +000016public actual val isNative = false
17
Nikita Koval8fa07b52019-09-27 18:26:03 +030018public actual open class TestBase actual constructor() {
Vsevolod Tolstopyatov25715162021-07-16 14:38:58 +030019 public actual val isBoundByJsTestTimeout = true
Roman Elizarov8bff72b2017-12-20 12:55:38 +030020 private var actionIndex = 0
21 private var finished = false
22 private var error: Throwable? = null
Vsevolod Tolstopyatov25715162021-07-16 14:38:58 +030023 private var lastTestPromise: Promise<*>? = null
Roman Elizarov8bff72b2017-12-20 12:55:38 +030024
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 */
Ilya Gorbunovf0cd1802018-04-17 19:59:31 +030029 @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
Roman Elizarov8bff72b2017-12-20 12:55:38 +030030 public actual fun error(message: Any, cause: Throwable? = null): Nothing {
Roman Elizarovaa461cf2018-04-11 13:20:29 +030031 if (cause != null) console.log(cause)
Roman Elizarov8bff72b2017-12-20 12:55:38 +030032 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
Roman Elizarov7587eba2018-07-25 12:22:46 +030038 private fun printError(message: String, cause: Throwable) {
39 if (error == null) error = cause
40 println("$message: $cause")
41 console.log(cause)
42 }
43
Roman Elizarov8bff72b2017-12-20 12:55:38 +030044 /**
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
Vsevolod Tolstopyatovfe820ba2019-04-24 17:14:03 +030068 /**
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
Vsevolod Tolstopyatov732474f2018-07-20 11:36:20 +030075 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
Ilya Gorbunovf0cd1802018-04-17 19:59:31 +030081 @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
Roman Elizarov8bff72b2017-12-20 12:55:38 +030082 public actual fun runTest(
83 expected: ((Throwable) -> Boolean)? = null,
84 unhandled: List<(Throwable) -> Boolean> = emptyList(),
85 block: suspend CoroutineScope.() -> Unit
Vsevolod Tolstopyatova3429f72021-07-16 16:02:36 +030086 ): TestResult {
Roman Elizarov8bff72b2017-12-20 12:55:38 +030087 var exCount = 0
88 var ex: Throwable? = null
Vsevolod Tolstopyatov25715162021-07-16 14:38:58 +030089 /*
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 }
Vsevolod Tolstopyatov8baa7362021-08-02 12:55:35 +0300111 val result = GlobalScope.promise(block = block, context = CoroutineExceptionHandler { _, e ->
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300112 if (e is CancellationException) return@CoroutineExceptionHandler // are ignored
113 exCount++
Roman Elizarov7587eba2018-07-25 12:22:46 +0300114 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 }
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300120 }).catch { e ->
Roman Elizarov8bff72b2017-12-20 12:55:38 +0300121 ex = e
122 if (expected != null) {
123 if (!expected(e))
124 error("Unexpected exception", e)
125 } else
126 throw e
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300127 }.finally {
Roman Elizarov8bff72b2017-12-20 12:55:38 +0300128 if (ex == null && expected != null) error("Exception was expected but none produced")
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300129 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" }
Roman Elizarov8bff72b2017-12-20 12:55:38 +0300133 }
Vsevolod Tolstopyatov25715162021-07-16 14:38:58 +0300134 lastTestPromise = result
135 return result
Roman Elizarov8bff72b2017-12-20 12:55:38 +0300136 }
137}
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300138
139private fun <T> Promise<T>.finally(block: () -> Unit): Promise<T> =
140 then(onFulfilled = { value -> block(); value }, onRejected = { ex -> block(); throw ex })