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