aboutsummaryrefslogtreecommitdiffstats
path: root/kotlinx-coroutines-debug/src/junit4/CoroutinesTimeoutStatement.kt
blob: 4baf409de80772f4cca1e9083faee2eca7a3bacb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
/*
 * Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
 */

package kotlinx.coroutines.debug.junit4

import kotlinx.coroutines.debug.*
import org.junit.runner.*
import org.junit.runners.model.*
import java.util.concurrent.*

internal class CoroutinesTimeoutStatement(
    testStatement: Statement,
    private val testDescription: Description,
    private val testTimeoutMs: Long,
    private val cancelOnTimeout: Boolean = false
) : Statement() {

    private val testStartedLatch = CountDownLatch(1)

    private val testResult = FutureTask<Unit> {
        testStartedLatch.countDown()
        testStatement.evaluate()
    }

    /*
     * We are using hand-rolled thread instead of single thread executor
     * in order to be able to safely interrupt thread in the end of a test
     */
    private val testThread =  Thread(testResult, "Timeout test thread").apply { isDaemon = true }

    override fun evaluate() {
        try {
            testThread.start()
            // Await until test is started to take only test execution time into account
            testStartedLatch.await()
            testResult.get(testTimeoutMs, TimeUnit.MILLISECONDS)
            return
        } catch (e: TimeoutException) {
            handleTimeout(testDescription)
        } catch (e: ExecutionException) {
            throw e.cause ?: e
        } finally {
            DebugProbes.uninstall()
        }
    }

    private fun handleTimeout(description: Description) {
        val units =
            if (testTimeoutMs % 1000 == 0L)
                "${testTimeoutMs / 1000} seconds"
            else "$testTimeoutMs milliseconds"

        System.err.println("\nTest ${description.methodName} timed out after $units\n")
        System.err.flush()

        DebugProbes.dumpCoroutines()
        System.out.flush() // Synchronize serr/sout

        /*
         * Order is important:
         * 1) Create exception with a stacktrace of hang test
         * 2) Cancel all coroutines via debug agent API (changing system state!)
         * 3) Throw created exception
         */
        val exception = createTimeoutException(testThread)
        cancelIfNecessary()
        // If timed out test throws an exception, we can't do much except ignoring it
        throw exception
    }

    private fun cancelIfNecessary() {
        if (cancelOnTimeout) {
            DebugProbes.dumpCoroutinesInfo().forEach {
                it.job?.cancel()
            }
        }
    }

    private fun createTimeoutException(thread: Thread): Exception {
        val stackTrace = thread.stackTrace
        val exception = TestTimedOutException(testTimeoutMs, TimeUnit.MILLISECONDS)
        exception.stackTrace = stackTrace
        thread.interrupt()
        return exception
    }
}