• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright 2016-2019 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.channels.*
9 import kotlinx.coroutines.flow.*
10 import kotlin.coroutines.*
11 import kotlin.test.*
12 import kotlin.time.Duration.Companion.milliseconds
13 
14 class TestScopeTest {
15     /** Tests failing to create a [TestScope] with incorrect contexts. */
16     @Test
17     fun testCreateThrowsOnInvalidArguments() {
18         for (ctx in invalidContexts) {
19             assertFailsWith<IllegalArgumentException> {
20                 TestScope(ctx)
21             }
22         }
23     }
24 
25     /** Tests that a newly-created [TestScope] provides the correct scheduler. */
26     @Test
27     fun testCreateProvidesScheduler() {
28         // Creates a new scheduler.
29         run {
30             val scope = TestScope()
31             assertNotNull(scope.coroutineContext[TestCoroutineScheduler])
32         }
33         // Reuses the scheduler that the dispatcher is linked to.
34         run {
35             val dispatcher = StandardTestDispatcher()
36             val scope = TestScope(dispatcher)
37             assertSame(dispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
38         }
39         // Uses the scheduler passed to it.
40         run {
41             val scheduler = TestCoroutineScheduler()
42             val scope = TestScope(scheduler)
43             assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
44             assertSame(scheduler, (scope.coroutineContext[ContinuationInterceptor] as TestDispatcher).scheduler)
45         }
46         // Doesn't touch the passed dispatcher and the scheduler if they match.
47         run {
48             val scheduler = TestCoroutineScheduler()
49             val dispatcher = StandardTestDispatcher(scheduler)
50             val scope = TestScope(scheduler + dispatcher)
51             assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
52             assertSame(dispatcher, scope.coroutineContext[ContinuationInterceptor])
53         }
54     }
55 
56     /** Part of [testCreateProvidesScheduler], disabled for Native */
57     @Test
58     fun testCreateReusesScheduler() {
59         // Reuses the scheduler of `Dispatchers.Main`
60         run {
61             val scheduler = TestCoroutineScheduler()
62             val mainDispatcher = StandardTestDispatcher(scheduler)
63             Dispatchers.setMain(mainDispatcher)
64             try {
65                 val scope = TestScope()
66                 assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
67                 assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
68             } finally {
69                 Dispatchers.resetMain()
70             }
71         }
72         // Does not reuse the scheduler of `Dispatchers.Main` if one is explicitly passed
73         run {
74             val mainDispatcher = StandardTestDispatcher()
75             Dispatchers.setMain(mainDispatcher)
76             try {
77                 val scheduler = TestCoroutineScheduler()
78                 val scope = TestScope(scheduler)
79                 assertSame(scheduler, scope.coroutineContext[TestCoroutineScheduler])
80                 assertNotSame(mainDispatcher.scheduler, scope.coroutineContext[TestCoroutineScheduler])
81                 assertNotSame(mainDispatcher, scope.coroutineContext[ContinuationInterceptor])
82             } finally {
83                 Dispatchers.resetMain()
84             }
85         }
86     }
87 
88     /** Tests that the cleanup procedure throws if there were uncompleted delays by the end. */
89     @Test
90     fun testPresentDelaysThrowing() {
91         val scope = TestScope()
92         var result = false
93         scope.launch {
94             delay(5)
95             result = true
96         }
97         assertFalse(result)
98         scope.asSpecificImplementation().enter()
99         assertFailsWith<UncompletedCoroutinesError> { scope.asSpecificImplementation().legacyLeave() }
100         assertFalse(result)
101     }
102 
103     /** Tests that the cleanup procedure throws if there were active jobs by the end. */
104     @Test
105     fun testActiveJobsThrowing() {
106         val scope = TestScope()
107         var result = false
108         val deferred = CompletableDeferred<String>()
109         scope.launch {
110             deferred.await()
111             result = true
112         }
113         assertFalse(result)
114         scope.asSpecificImplementation().enter()
115         assertFailsWith<UncompletedCoroutinesError> { scope.asSpecificImplementation().legacyLeave() }
116         assertFalse(result)
117     }
118 
119     /** Tests that the cleanup procedure throws even if it detects that the job is already cancelled. */
120     @Test
121     fun testCancelledDelaysThrowing() {
122         val scope = TestScope()
123         var result = false
124         val deferred = CompletableDeferred<String>()
125         val job = scope.launch {
126             deferred.await()
127             result = true
128         }
129         job.cancel()
130         assertFalse(result)
131         scope.asSpecificImplementation().enter()
132         assertFailsWith<UncompletedCoroutinesError> { scope.asSpecificImplementation().legacyLeave() }
133         assertFalse(result)
134     }
135 
136     /** Tests that uncaught exceptions are thrown at the cleanup. */
137     @Test
138     fun testGetsCancelledOnChildFailure(): TestResult {
139         val scope = TestScope()
140         val exception = TestException("test")
141         scope.launch {
142             throw exception
143         }
144         return testResultMap({
145             try {
146                 it()
147                 fail("should not reach")
148             } catch (e: TestException) {
149                 // expected
150             }
151         }) {
152             scope.runTest {
153             }
154         }
155     }
156 
157     /** Tests that, when reporting several exceptions, the first one is thrown, with the rest suppressed. */
158     @Test
159     fun testSuppressedExceptions() {
160         TestScope().apply {
161             asSpecificImplementation().enter()
162             launch(SupervisorJob()) { throw TestException("x") }
163             launch(SupervisorJob()) { throw TestException("y") }
164             launch(SupervisorJob()) { throw TestException("z") }
165             runCurrent()
166             val e = asSpecificImplementation().legacyLeave()
167             assertEquals(3, e.size)
168             assertEquals("x", e[0].message)
169             assertEquals("y", e[1].message)
170             assertEquals("z", e[2].message)
171         }
172     }
173 
174     /** Tests that the background work is being run at all. */
175     @Test
176     fun testBackgroundWorkBeingRun(): TestResult = runTest {
177         var i = 0
178         var j = 0
179         backgroundScope.launch {
180             ++i
181         }
182         backgroundScope.launch {
183             delay(10)
184             ++j
185         }
186         assertEquals(0, i)
187         assertEquals(0, j)
188         delay(1)
189         assertEquals(1, i)
190         assertEquals(0, j)
191         delay(10)
192         assertEquals(1, i)
193         assertEquals(1, j)
194     }
195 
196     /**
197      * Tests that the background work gets cancelled after the test body finishes.
198      */
199     @Test
200     fun testBackgroundWorkCancelled(): TestResult {
201         var cancelled = false
202         return testResultMap({
203             it()
204             assertTrue(cancelled)
205         }) {
206             runTest {
207                 var i = 0
208                 backgroundScope.launch {
209                     try {
210                         while (isActive) {
211                             ++i
212                             yield()
213                         }
214                     } catch (e: CancellationException) {
215                         cancelled = true
216                     }
217                 }
218                 repeat(5) {
219                     assertEquals(i, it)
220                     yield()
221                 }
222             }
223         }
224     }
225 
226     /** Tests the interactions between the time-control commands and the background work. */
227     @Test
228     fun testBackgroundWorkTimeControl(): TestResult = runTest {
229         var i = 0
230         var j = 0
231         backgroundScope.launch {
232             while (true) {
233                 ++i
234                 delay(100)
235             }
236         }
237         backgroundScope.launch {
238             while (true) {
239                 ++j
240                 delay(50)
241             }
242         }
243         advanceUntilIdle() // should do nothing, as only background work is left.
244         assertEquals(0, i)
245         assertEquals(0, j)
246         val job = launch {
247             delay(1)
248             // the background work scheduled for earlier gets executed before the normal work scheduled for later does
249             assertEquals(1, i)
250             assertEquals(1, j)
251         }
252         job.join()
253         advanceTimeBy(199.milliseconds) // should work the same for the background tasks
254         assertEquals(2, i)
255         assertEquals(4, j)
256         advanceUntilIdle() // once again, should do nothing
257         assertEquals(2, i)
258         assertEquals(4, j)
259         runCurrent() // should behave the same way as for the normal work
260         assertEquals(3, i)
261         assertEquals(5, j)
262         launch {
263             delay(1001)
264             assertEquals(13, i)
265             assertEquals(25, j)
266         }
267         advanceUntilIdle() // should execute the normal work, and with that, the background one, too
268     }
269 
270     /**
271      * Tests that an error in a background coroutine does not cancel the test, but is reported at the end.
272      */
273     @Test
274     fun testBackgroundWorkErrorReporting(): TestResult {
275         var testFinished = false
276         val exception = RuntimeException("x")
277         return testResultMap({
278             try {
279                 it()
280                 fail("unreached")
281             } catch (e: Throwable) {
282                 assertSame(e, exception)
283                 assertTrue(testFinished)
284             }
285         }) {
286             runTest {
287                 backgroundScope.launch {
288                     throw exception
289                 }
290                 delay(1000)
291                 testFinished = true
292             }
293         }
294     }
295 
296     /**
297      * Tests that the background work gets to finish what it's doing after the test is completed.
298      */
299     @Test
300     fun testBackgroundWorkFinalizing(): TestResult {
301         var taskEnded = 0
302         val nTasks = 10
303         return testResultMap({
304             try {
305                 it()
306                 fail("unreached")
307             } catch (e: TestException) {
308                 assertEquals(2, e.suppressedExceptions.size)
309                 assertEquals(nTasks, taskEnded)
310             }
311         }) {
312             runTest {
313                 repeat(nTasks) {
314                     backgroundScope.launch {
315                         try {
316                             while (true) {
317                                 delay(1)
318                             }
319                         } finally {
320                             ++taskEnded
321                             if (taskEnded <= 2)
322                                 throw TestException()
323                         }
324                     }
325                 }
326                 delay(100)
327                 throw TestException()
328             }
329         }
330     }
331 
332     /**
333      * Tests using [Flow.stateIn] as a background job.
334      */
335     @Test
336     fun testExampleBackgroundJob1() = runTest {
337         val myFlow = flow {
338             var i = 0
339             while (true) {
340                 emit(++i)
341                 delay(1)
342             }
343         }
344         val stateFlow = myFlow.stateIn(backgroundScope, SharingStarted.Eagerly, 0)
345         var j = 0
346         repeat(100) {
347             assertEquals(j++, stateFlow.value)
348             delay(1)
349         }
350     }
351 
352     /**
353      * A test from the documentation of [TestScope.backgroundScope].
354      */
355     @Test
356     fun testExampleBackgroundJob2() = runTest {
357         val channel = Channel<Int>()
358         backgroundScope.launch {
359             var i = 0
360             while (true) {
361                 channel.send(i++)
362             }
363         }
364         repeat(100) {
365             assertEquals(it, channel.receive())
366         }
367     }
368 
369     /**
370      * Tests that the test will timeout due to idleness even if some background tasks are running.
371      */
372     @Test
373     fun testBackgroundWorkNotPreventingTimeout(): TestResult = testResultMap({
374         try {
375             it()
376             fail("unreached")
377         } catch (_: UncompletedCoroutinesError) {
378 
379         }
380     }) {
381         runTest(timeout = 100.milliseconds) {
382             backgroundScope.launch {
383                 while (true) {
384                     yield()
385                 }
386             }
387             backgroundScope.launch {
388                 while (true) {
389                     delay(1)
390                 }
391             }
392             val deferred = CompletableDeferred<Unit>()
393             deferred.await()
394         }
395 
396     }
397 
398     /**
399      * Tests that the background work will not prevent the test from timing out even in some cases
400      * when the unconfined dispatcher is used.
401      */
402     @Test
403     fun testUnconfinedBackgroundWorkNotPreventingTimeout(): TestResult = testResultMap({
404         try {
405             it()
406             fail("unreached")
407         } catch (_: UncompletedCoroutinesError) {
408 
409         }
410     }) {
411         runTest(UnconfinedTestDispatcher(), timeout = 100.milliseconds) {
412             /**
413              * Having a coroutine like this will still cause the test to hang:
414                  backgroundScope.launch {
415                      while (true) {
416                          yield()
417                      }
418                  }
419              * The reason is that even the initial [advanceUntilIdle] will never return in this case.
420              */
421             backgroundScope.launch {
422                 while (true) {
423                     delay(1)
424                 }
425             }
426             val deferred = CompletableDeferred<Unit>()
427             deferred.await()
428         }
429     }
430 
431     /**
432      * Tests that even the exceptions in the background scope that don't typically get reported and need to be queried
433      * (like failures in [async]) will still surface in some simple scenarios.
434      */
435     @Test
436     fun testAsyncFailureInBackgroundReported() = testResultMap({
437         try {
438             it()
439             fail("unreached")
440         } catch (e: TestException) {
441             assertEquals("z", e.message)
442             assertEquals(setOf("x", "y"), e.suppressedExceptions.map { it.message }.toSet())
443         }
444     }) {
445         runTest {
446             backgroundScope.async {
447                 throw TestException("x")
448             }
449             backgroundScope.produce<Unit> {
450                 throw TestException("y")
451             }
452             delay(1)
453             throw TestException("z")
454         }
455     }
456 
457     /**
458      * Tests that, if an exception reaches the [TestScope] exception reporting mechanism via several
459      * channels, it will only be reported once.
460      */
461     @Test
462     fun testNoDuplicateExceptions() = testResultMap({
463         try {
464             it()
465             fail("unreached")
466         } catch (e: TestException) {
467             assertEquals("y", e.message)
468             assertEquals(listOf("x"), e.suppressedExceptions.map { it.message })
469         }
470     }) {
471         runTest {
472             backgroundScope.launch {
473                 throw TestException("x")
474             }
475             delay(1)
476             throw TestException("y")
477         }
478     }
479 
480     /**
481      * Tests that [TestScope.withTimeout] notifies the programmer about using the virtual time.
482      */
483     @Test
484     fun testTimingOutWithVirtualTimeMessage() = runTest {
485         try {
486             withTimeout(1_000_000) {
487                 Channel<Unit>().receive()
488             }
489         } catch (e: TimeoutCancellationException) {
490             assertContains(e.message!!, "virtual")
491         }
492     }
493 
494     /*
495      * Tests that the [TestScope] exception reporting mechanism will report the exceptions that happen between
496      * different tests.
497      *
498      * This test must be ran manually, because such exceptions still go through the global exception handler
499      * (as there's no guarantee that another test will happen), and the global exception handler will
500      * log the exceptions or, on Native, crash the test suite.
501      */
502     @Test
503     @Ignore
504     fun testReportingStrayUncaughtExceptionsBetweenTests() {
505         val thrown = TestException("x")
506         testResultChain({
507             // register a handler for uncaught exceptions
508             runTest { }
509         }, {
510             GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
511                 throw thrown
512             }
513             runTest {
514                 fail("unreached")
515             }
516         }, {
517             // this `runTest` will not report the exception
518             runTest {
519                 when (val exception = it.exceptionOrNull()) {
520                     is UncaughtExceptionsBeforeTest -> {
521                         assertEquals(1, exception.suppressedExceptions.size)
522                         assertSame(exception.suppressedExceptions[0], thrown)
523                     }
524                     else -> fail("unexpected exception: $exception")
525                 }
526             }
527         })
528     }
529 
530     /**
531      * Tests that the uncaught exceptions that happen during the test are reported.
532      */
533     @Test
534     fun testReportingStrayUncaughtExceptionsDuringTest(): TestResult {
535         val thrown = TestException("x")
536         return testResultChain({ _ ->
537             runTest {
538                 val job = launch(Dispatchers.Default + NonCancellable) {
539                     throw thrown
540                 }
541                 job.join()
542             }
543         }, {
544             runTest {
545                 assertEquals(thrown, it.exceptionOrNull())
546             }
547         })
548     }
549 
550     companion object {
551         internal val invalidContexts = listOf(
552             Dispatchers.Default, // not a [TestDispatcher]
553             CoroutineExceptionHandler { _, _ -> }, // exception handlers can't be overridden
554             StandardTestDispatcher() + TestCoroutineScheduler(), // the dispatcher is not linked to the scheduler
555         )
556     }
557 }
558