Roman Elizarov | a7db8ec | 2017-12-21 22:45:12 +0300 | [diff] [blame] | 1 | /* |
| 2 | * Copyright 2016-2017 JetBrains s.r.o. |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 17 | package kotlinx.coroutines.experimental |
| 18 | |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 19 | import kotlin.js.* |
| 20 | |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 21 | public actual open class TestBase actual constructor() { |
| 22 | public actual val isStressTest: Boolean = false |
| 23 | public actual val stressTestMultiplier: Int = 1 |
| 24 | |
| 25 | private var actionIndex = 0 |
| 26 | private var finished = false |
| 27 | private var error: Throwable? = null |
| 28 | |
| 29 | /** |
| 30 | * Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not |
| 31 | * complete successfully even if this exception is consumed somewhere in the test. |
| 32 | */ |
Ilya Gorbunov | f0cd180 | 2018-04-17 19:59:31 +0300 | [diff] [blame] | 33 | @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 34 | public actual fun error(message: Any, cause: Throwable? = null): Nothing { |
Roman Elizarov | aa461cf | 2018-04-11 13:20:29 +0300 | [diff] [blame] | 35 | if (cause != null) console.log(cause) |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 36 | val exception = IllegalStateException( |
| 37 | if (cause == null) message.toString() else "$message; caused by $cause") |
| 38 | if (error == null) error = exception |
| 39 | throw exception |
| 40 | } |
| 41 | |
| 42 | /** |
| 43 | * Asserts that this invocation is `index`-th in the execution sequence (counting from one). |
| 44 | */ |
| 45 | public actual fun expect(index: Int) { |
| 46 | val wasIndex = ++actionIndex |
| 47 | check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" } |
| 48 | } |
| 49 | |
| 50 | /** |
| 51 | * Asserts that this line is never executed. |
| 52 | */ |
| 53 | public actual fun expectUnreached() { |
| 54 | error("Should not be reached") |
| 55 | } |
| 56 | |
| 57 | /** |
| 58 | * Asserts that this it the last action in the test. It must be invoked by any test that used [expect]. |
| 59 | */ |
| 60 | public actual fun finish(index: Int) { |
| 61 | expect(index) |
| 62 | check(!finished) { "Should call 'finish(...)' at most once" } |
| 63 | finished = true |
| 64 | } |
| 65 | |
Roman Elizarov | 4b9ae98 | 2018-01-25 11:51:25 +0300 | [diff] [blame] | 66 | // todo: The dynamic (promise) result is a work-around for missing suspend tests, see KT-22228 |
Ilya Gorbunov | f0cd180 | 2018-04-17 19:59:31 +0300 | [diff] [blame] | 67 | @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS") |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 68 | public actual fun runTest( |
| 69 | expected: ((Throwable) -> Boolean)? = null, |
| 70 | unhandled: List<(Throwable) -> Boolean> = emptyList(), |
| 71 | block: suspend CoroutineScope.() -> Unit |
Roman Elizarov | 4b9ae98 | 2018-01-25 11:51:25 +0300 | [diff] [blame] | 72 | ): dynamic { |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 73 | var exCount = 0 |
| 74 | var ex: Throwable? = null |
Roman Elizarov | f066fe9 | 2018-01-25 11:38:43 +0300 | [diff] [blame] | 75 | return promise(block = block, context = CoroutineExceptionHandler { context, e -> |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 76 | if (e is CancellationException) return@CoroutineExceptionHandler // are ignored |
| 77 | exCount++ |
| 78 | if (exCount > unhandled.size) |
| 79 | error("Too many unhandled exceptions $exCount, expected ${unhandled.size}", e) |
| 80 | if (!unhandled[exCount - 1](e)) |
| 81 | error("Unhandled exception was unexpected", e) |
| 82 | context[Job]?.cancel(e) |
| 83 | }).catch { e -> |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 84 | ex = e |
| 85 | if (expected != null) { |
| 86 | if (!expected(e)) |
| 87 | error("Unexpected exception", e) |
| 88 | } else |
| 89 | throw e |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 90 | }.finally { |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 91 | if (ex == null && expected != null) error("Exception was expected but none produced") |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 92 | if (exCount < unhandled.size) |
| 93 | error("Too few unhandled exceptions $exCount, expected ${unhandled.size}") |
| 94 | error?.let { throw it } |
| 95 | 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] | 96 | } |
Roman Elizarov | 8bff72b | 2017-12-20 12:55:38 +0300 | [diff] [blame] | 97 | } |
| 98 | } |
Roman Elizarov | 1bb8c1c | 2018-01-11 10:39:46 +0300 | [diff] [blame] | 99 | |
| 100 | private fun <T> Promise<T>.finally(block: () -> Unit): Promise<T> = |
| 101 | then(onFulfilled = { value -> block(); value }, onRejected = { ex -> block(); throw ex }) |