blob: cc7865ba0721655c76354af8ede11258f634551f [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
Roman Elizarov8bff72b2017-12-20 12:55:38 +030011
Vsevolod Tolstopyatova3429f72021-07-16 16:02:36 +030012@Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE")
13public actual typealias TestResult = Promise<Unit>
14
Nikita Koval8fa07b52019-09-27 18:26:03 +030015public actual open class TestBase actual constructor() {
Vsevolod Tolstopyatov25715162021-07-16 14:38:58 +030016 public actual val isBoundByJsTestTimeout = true
Roman Elizarov8bff72b2017-12-20 12:55:38 +030017 private var actionIndex = 0
18 private var finished = false
19 private var error: Throwable? = null
Vsevolod Tolstopyatov25715162021-07-16 14:38:58 +030020 private var lastTestPromise: Promise<*>? = null
Roman Elizarov8bff72b2017-12-20 12:55:38 +030021
22 /**
23 * Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not
24 * complete successfully even if this exception is consumed somewhere in the test.
25 */
Ilya Gorbunovf0cd1802018-04-17 19:59:31 +030026 @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
Roman Elizarov8bff72b2017-12-20 12:55:38 +030027 public actual fun error(message: Any, cause: Throwable? = null): Nothing {
Roman Elizarovaa461cf2018-04-11 13:20:29 +030028 if (cause != null) console.log(cause)
Roman Elizarov8bff72b2017-12-20 12:55:38 +030029 val exception = IllegalStateException(
30 if (cause == null) message.toString() else "$message; caused by $cause")
31 if (error == null) error = exception
32 throw exception
33 }
34
Roman Elizarov7587eba2018-07-25 12:22:46 +030035 private fun printError(message: String, cause: Throwable) {
36 if (error == null) error = cause
37 println("$message: $cause")
38 console.log(cause)
39 }
40
Roman Elizarov8bff72b2017-12-20 12:55:38 +030041 /**
42 * Asserts that this invocation is `index`-th in the execution sequence (counting from one).
43 */
44 public actual fun expect(index: Int) {
45 val wasIndex = ++actionIndex
46 check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
47 }
48
49 /**
50 * Asserts that this line is never executed.
51 */
52 public actual fun expectUnreached() {
53 error("Should not be reached")
54 }
55
56 /**
57 * Asserts that this it the last action in the test. It must be invoked by any test that used [expect].
58 */
59 public actual fun finish(index: Int) {
60 expect(index)
61 check(!finished) { "Should call 'finish(...)' at most once" }
62 finished = true
63 }
64
Vsevolod Tolstopyatovfe820ba2019-04-24 17:14:03 +030065 /**
66 * Asserts that [finish] was invoked
67 */
68 public actual fun ensureFinished() {
69 require(finished) { "finish(...) should be caller prior to this check" }
70 }
71
Vsevolod Tolstopyatov732474f2018-07-20 11:36:20 +030072 public actual fun reset() {
73 check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" }
74 actionIndex = 0
75 finished = false
76 }
77
Ilya Gorbunovf0cd1802018-04-17 19:59:31 +030078 @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
Roman Elizarov8bff72b2017-12-20 12:55:38 +030079 public actual fun runTest(
80 expected: ((Throwable) -> Boolean)? = null,
81 unhandled: List<(Throwable) -> Boolean> = emptyList(),
82 block: suspend CoroutineScope.() -> Unit
Vsevolod Tolstopyatova3429f72021-07-16 16:02:36 +030083 ): TestResult {
Roman Elizarov8bff72b2017-12-20 12:55:38 +030084 var exCount = 0
85 var ex: Throwable? = null
Vsevolod Tolstopyatov25715162021-07-16 14:38:58 +030086 /*
87 * This is an additional sanity check against `runTest` mis-usage on JS.
88 * The only way to write an async test on JS is to return Promise from the test function.
89 * _Just_ launching promise and returning `Unit` won't suffice as the underlying test framework
90 * won't be able to detect an asynchronous failure in a timely manner.
91 * We cannot detect such situations, but we can detect the most common erroneous pattern
92 * in our code base, an attempt to use multiple `runTest` in the same `@Test` method,
93 * which typically is a premise to the same error:
94 * ```
95 * @Test
96 * fun incorrectTestForJs() { // <- promise is not returned
97 * for (parameter in parameters) {
98 * runTest {
99 * runTestForParameter(parameter)
100 * }
101 * }
102 * }
103 * ```
104 */
105 if (lastTestPromise != null) {
106 error("Attempt to run multiple asynchronous test within one @Test method")
107 }
Vsevolod Tolstopyatov8baa7362021-08-02 12:55:35 +0300108 val result = GlobalScope.promise(block = block, context = CoroutineExceptionHandler { _, e ->
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300109 if (e is CancellationException) return@CoroutineExceptionHandler // are ignored
110 exCount++
Roman Elizarov7587eba2018-07-25 12:22:46 +0300111 when {
112 exCount > unhandled.size ->
113 printError("Too many unhandled exceptions $exCount, expected ${unhandled.size}, got: $e", e)
114 !unhandled[exCount - 1](e) ->
115 printError("Unhandled exception was unexpected: $e", e)
116 }
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300117 }).catch { e ->
Roman Elizarov8bff72b2017-12-20 12:55:38 +0300118 ex = e
119 if (expected != null) {
120 if (!expected(e))
121 error("Unexpected exception", e)
122 } else
123 throw e
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300124 }.finally {
Roman Elizarov8bff72b2017-12-20 12:55:38 +0300125 if (ex == null && expected != null) error("Exception was expected but none produced")
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300126 if (exCount < unhandled.size)
127 error("Too few unhandled exceptions $exCount, expected ${unhandled.size}")
128 error?.let { throw it }
129 check(actionIndex == 0 || finished) { "Expecting that 'finish(...)' was invoked, but it was not" }
Roman Elizarov8bff72b2017-12-20 12:55:38 +0300130 }
Vsevolod Tolstopyatov25715162021-07-16 14:38:58 +0300131 lastTestPromise = result
132 return result
Roman Elizarov8bff72b2017-12-20 12:55:38 +0300133 }
134}
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +0300135
136private fun <T> Promise<T>.finally(block: () -> Unit): Promise<T> =
137 then(onFulfilled = { value -> block(); value }, onRejected = { ex -> block(); throw ex })