Skip to content

Commit 17d329f

Browse files
committed
API changes from review. Cleaning up docs.
1 parent 23d228b commit 17d329f

File tree

6 files changed

+47
-90
lines changed

6 files changed

+47
-90
lines changed

core/kotlinx-coroutines-core/src/test_/TestBuilders.kt

+23-32
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@ import kotlin.coroutines.CoroutineContext
4444
*
4545
* @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
4646
* (including coroutines suspended on await).
47-
* @throws UnhandledExceptionsError If an uncaught exception is not handled by [testBody]
47+
* @throws Throwable If an uncaught exception was captured by this test it will be rethrown.
4848
*
49-
* @param dispatcher An optional dispatcher, during [testBody] execution [TestCoroutineDispatcher.dispatchImmediately] will be set to false
49+
* @param context An optional dispatcher, during [testBody] execution [DelayController.dispatchImmediately] will be set to false
5050
* @param testBody The code of the unit-test.
5151
*
5252
* @see [runBlockingTest]
@@ -106,10 +106,15 @@ fun TestCoroutineScope.asyncTest(testBody: TestCoroutineScope.() -> Unit) =
106106
*
107107
* ```
108108
*
109-
* [runBlockingTest] will allow tests to finish successfully while started coroutines are unfinished. In addition unhandled
110-
* exceptions inside coroutines will not fail the test.
109+
* This method requires that all coroutines launched inside [testBody] complete, or are cancelled, as part of the test
110+
* conditions.
111+
*
112+
* In unhandled exceptions inside coroutines will not fail the test.
111113
*
112-
* @param dispatcher An optional dispatcher, during [testBody] execution [TestCoroutineDispatcher.dispatchImmediately] will be set to true
114+
* @throws UncompletedCoroutinesError If the [testBody] does not complete (or cancel) all coroutines that it launches
115+
* (including coroutines suspended on await).
116+
*
117+
* @param context An optional context, during [testBody] execution [DelayController.dispatchImmediately] will be set to true
113118
* @param testBody The code of the unit-test.
114119
*
115120
* @see [asyncTest]
@@ -127,58 +132,44 @@ fun runBlockingTest(context: CoroutineContext? = null, testBody: suspend Corouti
127132
scope.testBody()
128133
scope.cleanupTestCoroutines()
129134
}
135+
val job = checkNotNull(safeContext[Job]) { "Job required for asyncTest" }
136+
val activeChildren = job.children.filter { it.isActive }.toList()
137+
if (activeChildren.isNotEmpty()) {
138+
throw UncompletedCoroutinesError("Test finished with active jobs: ${activeChildren}")
139+
}
130140
} finally {
131141
dispatcher.dispatchImmediately = oldDispatch
132142
}
133143
}
134144

135145
/**
136-
* Convenience method for calling runBlocking on an existing [TestCoroutineScope].
137-
*
138-
* [block] will be executed in immediate execution mode, similar to [runBlockingTest].
146+
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineScope].
139147
*/
140-
fun <T> TestCoroutineScope.runBlocking(block: suspend CoroutineScope.() -> T): T {
141-
val oldDispatch = dispatchImmediately
142-
dispatchImmediately = true
143-
try {
144-
return runBlocking(coroutineContext, block)
145-
} finally {
146-
dispatchImmediately = oldDispatch
147-
}
148+
fun TestCoroutineScope.runBlockingTest(block: suspend CoroutineScope.() -> Unit) {
149+
runBlockingTest(coroutineContext, block)
148150
}
149151

150152
/**
151-
* Convenience method for calling runBlocking on an existing [TestCoroutineDispatcher].
153+
* Convenience method for calling [runBlockingTest] on an existing [TestCoroutineDispatcher].
152154
*
153-
* [block] will be executed in immediate execution mode, similar to [runBlockingTest].
154155
*/
155-
fun <T> TestCoroutineDispatcher.runBlocking(block: suspend CoroutineScope.() -> T): T {
156-
val oldDispatch = dispatchImmediately
157-
dispatchImmediately = true
158-
try {
159-
return runBlocking(this, block)
160-
} finally {
161-
dispatchImmediately = oldDispatch
162-
}
156+
fun TestCoroutineDispatcher.runBlockingTest(block: suspend CoroutineScope.() -> Unit) {
157+
runBlockingTest(this, block)
163158
}
164159

165160
private fun CoroutineContext?.checkArguments(): Pair<CoroutineContext, ContinuationInterceptor> {
166161
var safeContext= this ?: TestCoroutineExceptionHandler() + TestCoroutineDispatcher()
167162

168163
val dispatcher = safeContext[ContinuationInterceptor].run {
169164
this?.let {
170-
if (this !is DelayController) {
171-
throw IllegalArgumentException("Dispatcher must implement DelayController")
172-
}
165+
require(this is DelayController) { "Dispatcher must implement DelayController" }
173166
}
174167
this ?: TestCoroutineDispatcher()
175168
}
176169

177170
val exceptionHandler = safeContext[CoroutineExceptionHandler].run {
178171
this?.let {
179-
if (this !is ExceptionCaptor) {
180-
throw IllegalArgumentException("coroutineExceptionHandler must implement ExceptionCaptor")
181-
}
172+
require(this is ExceptionCaptor) { "coroutineExceptionHandler must implement ExceptionCaptor" }
182173
}
183174
this ?: TestCoroutineExceptionHandler()
184175
}

core/kotlinx-coroutines-core/src/test_/TestCoroutineDispatcher.kt

+7-10
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package kotlinx.coroutines.test
33
import kotlinx.coroutines.*
44
import kotlinx.coroutines.internal.ThreadSafeHeap
55
import kotlinx.coroutines.internal.ThreadSafeHeapNode
6-
import java.lang.IllegalStateException
76
import java.util.concurrent.TimeUnit
87
import kotlin.coroutines.CoroutineContext
98

@@ -24,8 +23,8 @@ interface DelayController {
2423
/**
2524
* Moves the Dispatcher's virtual clock forward by a specified amount of time.
2625
*
27-
* The returned delay-time can be larger than the specified delay-time if the code
28-
* under test contains *blocking* Coroutines.
26+
* The amount the clock is progressed may be larger than the requested delayTime if the code under test uses
27+
* blocking coroutines.
2928
*
3029
* @param delayTime The amount of time to move the CoroutineContext's clock forward.
3130
* @param unit The [TimeUnit] in which [delayTime] and the return value is expressed.
@@ -34,20 +33,18 @@ interface DelayController {
3433
fun advanceTimeBy(delayTime: Long, unit: TimeUnit = TimeUnit.MILLISECONDS): Long
3534

3635
/**
37-
* Moves the current virtual clock forward until the next pending delay.
38-
*
39-
* Any pending immediate dispatches will be executed, and the clock will be advanced to the next delayed dispatch.
36+
* Moves the current virtual clock forward just far enough so the next delay will return.
4037
*
4138
* @return the amount of delay-time that this Dispatcher's clock has been forwarded.
4239
*/
4340
fun advanceTimeToNextDelayed(): Long
4441

4542
/**
46-
* Immediately execute all pending tasks and advance the virtual clock-time to the latest delay.
43+
* Immediately execute all pending tasks and advance the virtual clock-time to the last delay.
4744
*
4845
* @return the amount of delay-time that this Dispatcher's clock has been forwarded.
4946
*/
50-
fun runUntilIdle(): Long
47+
fun advanceUntilIdle(): Long
5148

5249
/**
5350
* Run any tasks that are pending at or before the current virtual clock-time.
@@ -129,7 +126,7 @@ class TestCoroutineDispatcher:
129126
field = value
130127
if (value) {
131128
// there may already be tasks from setup code we need to run
132-
runUntilIdle()
129+
advanceUntilIdle()
133130
}
134131
}
135132

@@ -225,7 +222,7 @@ class TestCoroutineDispatcher:
225222
return time - oldTime
226223
}
227224

228-
override fun runUntilIdle(): Long {
225+
override fun advanceUntilIdle(): Long {
229226
val oldTime = time
230227
while(!queue.isEmpty) {
231228
advanceTimeToNextDelayed()

core/kotlinx-coroutines-core/src/test_/TestCoroutineExceptionHandler.kt

+3-31
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,18 @@ interface ExceptionCaptor {
1414
/**
1515
* List of uncaught coroutine exceptions.
1616
*
17-
* Tests must process these exceptions prior to [cleanupTestCoroutines] either by clearing this list with
18-
* [exceptions.clear] or calling [rethrowUncaughtCoroutineException].
17+
* During [cleanupTestCoroutines] the first element of this list will be rethrown if it is not empty.
1918
*/
2019
val exceptions: MutableList<Throwable>
2120

22-
/**
23-
* Rethrow the first uncaught coroutine exception immediately.
24-
*
25-
* This allows tests to use their preferred exception testing techniques.
26-
*
27-
* If a test generates uncaught exceptions, it must call this method, or clear [exceptions] prior to calling
28-
* [cleanupTestCoroutines].
29-
*/
30-
fun rethrowUncaughtCoroutineException(): Nothing
31-
3221
/**
3322
* Call after the test completes.
3423
*
35-
* @throws UnhandledExceptionsError if any exceptions have not been handled via [rethrowUncaughtCoroutineException]
36-
* or ignored via [exceptions.clear]
24+
* @throws Throwable the first uncaught exception, if there are any uncaught exceptions
3725
*/
3826
fun cleanupTestCoroutines()
3927
}
4028

41-
/**
42-
* Thrown when a test completes with uncaught exceptions that have not been handled.
43-
*
44-
* @param message descriptive message
45-
* @param cause the first uncaught exception
46-
*/
47-
class UnhandledExceptionsError(message: String, cause: Throwable): AssertionError(message, cause)
48-
4929
/**
5030
* An exception handler that can be used to capture uncaught exceptions in tests.
5131
*/
@@ -58,16 +38,8 @@ class TestCoroutineExceptionHandler: ExceptionCaptor, CoroutineExceptionHandler
5838

5939
override val exceptions = LinkedList<Throwable>()
6040

61-
override fun rethrowUncaughtCoroutineException(): Nothing {
62-
if(!exceptions.isEmpty()) {
63-
throw exceptions.removeAt(0)
64-
}
65-
throw AssertionError("No exceptions were caught to rethrow")
66-
}
67-
6841
override fun cleanupTestCoroutines() {
6942
val exception = exceptions.firstOrNull() ?: return
70-
throw UnhandledExceptionsError("Unhandled exceptions were not processed by test. " +
71-
"Call rethrowUncaughtCoroutineException() to handle uncaught exceptions.", exception)
43+
throw exception
7244
}
7345
}

core/kotlinx-coroutines-core/test/test/TestAsyncTest.kt

+4-7
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ class TestAsyncTest {
219219
}
220220
}
221221

222-
@Test(expected = AssertionError::class)
222+
@Test(expected = IllegalAccessError::class)
223223
fun testWithTestContextThrowingAnAssertionError() = asyncTest {
224224
val expectedError = IllegalAccessError("hello")
225225

@@ -240,7 +240,6 @@ class TestAsyncTest {
240240
}
241241

242242
runCurrent()
243-
rethrowUncaughtCoroutineException()
244243
}
245244

246245
@Test(expected = IllegalArgumentException::class)
@@ -255,8 +254,6 @@ class TestAsyncTest {
255254

256255
advanceTimeBy(delay)
257256
assertTrue(job.isCancelled)
258-
259-
rethrowUncaughtCoroutineException()
260257
}
261258

262259
@Test
@@ -367,7 +364,7 @@ class TestAsyncTest {
367364
}
368365
}
369366

370-
@Test(expected = UnhandledExceptionsError::class)
367+
@Test(expected = IllegalArgumentException::class)
371368
fun asyncTest_withUnhandledExceptions_failsTest() {
372369
asyncTest {
373370
launch {
@@ -422,7 +419,7 @@ class TestAsyncTest {
422419
}
423420
}
424421

425-
this.runBlocking {
422+
this.runBlockingTest {
426423
// the only way to make the thread switch is to use runBlocking and await()
427424
assertEquals(3, deferred.await())
428425
}
@@ -447,7 +444,7 @@ class TestAsyncTest {
447444
3
448445
}
449446
}
450-
runUntilIdle()
447+
advanceUntilIdle()
451448
assertEquals(3, deferred.getCompleted())
452449
}
453450
}

core/kotlinx-coroutines-core/test/test/TestRunBlockingTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -269,8 +269,8 @@ class TestRunBlockingTest {
269269
job.join()
270270
}
271271

272-
@Test
273-
fun whenACoroutineLeaks_thereIsNoError() = runBlockingTest {
272+
@Test(expected = UncompletedCoroutinesError::class)
273+
fun whenACoroutineLeaks_errorIsThrown() = runBlockingTest {
274274
val uncompleted = CompletableDeferred<Unit>()
275275
launch {
276276
uncompleted.await()

core/kotlinx-coroutines-core/test/test/TestTestBuilders.kt

+8-8
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ class TestTestBuilders {
1313
@Test
1414
fun scopeRunBlocking_passesDispatcher() {
1515
val scope = TestCoroutineScope()
16-
scope.runBlocking {
16+
scope.runBlockingTest {
1717
assertSame(scope.coroutineContext[ContinuationInterceptor], coroutineContext[ContinuationInterceptor])
1818
}
1919
}
2020

2121
@Test
2222
fun dispatcherRunBlocking_passesDispatcher() {
2323
val dispatcher = TestCoroutineDispatcher()
24-
dispatcher.runBlocking {
24+
dispatcher.runBlockingTest {
2525
assertSame(dispatcher, coroutineContext[ContinuationInterceptor])
2626
}
2727
}
@@ -34,7 +34,7 @@ class TestTestBuilders {
3434
3
3535
}
3636

37-
scope.runBlocking {
37+
scope.runBlockingTest {
3838
assertRunsFast {
3939
assertEquals(3, deferred.await())
4040
}
@@ -50,7 +50,7 @@ class TestTestBuilders {
5050
3
5151
}
5252

53-
dispatcher.runBlocking {
53+
dispatcher.runBlockingTest {
5454
assertRunsFast {
5555
assertEquals(3, deferred.await())
5656
}
@@ -60,7 +60,7 @@ class TestTestBuilders {
6060
@Test
6161
fun scopeRunBlocking_disablesImmedateOnExit() {
6262
val scope = TestCoroutineScope()
63-
scope.runBlocking {
63+
scope.runBlockingTest {
6464
assertRunsFast {
6565
delay(SLOW)
6666
}
@@ -109,7 +109,7 @@ class TestTestBuilders {
109109
val scope = TestCoroutineScope()
110110
var calls = 0
111111

112-
val result = scope.runBlocking {
112+
scope.runBlockingTest {
113113
delay(1_000)
114114
calls++
115115
asyncTest {
@@ -118,13 +118,13 @@ class TestTestBuilders {
118118
calls++
119119
}
120120
assertTrue(job.isActive)
121-
runUntilIdle()
121+
advanceUntilIdle()
122122
assertFalse(job.isActive)
123123
calls++
124124
}
125125
++calls
126126
}
127127

128-
assertEquals(4, result)
128+
assertEquals(4, calls)
129129
}
130130
}

0 commit comments

Comments
 (0)