|
| 1 | +/* |
| 2 | + * Copyright 2016-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. |
| 3 | + */ |
| 4 | + |
| 5 | +package kotlinx.coroutines.debug.junit4 |
| 6 | + |
| 7 | +import kotlinx.coroutines.debug.* |
| 8 | +import org.junit.runner.* |
| 9 | +import org.junit.runners.model.* |
| 10 | +import java.util.concurrent.* |
| 11 | + |
| 12 | +internal class CoroutinesTimeoutStatement( |
| 13 | + testStatement: Statement, |
| 14 | + private val testDescription: Description, |
| 15 | + private val testTimeoutMs: Long, |
| 16 | + private val cancelOnTimeout: Boolean = false |
| 17 | +) : Statement() { |
| 18 | + |
| 19 | + private val testStartedLatch = CountDownLatch(1) |
| 20 | + |
| 21 | + private val testResult = FutureTask<Unit> { |
| 22 | + testStartedLatch.countDown() |
| 23 | + testStatement.evaluate() |
| 24 | + } |
| 25 | + |
| 26 | + /* |
| 27 | + * We are using hand-rolled thread instead of single thread executor |
| 28 | + * in order to be able to safely interrupt thread in the end of a test |
| 29 | + */ |
| 30 | + private val testThread = Thread(testResult, "Timeout test thread").apply { isDaemon = true } |
| 31 | + |
| 32 | + override fun evaluate() { |
| 33 | + DebugProbes.install() |
| 34 | + testThread.start() |
| 35 | + // Await until test is started to take only test execution time into account |
| 36 | + testStartedLatch.await() |
| 37 | + try { |
| 38 | + testResult.get(testTimeoutMs, TimeUnit.MILLISECONDS) |
| 39 | + return |
| 40 | + } catch (e: TimeoutException) { |
| 41 | + handleTimeout(testDescription) |
| 42 | + } catch (e: ExecutionException) { |
| 43 | + throw e.cause ?: e |
| 44 | + } finally { |
| 45 | + DebugProbes.uninstall() |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + private fun handleTimeout(description: Description) { |
| 50 | + val units = |
| 51 | + if (testTimeoutMs % 1000 == 0L) |
| 52 | + "${testTimeoutMs / 1000} seconds" |
| 53 | + else "$testTimeoutMs milliseconds" |
| 54 | + |
| 55 | + val message = "Test ${description.methodName} timed out after $units" |
| 56 | + System.err.println("\n$message\n") |
| 57 | + System.err.flush() |
| 58 | + |
| 59 | + DebugProbes.dumpCoroutines() |
| 60 | + System.out.flush() // Synchronize serr/sout |
| 61 | + |
| 62 | + /* |
| 63 | + * Order is important: |
| 64 | + * 1) Create exception with a stacktrace of hang test |
| 65 | + * 2) Cancel all coroutines via debug agent API (changing system state!) |
| 66 | + * 3) Throw created exception |
| 67 | + */ |
| 68 | + val exception = createTimeoutException(message, testThread) |
| 69 | + cancelIfNecessary() |
| 70 | + // If timed out test throws an exception, we can't do much except ignoring it |
| 71 | + throw exception |
| 72 | + } |
| 73 | + |
| 74 | + private fun cancelIfNecessary() { |
| 75 | + if (cancelOnTimeout) { |
| 76 | + DebugProbes.dumpCoroutinesState().forEach { |
| 77 | + it.jobOrNull?.cancel() |
| 78 | + } |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + private fun createTimeoutException(message: String, thread: Thread): Exception { |
| 83 | + val stackTrace = thread.stackTrace |
| 84 | + val exception = TestTimedOutException(testTimeoutMs, TimeUnit.MILLISECONDS) |
| 85 | + exception.stackTrace = stackTrace |
| 86 | + thread.interrupt() |
| 87 | + return exception |
| 88 | + } |
| 89 | +} |
0 commit comments