blob: 3cc9b206f7a97ddc3db10ed1d2536cdbaf166c9a [file] [log] [blame]
Roman Elizarova7db8ec2017-12-21 22:45:12 +03001/*
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 Elizarov8bff72b2017-12-20 12:55:38 +030017package kotlinx.coroutines.experimental
18
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +030019import kotlin.js.*
20
Roman Elizarov8bff72b2017-12-20 12:55:38 +030021public 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 Gorbunovf0cd1802018-04-17 19:59:31 +030033 @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
Roman Elizarov8bff72b2017-12-20 12:55:38 +030034 public actual fun error(message: Any, cause: Throwable? = null): Nothing {
Roman Elizarovaa461cf2018-04-11 13:20:29 +030035 if (cause != null) console.log(cause)
Roman Elizarov8bff72b2017-12-20 12:55:38 +030036 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 Elizarov4b9ae982018-01-25 11:51:25 +030066 // todo: The dynamic (promise) result is a work-around for missing suspend tests, see KT-22228
Ilya Gorbunovf0cd1802018-04-17 19:59:31 +030067 @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
Roman Elizarov8bff72b2017-12-20 12:55:38 +030068 public actual fun runTest(
69 expected: ((Throwable) -> Boolean)? = null,
70 unhandled: List<(Throwable) -> Boolean> = emptyList(),
71 block: suspend CoroutineScope.() -> Unit
Roman Elizarov4b9ae982018-01-25 11:51:25 +030072 ): dynamic {
Roman Elizarov8bff72b2017-12-20 12:55:38 +030073 var exCount = 0
74 var ex: Throwable? = null
Roman Elizarovf066fe92018-01-25 11:38:43 +030075 return promise(block = block, context = CoroutineExceptionHandler { context, e ->
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +030076 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 Elizarov8bff72b2017-12-20 12:55:38 +030084 ex = e
85 if (expected != null) {
86 if (!expected(e))
87 error("Unexpected exception", e)
88 } else
89 throw e
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +030090 }.finally {
Roman Elizarov8bff72b2017-12-20 12:55:38 +030091 if (ex == null && expected != null) error("Exception was expected but none produced")
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +030092 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 Elizarov8bff72b2017-12-20 12:55:38 +030096 }
Roman Elizarov8bff72b2017-12-20 12:55:38 +030097 }
98}
Roman Elizarov1bb8c1c2018-01-11 10:39:46 +030099
100private fun <T> Promise<T>.finally(block: () -> Unit): Promise<T> =
101 then(onFulfilled = { value -> block(); value }, onRejected = { ex -> block(); throw ex })