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