Roman Elizarov | a7db8ec | 2017-12-21 22:45:12 +0300 | [diff] [blame] | 1 | /* |
Roman Elizarov | 1f74a2d | 2018-06-29 19:19:45 +0300 | [diff] [blame] | 2 | * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. |
Roman Elizarov | a7db8ec | 2017-12-21 22:45:12 +0300 | [diff] [blame] | 3 | */ |
| 4 | |
Roman Elizarov | 0950dfa | 2018-07-13 10:33:25 +0300 | [diff] [blame] | 5 | package kotlinx.coroutines |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 6 | |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 7 | import kotlin.js.* |
| 8 | |
Nikita Koval | 8fa07b5 | 2019-09-27 18:26:03 +0300 | [diff] [blame] | 9 | public actual val isStressTest: Boolean = false |
| 10 | public actual val stressTestMultiplier: Int = 1 |
Steve Elliott | ca095be | 2022-07-25 14:26:10 +0000 | [diff] [blame] | 11 | public actual val stressTestMultiplierSqrt: Int = 1 |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 12 | |
Vsevolod Tolstopyatov | a3429f7 | 2021-07-16 16:02:36 +0300 | [diff] [blame] | 13 | @Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_TYPE_ALIAS_TO_CLASS_WITH_DECLARATION_SITE_VARIANCE") |
| 14 | public actual typealias TestResult = Promise<Unit> |
| 15 | |
Steve Elliott | ca095be | 2022-07-25 14:26:10 +0000 | [diff] [blame] | 16 | public actual val isNative = false |
| 17 | |
Nikita Koval | 8fa07b5 | 2019-09-27 18:26:03 +0300 | [diff] [blame] | 18 | public actual open class TestBase actual constructor() { |
Vsevolod Tolstopyatov | 2571516 | 2021-07-16 14:38:58 +0300 | [diff] [blame] | 19 | public actual val isBoundByJsTestTimeout = true |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 20 | private var actionIndex = 0 |
| 21 | private var finished = false |
| 22 | private var error: Throwable? = null |
Vsevolod Tolstopyatov | 2571516 | 2021-07-16 14:38:58 +0300 | [diff] [blame] | 23 | private var lastTestPromise: Promise<*>? = null |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 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 | */ |
Ilya Gorbunov | f0cd180 | 2018-04-17 19:59:31 +0300 | [diff] [blame] | 29 | @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 30 | public actual fun error(message: Any, cause: Throwable? = null): Nothing { |
Roman Elizarov | aa461cf | 2018-04-11 13:20:29 +0300 | [diff] [blame] | 31 | if (cause != null) console.log(cause) |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 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 | |
Roman Elizarov | 7587eba | 2018-07-25 12:22:46 +0300 | [diff] [blame] | 38 | private fun printError(message: String, cause: Throwable) { |
| 39 | if (error == null) error = cause |
| 40 | println("$message: $cause") |
| 41 | console.log(cause) |
| 42 | } |
| 43 | |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 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 | |
Vsevolod Tolstopyatov | fe820ba | 2019-04-24 17:14:03 +0300 | [diff] [blame] | 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 | |
Vsevolod Tolstopyatov | 732474f | 2018-07-20 11:36:20 +0300 | [diff] [blame] | 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 | |
Ilya Gorbunov | f0cd180 | 2018-04-17 19:59:31 +0300 | [diff] [blame] | 81 | @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 82 | public actual fun runTest( |
| 83 | expected: ((Throwable) -> Boolean)? = null, |
| 84 | unhandled: List<(Throwable) -> Boolean> = emptyList(), |
| 85 | block: suspend CoroutineScope.() -> Unit |
Vsevolod Tolstopyatov | a3429f7 | 2021-07-16 16:02:36 +0300 | [diff] [blame] | 86 | ): TestResult { |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 87 | var exCount = 0 |
| 88 | var ex: Throwable? = null |
Vsevolod Tolstopyatov | 2571516 | 2021-07-16 14:38:58 +0300 | [diff] [blame] | 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 | } |
Vsevolod Tolstopyatov | 8baa736 | 2021-08-02 12:55:35 +0300 | [diff] [blame] | 111 | val result = GlobalScope.promise(block = block, context = CoroutineExceptionHandler { _, e -> |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 112 | if (e is CancellationException) return@CoroutineExceptionHandler // are ignored |
| 113 | exCount++ |
Roman Elizarov | 7587eba | 2018-07-25 12:22:46 +0300 | [diff] [blame] | 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 | } |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 120 | }).catch { e -> |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 121 | ex = e |
| 122 | if (expected != null) { |
| 123 | if (!expected(e)) |
| 124 | error("Unexpected exception", e) |
| 125 | } else |
| 126 | throw e |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 127 | }.finally { |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 128 | if (ex == null && expected != null) error("Exception was expected but none produced") |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 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" } |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 133 | } |
Vsevolod Tolstopyatov | 2571516 | 2021-07-16 14:38:58 +0300 | [diff] [blame] | 134 | lastTestPromise = result |
| 135 | return result |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 136 | } |
| 137 | } |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 138 | |
| 139 | private fun <T> Promise<T>.finally(block: () -> Unit): Promise<T> = |
| 140 | then(onFulfilled = { value -> block(); value }, onRejected = { ex -> block(); throw ex }) |