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