• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * 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
6 
7 import kotlinx.coroutines.internal.*
8 import kotlinx.coroutines.scheduling.*
9 import org.junit.*
10 import java.lang.Math.*
11 import java.util.*
12 import java.util.concurrent.atomic.*
13 import kotlin.coroutines.*
14 import kotlin.math.*
15 import kotlin.test.*
16 
17 private val VERBOSE = systemProp("test.verbose", false)
18 
19 /**
20  * Is `true` when running in a nightly stress test mode.
21  */
22 public actual val isStressTest = System.getProperty("stressTest")?.toBoolean() ?: false
23 
24 public val stressTestMultiplierSqrt = if (isStressTest) 5 else 1
25 
26 /**
27  * Multiply various constants in stress tests by this factor, so that they run longer during nightly stress test.
28  */
29 public actual val stressTestMultiplier = stressTestMultiplierSqrt * stressTestMultiplierSqrt
30 
31 public val stressTestMultiplierCbrt = cbrt(stressTestMultiplier.toDouble()).roundToInt()
32 
33 /**
34  * Base class for tests, so that tests for predictable scheduling of actions in multiple coroutines sharing a single
35  * thread can be written. Use it like this:
36  *
37  * ```
38  * class MyTest : TestBase() {
39  *    @Test
40  *    fun testSomething() = runBlocking { // run in the context of the main thread
41  *        expect(1) // initiate action counter
42  *        launch { // use the context of the main thread
43  *           expect(3) // the body of this coroutine in going to be executed in the 3rd step
44  *        }
45  *        expect(2) // launch just scheduled coroutine for execution later, so this line is executed second
46  *        yield() // yield main thread to the launched job
47  *        finish(4) // fourth step is the last one. `finish` must be invoked or test fails
48  *    }
49  * }
50  * ```
51  */
52 public actual open class TestBase actual constructor() {
53     private var actionIndex = AtomicInteger()
54     private var finished = AtomicBoolean()
55     private var error = AtomicReference<Throwable>()
56 
57     // Shutdown sequence
58     private lateinit var threadsBefore: Set<Thread>
59     private val uncaughtExceptions = Collections.synchronizedList(ArrayList<Throwable>())
60     private var originalUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null
61     private val SHUTDOWN_TIMEOUT = 1_000L // 1s at most to wait per thread
62 
63     /**
64      * Throws [IllegalStateException] like `error` in stdlib, but also ensures that the test will not
65      * complete successfully even if this exception is consumed somewhere in the test.
66      */
67     @Suppress("ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
68     public actual fun error(message: Any, cause: Throwable? = null): Nothing {
69         throw makeError(message, cause)
70     }
71 
72     public fun hasError() = error.get() != null
73 
74     private fun makeError(message: Any, cause: Throwable? = null): IllegalStateException =
75         IllegalStateException(message.toString(), cause).also {
76             setError(it)
77         }
78 
79     private fun setError(exception: Throwable) {
80         error.compareAndSet(null, exception)
81     }
82 
83     private fun printError(message: String, cause: Throwable) {
84         setError(cause)
85         println("$message: $cause")
86         cause.printStackTrace(System.out)
87         println("--- Detected at ---")
88         Throwable().printStackTrace(System.out)
89     }
90 
91     /**
92      * Throws [IllegalStateException] when `value` is false like `check` in stdlib, but also ensures that the
93      * test will not complete successfully even if this exception is consumed somewhere in the test.
94      */
95     public inline fun check(value: Boolean, lazyMessage: () -> Any) {
96         if (!value) error(lazyMessage())
97     }
98 
99     /**
100      * Asserts that this invocation is `index`-th in the execution sequence (counting from one).
101      */
102     public actual fun expect(index: Int) {
103         val wasIndex = actionIndex.incrementAndGet()
104         if (VERBOSE) println("expect($index), wasIndex=$wasIndex")
105         check(index == wasIndex) { "Expecting action index $index but it is actually $wasIndex" }
106     }
107 
108     /**
109      * Asserts that this line is never executed.
110      */
111     public actual fun expectUnreached() {
112         error("Should not be reached, current action index is ${actionIndex.get()}")
113     }
114 
115     /**
116      * Asserts that this it the last action in the test. It must be invoked by any test that used [expect].
117      */
118     public actual fun finish(index: Int) {
119         expect(index)
120         check(!finished.getAndSet(true)) { "Should call 'finish(...)' at most once" }
121     }
122 
123     /**
124      * Asserts that [finish] was invoked
125      */
126     public actual fun ensureFinished() {
127         require(finished.get()) { "finish(...) should be caller prior to this check" }
128     }
129 
130     public actual fun reset() {
131         check(actionIndex.get() == 0 || finished.get()) { "Expecting that 'finish(...)' was invoked, but it was not" }
132         actionIndex.set(0)
133         finished.set(false)
134     }
135 
136     @Before
137     fun before() {
138         initPoolsBeforeTest()
139         threadsBefore = currentThreads()
140         originalUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
141         Thread.setDefaultUncaughtExceptionHandler { t, e ->
142             println("Exception in thread $t: $e") // The same message as in default handler
143             e.printStackTrace()
144             uncaughtExceptions.add(e)
145         }
146     }
147 
148     @After
149     fun onCompletion() {
150         // onCompletion should not throw exceptions before it finishes all cleanup, so that other tests always
151         // start in a clear, restored state
152         if (actionIndex.get() != 0 && !finished.get()) {
153             makeError("Expecting that 'finish(${actionIndex.get() + 1})' was invoked, but it was not")
154         }
155         // Shutdown all thread pools
156         shutdownPoolsAfterTest()
157         // Check that that are now leftover threads
158         runCatching {
159             checkTestThreads(threadsBefore)
160         }.onFailure {
161             setError(it)
162         }
163         // Restore original uncaught exception handler
164         Thread.setDefaultUncaughtExceptionHandler(originalUncaughtExceptionHandler)
165         if (uncaughtExceptions.isNotEmpty()) {
166             makeError("Expected no uncaught exceptions, but got $uncaughtExceptions")
167         }
168         // The very last action -- throw error if any was detected
169         error.get()?.let { throw it }
170     }
171 
172     fun initPoolsBeforeTest() {
173         CommonPool.usePrivatePool()
174         DefaultScheduler.usePrivateScheduler()
175     }
176 
177     fun shutdownPoolsAfterTest() {
178         CommonPool.shutdown(SHUTDOWN_TIMEOUT)
179         DefaultScheduler.shutdown(SHUTDOWN_TIMEOUT)
180         DefaultExecutor.shutdown(SHUTDOWN_TIMEOUT)
181         CommonPool.restore()
182         DefaultScheduler.restore()
183     }
184 
185     @Suppress("ACTUAL_WITHOUT_EXPECT", "ACTUAL_FUNCTION_WITH_DEFAULT_ARGUMENTS")
186     public actual fun runTest(
187         expected: ((Throwable) -> Boolean)? = null,
188         unhandled: List<(Throwable) -> Boolean> = emptyList(),
189         block: suspend CoroutineScope.() -> Unit
190     ) {
191         var exCount = 0
192         var ex: Throwable? = null
193         try {
194             runBlocking(block = block, context = CoroutineExceptionHandler { _, e ->
195                 if (e is CancellationException) return@CoroutineExceptionHandler // are ignored
196                 exCount++
197                 when {
198                     exCount > unhandled.size ->
199                         printError("Too many unhandled exceptions $exCount, expected ${unhandled.size}, got: $e", e)
200                     !unhandled[exCount - 1](e) ->
201                         printError("Unhandled exception was unexpected: $e", e)
202                 }
203             })
204         } catch (e: Throwable) {
205             ex = e
206             if (expected != null) {
207                 if (!expected(e))
208                     error("Unexpected exception: $e", e)
209             } else
210                 throw e
211         } finally {
212             if (ex == null && expected != null) error("Exception was expected but none produced")
213         }
214         if (exCount < unhandled.size)
215             error("Too few unhandled exceptions $exCount, expected ${unhandled.size}")
216     }
217 
218     protected inline fun <reified T: Throwable> assertFailsWith(block: () -> Unit): T {
219         val result = runCatching(block)
220         assertTrue(result.exceptionOrNull() is T, "Expected ${T::class}, but had $result")
221         return result.exceptionOrNull()!! as T
222     }
223 
224     protected suspend fun currentDispatcher() = coroutineContext[ContinuationInterceptor]!!
225 }
226