-
Notifications
You must be signed in to change notification settings - Fork 1.9k
/
Copy pathTestBase.common.kt
301 lines (248 loc) · 10.2 KB
/
TestBase.common.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
@file:Suppress("unused")
package kotlinx.coroutines.testing
import kotlinx.atomicfu.*
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.*
import kotlinx.coroutines.internal.*
import kotlin.coroutines.*
import kotlin.test.*
import kotlin.time.*
import kotlin.time.Duration.Companion.seconds
/**
* The number of milliseconds that is sure not to pass [assertRunsFast].
*/
const val SLOW = 100_000L
/**
* Asserts that a block completed within [timeout].
*/
inline fun <T> assertRunsFast(timeout: Duration, block: () -> T): T {
val result: T
val elapsed = TimeSource.Monotonic.measureTime { result = block() }
assertTrue("Should complete in $timeout, but took $elapsed") { elapsed < timeout }
return result
}
/**
* Asserts that a block completed within two seconds.
*/
inline fun <T> assertRunsFast(block: () -> T): T = assertRunsFast(2.seconds, block)
/**
* Whether the tests should trace their calls to `expect` and `finish` with `println`.
* `false` by default. On the JVM, can be set to `true` by setting the `test.verbose` system property.
*/
expect val VERBOSE: Boolean
interface OrderedExecution {
/** Expect the next action to be [index] in order. */
fun expect(index: Int)
/** Expect this action to be final, with the given [index]. */
fun finish(index: Int)
/** * Asserts that this line is never executed. */
fun expectUnreached()
/**
* Checks that [finish] was called.
*
* By default, it is allowed to not call [finish] if [expect] was not called.
* This is useful for tests that don't check the ordering of events.
* When [allowNotUsingExpect] is set to `false`, it is an error to not call [finish] in any case.
*/
fun checkFinishCall(allowNotUsingExpect: Boolean = true)
class Impl : OrderedExecution {
private val actionIndex = atomic(0)
override fun expect(index: Int) {
val wasIndex = actionIndex.incrementAndGet()
if (VERBOSE) println("expect($index), wasIndex=$wasIndex")
check(index == wasIndex) {
if (wasIndex < 0) "Expecting action index $index but it is actually finished"
else "Expecting action index $index but it is actually $wasIndex"
}
}
override fun finish(index: Int) {
val wasIndex = actionIndex.getAndSet(Int.MIN_VALUE) + 1
if (VERBOSE) println("finish($index), wasIndex=${if (wasIndex < 0) "finished" else wasIndex}")
check(index == wasIndex) {
if (wasIndex < 0) "Finished more than once"
else "Finishing with action index $index but it is actually $wasIndex"
}
}
override fun expectUnreached() {
error("Should not be reached, ${
actionIndex.value.let {
when {
it < 0 -> "already finished"
it == 0 -> "'expect' was not called yet"
else -> "the last executed action was $it"
}
}
}")
}
override fun checkFinishCall(allowNotUsingExpect: Boolean) {
actionIndex.value.let {
assertTrue(
it < 0 || allowNotUsingExpect && it == 0,
"Expected `finish(${actionIndex.value + 1})` to be called, but the test finished"
)
}
}
}
}
interface ErrorCatching {
/**
* Returns `true` if errors were logged in the test.
*/
fun hasError(): Boolean
/**
* Directly reports an error to the test catching facilities.
*/
fun reportError(error: Throwable)
class Impl : ErrorCatching {
private val errors = mutableListOf<Throwable>()
private val lock = SynchronizedObject()
private var closed = false
override fun hasError(): Boolean = synchronized(lock) {
errors.isNotEmpty()
}
override fun reportError(error: Throwable) {
synchronized(lock) {
if (closed) {
lastResortReportException(error)
} else {
errors.add(error)
}
}
}
fun close() {
synchronized(lock) {
if (closed) {
val error = IllegalStateException("ErrorCatching closed more than once")
lastResortReportException(error)
errors.add(error)
}
closed = true
errors.firstOrNull()?.let {
for (error in errors.drop(1))
it.addSuppressed(error)
throw it
}
}
}
}
}
/**
* Reports an error *somehow* so that it doesn't get completely forgotten.
*/
internal expect fun lastResortReportException(error: Throwable)
/**
* Throws [IllegalStateException] when `value` is false, like `check` in stdlib, but also ensures that the
* test will not complete successfully even if this exception is consumed somewhere in the test.
*/
public inline fun ErrorCatching.check(value: Boolean, lazyMessage: () -> Any) {
if (!value) error(lazyMessage())
}
/**
* Throws [IllegalStateException], like `error` in stdlib, but also ensures that the test will not
* complete successfully even if this exception is consumed somewhere in the test.
*/
fun ErrorCatching.error(message: Any, cause: Throwable? = null): Nothing {
throw IllegalStateException(message.toString(), cause).also {
reportError(it)
}
}
/**
* A class inheriting from which allows to check the execution order inside tests.
*
* @see TestBase
*/
open class OrderedExecutionTestBase : OrderedExecution
{
// TODO: move to by-delegation when [reset] is no longer needed.
private var orderedExecutionDelegate = OrderedExecution.Impl()
@AfterTest
fun checkFinished() { orderedExecutionDelegate.checkFinishCall() }
/** Resets counter and finish flag. Workaround for parametrized tests absence in common */
public fun reset() {
orderedExecutionDelegate.checkFinishCall()
orderedExecutionDelegate = OrderedExecution.Impl()
}
override fun expect(index: Int) = orderedExecutionDelegate.expect(index)
override fun finish(index: Int) = orderedExecutionDelegate.finish(index)
override fun expectUnreached() = orderedExecutionDelegate.expectUnreached()
override fun checkFinishCall(allowNotUsingExpect: Boolean) =
orderedExecutionDelegate.checkFinishCall(allowNotUsingExpect)
}
fun <T> T.void() {}
@OptionalExpectation
expect annotation class NoJs()
@OptionalExpectation
expect annotation class NoNative()
@OptionalExpectation
expect annotation class NoWasmJs()
@OptionalExpectation
expect annotation class NoWasmWasi()
expect val isStressTest: Boolean
expect val stressTestMultiplier: Int
expect val stressTestMultiplierSqrt: Int
/**
* The result of a multiplatform asynchronous test.
* Aliases into Unit on K/JVM and K/N, and into Promise on K/JS.
*/
@Suppress("NO_ACTUAL_FOR_EXPECT")
public expect class TestResult
public expect open class TestBase(): OrderedExecutionTestBase, ErrorCatching {
public fun println(message: Any?)
public fun runTest(
expected: ((Throwable) -> Boolean)? = null,
unhandled: List<(Throwable) -> Boolean> = emptyList(),
block: suspend CoroutineScope.() -> Unit
): TestResult
override fun hasError(): Boolean
override fun reportError(error: Throwable)
}
public suspend inline fun hang(onCancellation: () -> Unit) {
try {
suspendCancellableCoroutine<Unit> { }
} finally {
onCancellation()
}
}
suspend inline fun <reified T : Throwable> assertFailsWith(flow: Flow<*>) = assertFailsWith<T> { flow.collect() }
public suspend fun Flow<Int>.sum() = fold(0) { acc, value -> acc + value }
public suspend fun Flow<Long>.longSum() = fold(0L) { acc, value -> acc + value }
// data is added to avoid stacktrace recovery because CopyableThrowable is not accessible from common modules
public class TestException(message: String? = null, private val data: Any? = null) : Throwable(message)
public class TestException1(message: String? = null, private val data: Any? = null) : Throwable(message)
public class TestException2(message: String? = null, private val data: Any? = null) : Throwable(message)
public class TestException3(message: String? = null, private val data: Any? = null) : Throwable(message)
public class TestCancellationException(message: String? = null, private val data: Any? = null) :
CancellationException(message)
public class TestRuntimeException(message: String? = null, private val data: Any? = null) : RuntimeException(message)
public class RecoverableTestException(message: String? = null) : RuntimeException(message)
public class RecoverableTestCancellationException(message: String? = null) : CancellationException(message)
// Erases identity and equality checks for tests
public fun wrapperDispatcher(context: CoroutineContext): CoroutineContext {
val dispatcher = context[ContinuationInterceptor] as CoroutineDispatcher
return object : CoroutineDispatcher() {
override fun isDispatchNeeded(context: CoroutineContext): Boolean =
dispatcher.isDispatchNeeded(context)
override fun dispatch(context: CoroutineContext, block: Runnable) =
dispatcher.dispatch(context, block)
}
}
public suspend fun wrapperDispatcher(): CoroutineContext = wrapperDispatcher(coroutineContext)
class BadClass {
override fun equals(other: Any?): Boolean = error("equals")
override fun hashCode(): Int = error("hashCode")
override fun toString(): String = error("toString")
}
public expect val isJavaAndWindows: Boolean
public expect val isNative: Boolean
/*
* In common tests we emulate parameterized tests
* by iterating over parameters space in the single @Test method.
* This kind of tests is too slow for JS and does not fit into
* the default Mocha timeout, so we're using this flag to bail-out
* and run such tests only on JVM and K/N.
*/
public expect val isBoundByJsTestTimeout: Boolean
/**
* `true` if this platform has the same event loop for `DefaultExecutor` and [Dispatchers.Unconfined]
*/
public expect val usesSharedEventLoop: Boolean