• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3  */
4 
5 package kotlinx.coroutines.test
6 
7 import kotlinx.coroutines.*
8 import kotlinx.coroutines.internal.*
9 import kotlinx.coroutines.flow.*
10 import kotlin.coroutines.*
11 import kotlin.test.*
12 
13 class RunTestTest {
14 
15     /** Tests that [withContext] that sends work to other threads works in [runTest]. */
16     @Test
17     fun testWithContextDispatching() = runTest {
18         var counter = 0
19         withContext(Dispatchers.Default) {
20             counter += 1
21         }
22         assertEquals(counter, 1)
23     }
24 
25     /** Tests that joining [GlobalScope.launch] works in [runTest]. */
26     @Test
27     fun testJoiningForkedJob() = runTest {
28         var counter = 0
29         val job = GlobalScope.launch {
30             counter += 1
31         }
32         job.join()
33         assertEquals(counter, 1)
34     }
35 
36     /** Tests [suspendCoroutine] not failing [runTest]. */
37     @Test
38     fun testSuspendCoroutine() = runTest {
39         val answer = suspendCoroutine<Int> {
40             it.resume(42)
41         }
42         assertEquals(42, answer)
43     }
44 
45     /** Tests that [runTest] attempts to detect it being run inside another [runTest] and failing in such scenarios. */
46     @Test
47     fun testNestedRunTestForbidden() = runTest {
48         assertFailsWith<IllegalStateException> {
49             runTest { }
50         }
51     }
52 
53     /** Tests that even the dispatch timeout of `0` is fine if all the dispatches go through the same scheduler. */
54     @Test
55     fun testRunTestWithZeroTimeoutWithControlledDispatches() = runTest(dispatchTimeoutMs = 0) {
56         // below is some arbitrary concurrent code where all dispatches go through the same scheduler.
57         launch {
58             delay(2000)
59         }
60         val deferred = async {
61             val job = launch(StandardTestDispatcher(testScheduler)) {
62                 launch {
63                     delay(500)
64                 }
65                 delay(1000)
66             }
67             job.join()
68         }
69         deferred.await()
70     }
71 
72     /** Tests that a dispatch timeout of `0` will fail the test if there are some dispatches outside the scheduler. */
73     @Test
74     @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
75     fun testRunTestWithZeroTimeoutWithUncontrolledDispatches() = testResultMap({ fn ->
76         assertFailsWith<UncompletedCoroutinesError> { fn() }
77     }) {
78         runTest(dispatchTimeoutMs = 0) {
79             withContext(Dispatchers.Default) {
80                 delay(10)
81                 3
82             }
83             fail("shouldn't be reached")
84         }
85     }
86 
87     /** Tests that too low of a dispatch timeout causes crashes. */
88     @Test
89     @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
90     fun testRunTestWithSmallTimeout() = testResultMap({ fn ->
91         assertFailsWith<UncompletedCoroutinesError> { fn() }
92     }) {
93         runTest(dispatchTimeoutMs = 100) {
94             withContext(Dispatchers.Default) {
95                 delay(10000)
96                 3
97             }
98             fail("shouldn't be reached")
99         }
100     }
101 
102     /** Tests that, on timeout, the names of the active coroutines are listed,
103      * whereas the names of the completed ones are not. */
104     @Test
105     @NoJs
106     @NoNative
107     fun testListingActiveCoroutinesOnTimeout(): TestResult {
108         val name1 = "GoodUniqueName"
109         val name2 = "BadUniqueName"
110         return testResultMap({
111             try {
112                 it()
113                 fail("unreached")
114             } catch (e: UncompletedCoroutinesError) {
115                 assertTrue((e.message ?: "").contains(name1))
116                 assertFalse((e.message ?: "").contains(name2))
117             }
118         }) {
119             runTest(dispatchTimeoutMs = 10) {
120                 launch(CoroutineName(name1)) {
121                     CompletableDeferred<Unit>().await()
122                 }
123                 launch(CoroutineName(name2)) {
124                 }
125             }
126         }
127     }
128 
129     /** Tests that the [UncompletedCoroutinesError] suppresses an exception with which the coroutine is completing. */
130     @Test
131     fun testFailureWithPendingCoroutine() = testResultMap({
132         try {
133             it()
134             fail("unreached")
135         } catch (e: UncompletedCoroutinesError) {
136             @Suppress("INVISIBLE_MEMBER")
137             val suppressed = unwrap(e).suppressedExceptions
138             assertEquals(1, suppressed.size)
139             assertIs<TestException>(suppressed[0]).also {
140                 assertEquals("A", it.message)
141             }
142         }
143     }) {
144         runTest(dispatchTimeoutMs = 10) {
145             launch {
146                 withContext(NonCancellable) {
147                     awaitCancellation()
148                 }
149             }
150             yield()
151             throw TestException("A")
152         }
153     }
154 
155     /** Tests that real delays can be accounted for with a large enough dispatch timeout. */
156     @Test
157     fun testRunTestWithLargeTimeout() = runTest(dispatchTimeoutMs = 5000) {
158         withContext(Dispatchers.Default) {
159             delay(50)
160         }
161     }
162 
163     /** Tests uncaught exceptions being suppressed by the dispatch timeout error. */
164     @Test
165     @NoNative // TODO: timeout leads to `Cannot execute task because event loop was shut down` on Native
166     fun testRunTestTimingOutAndThrowing() = testResultMap({ fn ->
167         try {
168             fn()
169             fail("unreached")
170         } catch (e: UncompletedCoroutinesError) {
171             @Suppress("INVISIBLE_MEMBER")
172             val suppressed = unwrap(e).suppressedExceptions
173             assertEquals(1, suppressed.size)
174             assertIs<TestException>(suppressed[0]).also {
175                 assertEquals("A", it.message)
176             }
177         }
178     }) {
179         runTest(dispatchTimeoutMs = 1) {
180             coroutineContext[CoroutineExceptionHandler]!!.handleException(coroutineContext, TestException("A"))
181             withContext(Dispatchers.Default) {
182                 delay(10000)
183                 3
184             }
185             fail("shouldn't be reached")
186         }
187     }
188 
189     /** Tests that passing invalid contexts to [runTest] causes it to fail (on JS, without forking). */
190     @Test
191     fun testRunTestWithIllegalContext() {
192         for (ctx in TestScopeTest.invalidContexts) {
193             assertFailsWith<IllegalArgumentException> {
194                 runTest(ctx) { }
195             }
196         }
197     }
198 
199     /** Tests that throwing exceptions in [runTest] fails the test with them. */
200     @Test
201     fun testThrowingInRunTestBody() = testResultMap({
202         assertFailsWith<RuntimeException> { it() }
203     }) {
204         runTest {
205             throw RuntimeException()
206         }
207     }
208 
209     /** Tests that throwing exceptions in pending tasks [runTest] fails the test with them. */
210     @Test
211     fun testThrowingInRunTestPendingTask() = testResultMap({
212         assertFailsWith<RuntimeException> { it() }
213     }) {
214         runTest {
215             launch {
216                 delay(SLOW)
217                 throw RuntimeException()
218             }
219         }
220     }
221 
222     @Test
223     fun reproducer2405() = runTest {
224         val dispatcher = StandardTestDispatcher(testScheduler)
225         var collectedError = false
226         withContext(dispatcher) {
227             flow { emit(1) }
228                 .combine(
229                     flow<String> { throw IllegalArgumentException() }
230                 ) { int, string -> int.toString() + string }
231                 .catch { emit("error") }
232                 .collect {
233                     assertEquals("error", it)
234                     collectedError = true
235                 }
236         }
237         assertTrue(collectedError)
238     }
239 
240     /** Tests that, once the test body has thrown, the child coroutines are cancelled. */
241     @Test
242     fun testChildrenCancellationOnTestBodyFailure(): TestResult {
243         var job: Job? = null
244         return testResultMap({
245             assertFailsWith<AssertionError> { it() }
246             assertTrue(job!!.isCancelled)
247         }) {
248             runTest {
249                 job = launch {
250                     while (true) {
251                         delay(1000)
252                     }
253                 }
254                 throw AssertionError()
255             }
256         }
257     }
258 
259     /** Tests that [runTest] reports [TimeoutCancellationException]. */
260     @Test
261     fun testTimeout() = testResultMap({
262         assertFailsWith<TimeoutCancellationException> { it() }
263     }) {
264         runTest {
265             withTimeout(50) {
266                 launch {
267                     delay(1000)
268                 }
269             }
270         }
271     }
272 
273     /** Checks that [runTest] throws the root cause and not [JobCancellationException] when a child coroutine throws. */
274     @Test
275     fun testRunTestThrowsRootCause() = testResultMap({
276         assertFailsWith<TestException> { it() }
277     }) {
278         runTest {
279             launch {
280                 throw TestException()
281             }
282         }
283     }
284 
285     /** Tests that [runTest] completes its job. */
286     @Test
287     fun testCompletesOwnJob(): TestResult {
288         var handlerCalled = false
289         return testResultMap({
290             it()
291             assertTrue(handlerCalled)
292         }) {
293             runTest {
294                 coroutineContext.job.invokeOnCompletion {
295                     handlerCalled = true
296                 }
297             }
298         }
299     }
300 
301     /** Tests that [runTest] doesn't complete the job that was passed to it as an argument. */
302     @Test
303     fun testDoesNotCompleteGivenJob(): TestResult {
304         var handlerCalled = false
305         val job = Job()
306         job.invokeOnCompletion {
307             handlerCalled = true
308         }
309         return testResultMap({
310             it()
311             assertFalse(handlerCalled)
312             assertEquals(0, job.children.filter { it.isActive }.count())
313         }) {
314             runTest(job) {
315                 assertTrue(coroutineContext.job in job.children)
316             }
317         }
318     }
319 
320     /** Tests that, when the test body fails, the reported exceptions are suppressed. */
321     @Test
322     fun testSuppressedExceptions() = testResultMap({
323         try {
324             it()
325             fail("should not be reached")
326         } catch (e: TestException) {
327             assertEquals("w", e.message)
328             val suppressed = e.suppressedExceptions +
329                 (e.suppressedExceptions.firstOrNull()?.suppressedExceptions ?: emptyList())
330             assertEquals(3, suppressed.size)
331             assertEquals("x", suppressed[0].message)
332             assertEquals("y", suppressed[1].message)
333             assertEquals("z", suppressed[2].message)
334         }
335     }) {
336         runTest {
337             launch(SupervisorJob()) { throw TestException("x") }
338             launch(SupervisorJob()) { throw TestException("y") }
339             launch(SupervisorJob()) { throw TestException("z") }
340             throw TestException("w")
341         }
342     }
343 
344     /** Tests that [TestCoroutineScope.runTest] does not inherit the exception handler and works. */
345     @Test
346     fun testScopeRunTestExceptionHandler(): TestResult {
347         val scope = TestScope()
348         return testResultMap({
349             try {
350                 it()
351                 fail("should not be reached")
352             } catch (e: TestException) {
353                 // expected
354             }
355         }) {
356             scope.runTest {
357                 launch(SupervisorJob()) { throw TestException("x") }
358             }
359         }
360     }
361 
362     /**
363      * Tests that if the main coroutine is completed without a dispatch, [runTest] will not consider this to be
364      * inactivity.
365      *
366      * The test will hang if this is not the case.
367      */
368     @Test
369     fun testCoroutineCompletingWithoutDispatch() = runTest(dispatchTimeoutMs = Long.MAX_VALUE) {
370         launch(Dispatchers.Default) { delay(100) }
371     }
372 }
373