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