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