• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2016-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3  */
4 
5 package kotlinx.coroutines.debug
6 
7 import java.util.concurrent.*
8 
9 /**
10  * Run [invocation] in a separate thread with the given timeout in ms, after which the coroutines info is dumped and, if
11  * [cancelOnTimeout] is set, the execution is interrupted.
12  *
13  * Assumes that [DebugProbes] are installed. Does not deinstall them.
14  */
runWithTimeoutDumpingCoroutinesnull15 internal inline fun <T : Any?> runWithTimeoutDumpingCoroutines(
16     methodName: String,
17     testTimeoutMs: Long,
18     cancelOnTimeout: Boolean,
19     initCancellationException: () -> Throwable,
20     crossinline invocation: () -> T
21 ): T {
22     val testStartedLatch = CountDownLatch(1)
23     val testResult = FutureTask {
24         testStartedLatch.countDown()
25         invocation()
26     }
27     /*
28      * We are using hand-rolled thread instead of single thread executor
29      * in order to be able to safely interrupt thread in the end of a test
30      */
31     val testThread = Thread(testResult, "Timeout test thread").apply { isDaemon = true }
32     try {
33         testThread.start()
34         // Await until test is started to take only test execution time into account
35         testStartedLatch.await()
36         return testResult.get(testTimeoutMs, TimeUnit.MILLISECONDS)
37     } catch (e: TimeoutException) {
38         handleTimeout(testThread, methodName, testTimeoutMs, cancelOnTimeout, initCancellationException())
39     } catch (e: ExecutionException) {
40         throw e.cause ?: e
41     }
42 }
43 
handleTimeoutnull44 private fun handleTimeout(testThread: Thread, methodName: String, testTimeoutMs: Long, cancelOnTimeout: Boolean,
45                           cancellationException: Throwable): Nothing {
46     val units =
47         if (testTimeoutMs % 1000 == 0L)
48             "${testTimeoutMs / 1000} seconds"
49         else "$testTimeoutMs milliseconds"
50 
51     System.err.println("\nTest $methodName timed out after $units\n")
52     System.err.flush()
53 
54     DebugProbes.dumpCoroutines()
55     System.out.flush() // Synchronize serr/sout
56 
57     /*
58      * Order is important:
59      * 1) Create exception with a stacktrace of hang test
60      * 2) Cancel all coroutines via debug agent API (changing system state!)
61      * 3) Throw created exception
62      */
63     cancellationException.attachStacktraceFrom(testThread)
64     testThread.interrupt()
65     cancelIfNecessary(cancelOnTimeout)
66     // If timed out test throws an exception, we can't do much except ignoring it
67     throw cancellationException
68 }
69 
cancelIfNecessarynull70 private fun cancelIfNecessary(cancelOnTimeout: Boolean) {
71     if (cancelOnTimeout) {
72         DebugProbes.dumpCoroutinesInfo().forEach {
73             it.job?.cancel()
74         }
75     }
76 }
77 
attachStacktraceFromnull78 private fun Throwable.attachStacktraceFrom(thread: Thread) {
79     val stackTrace = thread.stackTrace
80     this.stackTrace = stackTrace
81 }
82