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.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 <lambda>null21 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 */ <lambda>null30 private val testThread = Thread(testResult, "Timeout test thread").apply { isDaemon = true } 31 evaluatenull32 override fun evaluate() { 33 try { 34 testThread.start() 35 // Await until test is started to take only test execution time into account 36 testStartedLatch.await() 37 testResult.get(testTimeoutMs, TimeUnit.MILLISECONDS) 38 return 39 } catch (e: TimeoutException) { 40 handleTimeout(testDescription) 41 } catch (e: ExecutionException) { 42 throw e.cause ?: e 43 } finally { 44 DebugProbes.uninstall() 45 } 46 } 47 handleTimeoutnull48 private fun handleTimeout(description: Description) { 49 val units = 50 if (testTimeoutMs % 1000 == 0L) 51 "${testTimeoutMs / 1000} seconds" 52 else "$testTimeoutMs milliseconds" 53 54 System.err.println("\nTest ${description.methodName} timed out after $units\n") 55 System.err.flush() 56 57 DebugProbes.dumpCoroutines() 58 System.out.flush() // Synchronize serr/sout 59 60 /* 61 * Order is important: 62 * 1) Create exception with a stacktrace of hang test 63 * 2) Cancel all coroutines via debug agent API (changing system state!) 64 * 3) Throw created exception 65 */ 66 val exception = createTimeoutException(testThread) 67 cancelIfNecessary() 68 // If timed out test throws an exception, we can't do much except ignoring it 69 throw exception 70 } 71 cancelIfNecessarynull72 private fun cancelIfNecessary() { 73 if (cancelOnTimeout) { 74 DebugProbes.dumpCoroutinesInfo().forEach { 75 it.job?.cancel() 76 } 77 } 78 } 79 createTimeoutExceptionnull80 private fun createTimeoutException(thread: Thread): Exception { 81 val stackTrace = thread.stackTrace 82 val exception = TestTimedOutException(testTimeoutMs, TimeUnit.MILLISECONDS) 83 exception.stackTrace = stackTrace 84 thread.interrupt() 85 return exception 86 } 87 } 88