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