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
| internal class TestScopeImpl(context: CoroutineContext) :
AbstractCoroutine<Unit>(context, initParentJob = true, active = true), TestScope {
override val testScheduler get() = context[TestCoroutineScheduler]!!
private var entered = false
private var finished = false
private val uncaughtExceptions = mutableListOf<Throwable>()
private val lock = SynchronizedObject()
**override val backgroundScope: CoroutineScope =
CoroutineScope(coroutineContext + BackgroundWork + ReportingSupervisorJob {
if (it !is CancellationException) reportException(it)
})**
/** Called upon entry to [runTest]. Will throw if called more than once. */
fun enter() {
val exceptions = synchronized(lock) {
if (entered)
throw IllegalStateException("Only a single call to `runTest` can be performed during one test.")
entered = true
check(!finished)
/** the order is important: [reportException] is only guaranteed not to throw if [entered] is `true` but
* [finished] is `false`.
* However, we also want [uncaughtExceptions] to be queried after the callback is registered,
* because the exception collector will be able to report the exceptions that arrived before this test but
* after the previous one, and learning about such exceptions as soon is possible is nice. */
@Suppress("INVISIBLE_REFERENCE", "INVISIBLE_MEMBER")
run { ensurePlatformExceptionHandlerLoaded(ExceptionCollector) }
if (catchNonTestRelatedExceptions) {
ExceptionCollector.addOnExceptionCallback(lock, this::reportException)
}
uncaughtExceptions
}
if (exceptions.isNotEmpty()) {
throw UncaughtExceptionsBeforeTest().apply {
for (e in exceptions)
addSuppressed(e)
}
}
}
/** Called at the end of the test. May only be called once. Returns the list of caught unhandled exceptions. */
fun leave(): List<Throwable> = synchronized(lock) {
check(entered && !finished)
/** After [finished] becomes `true`, it is no longer valid to have [reportException] as the callback. */
ExceptionCollector.removeOnExceptionCallback(lock)
finished = true
uncaughtExceptions
}
/** Called at the end of the test. May only be called once. */
fun legacyLeave(): List<Throwable> {
val exceptions = synchronized(lock) {
check(entered && !finished)
/** After [finished] becomes `true`, it is no longer valid to have [reportException] as the callback. */
ExceptionCollector.removeOnExceptionCallback(lock)
finished = true
uncaughtExceptions
}
val activeJobs = children.filter { it.isActive }.toList() // only non-empty if used with `runBlockingTest`
if (exceptions.isEmpty()) {
if (activeJobs.isNotEmpty())
throw UncompletedCoroutinesError(
"Active jobs found during the tear-down. " +
"Ensure that all coroutines are completed or cancelled by your test. " +
"The active jobs: $activeJobs"
)
if (!testScheduler.isIdle())
throw UncompletedCoroutinesError(
"Unfinished coroutines found during the tear-down. " +
"Ensure that all coroutines are completed or cancelled by your test."
)
}
return exceptions
}
/** Stores an exception to report after [runTest], or rethrows it if not inside [runTest]. */
fun reportException(throwable: Throwable) {
synchronized(lock) {
if (finished) {
throw throwable
} else {
@Suppress("INVISIBLE_MEMBER")
for (existingThrowable in uncaughtExceptions) {
// avoid reporting exceptions that already were reported.
if (unwrap(throwable) == unwrap(existingThrowable))
return
}
uncaughtExceptions.add(throwable)
if (!entered)
throw UncaughtExceptionsBeforeTest().apply { addSuppressed(throwable) }
}
}
}
/** Throws an exception if the coroutine is not completing. */
fun tryGetCompletionCause(): Throwable? = completionCause
override fun toString(): String =
"TestScope[" + (if (finished) "test ended" else if (entered) "test started" else "test not started") + "]"
}
|