|
| 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 | + */ |
| 15 | +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 | + |
| 44 | +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 | + |
| 70 | +private fun cancelIfNecessary(cancelOnTimeout: Boolean) { |
| 71 | + if (cancelOnTimeout) { |
| 72 | + DebugProbes.dumpCoroutinesInfo().forEach { |
| 73 | + it.job?.cancel() |
| 74 | + } |
| 75 | + } |
| 76 | +} |
| 77 | + |
| 78 | +private fun Throwable.attachStacktraceFrom(thread: Thread) { |
| 79 | + val stackTrace = thread.stackTrace |
| 80 | + this.stackTrace = stackTrace |
| 81 | +} |
0 commit comments