xref: /aosp_15_r20/external/kotlinx.coroutines/test-utils/jvm/src/TestBase.kt (revision 7a7160fed73afa6648ef8aa100d4a336fe921d9a)

<lambda>null1 package kotlinx.coroutines.testing
2 
3 import kotlinx.coroutines.scheduling.*
4 import java.io.*
5 import java.util.*
6 import kotlin.coroutines.*
7 import kotlinx.coroutines.*
8 import kotlin.test.*
9 
10 actual val VERBOSE = try {
11     System.getProperty("test.verbose")?.toBoolean() ?: false
12 } catch (e: SecurityException) {
13     false
14 }
15 
16 /**
17  * Is `true` when running in a nightly stress test mode.
18  */
19 actual val isStressTest = System.getProperty("stressTest")?.toBoolean() ?: false
20 
21 actual val stressTestMultiplierSqrt = if (isStressTest) 5 else 1
22 
23 private const val SHUTDOWN_TIMEOUT = 1_000L // 1s at most to wait per thread
24 
25 /**
26  * Multiply various constants in stress tests by this factor, so that they run longer during nightly stress test.
27  */
28 actual val stressTestMultiplier = stressTestMultiplierSqrt * stressTestMultiplierSqrt
29 
30 
31 @Suppress("ACTUAL_WITHOUT_EXPECT")
32 actual typealias TestResult = Unit
33 
lastResortReportExceptionnull34 internal actual fun lastResortReportException(error: Throwable) {
35     System.err.println("${error.message}${error.cause?.let { ": $it" } ?: ""}")
36     error.cause?.printStackTrace(System.err)
37     System.err.println("--- Detected at ---")
38     Throwable().printStackTrace(System.err)
39 }
40 
41 /**
42  * Base class for tests, so that tests for predictable scheduling of actions in multiple coroutines sharing a single
43  * thread can be written. Use it like this:
44  *
45  * ```
46  * class MyTest : TestBase() {
47  *    @Test
48  *    fun testSomething() = runBlocking { // run in the context of the main thread
49  *        expect(1) // initiate action counter
50  *        launch { // use the context of the main thread
51  *           expect(3) // the body of this coroutine in going to be executed in the 3rd step
52  *        }
53  *        expect(2) // launch just scheduled coroutine for execution later, so this line is executed second
54  *        yield() // yield main thread to the launched job
55  *        finish(4) // fourth step is the last one. `finish` must be invoked or test fails
56  *    }
57  * }
58  * ```
59  */
60 actual open class TestBase(
61     private var disableOutCheck: Boolean,
62     private val errorCatching: ErrorCatching.Impl = ErrorCatching.Impl()
63 ): OrderedExecutionTestBase(), ErrorCatching by errorCatching {
64 
65     actual constructor(): this(false)
66 
67     // Shutdown sequence
68     private lateinit var threadsBefore: Set<Thread>
69     private val uncaughtExceptions = Collections.synchronizedList(ArrayList<Throwable>())
70     private var originalUncaughtExceptionHandler: Thread.UncaughtExceptionHandler? = null
71     /*
72      * System.out that we redefine in order to catch any debugging/diagnostics
73      * 'println' from main source set.
74      * NB: We do rely on the name 'previousOut' in the FieldWalker in order to skip its
75      * processing
76      */
77     private lateinit var previousOut: PrintStream
78 
79     private object TestOutputStream : PrintStream(object : OutputStream() {
writenull80         override fun write(b: Int) {
81             error("Detected unexpected call to 'println' from source code")
82         }
83     })
84 
printlnnull85     actual fun println(message: Any?) {
86         if (disableOutCheck) kotlin.io.println(message)
87         else previousOut.println(message)
88     }
89 
90     @BeforeTest
beforenull91     fun before() {
92         initPoolsBeforeTest()
93         threadsBefore = currentThreads()
94         originalUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
95         Thread.setDefaultUncaughtExceptionHandler { t, e ->
96             println("Exception in thread $t: $e") // The same message as in default handler
97             e.printStackTrace()
98             uncaughtExceptions.add(e)
99         }
100         if (!disableOutCheck) {
101             previousOut = System.out
102             System.setOut(TestOutputStream)
103         }
104     }
105 
106     @AfterTest
onCompletionnull107     fun onCompletion() {
108         // onCompletion should not throw exceptions before it finishes all cleanup, so that other tests always
109         // start in a clear, restored state
110         checkFinishCall()
111         if (!disableOutCheck) { // Restore global System.out first
112             System.setOut(previousOut)
113         }
114         // Shutdown all thread pools
115         shutdownPoolsAfterTest()
116         // Check that are now leftover threads
117         runCatching {
118             checkTestThreads(threadsBefore)
119         }.onFailure {
120             reportError(it)
121         }
122         // Restore original uncaught exception handler after the main shutdown sequence
123         Thread.setDefaultUncaughtExceptionHandler(originalUncaughtExceptionHandler)
124         if (uncaughtExceptions.isNotEmpty()) {
125             error("Expected no uncaught exceptions, but got $uncaughtExceptions")
126         }
127         // The very last action -- throw error if any was detected
128         errorCatching.close()
129     }
130 
runTestnull131     actual fun runTest(
132         expected: ((Throwable) -> Boolean)?,
133         unhandled: List<(Throwable) -> Boolean>,
134         block: suspend CoroutineScope.() -> Unit
135     ): TestResult {
136         var exCount = 0
137         var ex: Throwable? = null
138         try {
139             runBlocking(block = block, context = CoroutineExceptionHandler { _, e ->
140                 if (e is CancellationException) return@CoroutineExceptionHandler // are ignored
141                 exCount++
142                 when {
143                     exCount > unhandled.size ->
144                         error("Too many unhandled exceptions $exCount, expected ${unhandled.size}, got: $e", e)
145                     !unhandled[exCount - 1](e) ->
146                         error("Unhandled exception was unexpected: $e", e)
147                 }
148             })
149         } catch (e: Throwable) {
150             ex = e
151             if (expected != null) {
152                 if (!expected(e))
153                     error("Unexpected exception: $e", e)
154             } else {
155                 throw e
156             }
157         } finally {
158             if (ex == null && expected != null) error("Exception was expected but none produced")
159         }
160         if (exCount < unhandled.size)
161             error("Too few unhandled exceptions $exCount, expected ${unhandled.size}")
162     }
163 
currentDispatchernull164     protected suspend fun currentDispatcher() = coroutineContext[ContinuationInterceptor]!!
165 }
166 
167 @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
168 fun initPoolsBeforeTest() {
169     DefaultScheduler.usePrivateScheduler()
170 }
171 
172 @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE")
shutdownPoolsAfterTestnull173 fun shutdownPoolsAfterTest() {
174     DefaultScheduler.shutdown(SHUTDOWN_TIMEOUT)
175     DefaultExecutor.shutdownForTests(SHUTDOWN_TIMEOUT)
176     DefaultScheduler.restore()
177 }
178 
179 actual val isNative = false
180 
181 actual val isBoundByJsTestTimeout = false
182 
183 /*
184  * We ignore tests that test **real** non-virtualized tests with time on Windows, because
185  * our CI Windows is virtualized itself (oh, the irony) and its clock resolution is dozens of ms,
186  * which makes such tests flaky.
187  */
188 actual val isJavaAndWindows: Boolean = System.getProperty("os.name")!!.contains("Windows")
189