@@ -5,6 +5,7 @@ import java.io.*
5
5
import java.util.*
6
6
import kotlin.coroutines.*
7
7
import kotlinx.coroutines.*
8
+ import java.util.concurrent.atomic.AtomicReference
8
9
import kotlin.test.*
9
10
10
11
actual val VERBOSE = try {
@@ -68,23 +69,9 @@ actual open class TestBase(
68
69
private lateinit var threadsBefore: Set <Thread >
69
70
private val uncaughtExceptions = Collections .synchronizedList(ArrayList <Throwable >())
70
71
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 () {
80
- override fun write(b: Int ) {
81
- error("Detected unexpected call to 'println' from source code")
82
- }
83
- })
84
72
85
73
actual fun println (message : Any? ) {
86
- if (disableOutCheck) kotlin.io.println (message)
87
- else previousOut.println (message)
74
+ PrintlnStrategy .actualSystemOut.println (message)
88
75
}
89
76
90
77
@BeforeTest
@@ -97,34 +84,33 @@ actual open class TestBase(
97
84
e.printStackTrace()
98
85
uncaughtExceptions.add(e)
99
86
}
100
- if (! disableOutCheck) {
101
- previousOut = System .out
102
- System .setOut(TestOutputStream )
103
- }
87
+ PrintlnStrategy .configure(disableOutCheck)
104
88
}
105
89
106
90
@AfterTest
107
91
fun onCompletion () {
108
92
// 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)
93
+ // start in a clear, restored state, so we postpone throwing the observed errors.
94
+ fun cleanupStep (block : () -> Unit ) {
95
+ try {
96
+ block()
97
+ } catch (e: Throwable ) {
98
+ reportError(e)
99
+ }
113
100
}
101
+ cleanupStep { checkFinishCall() }
102
+ // Reset the output stream first
103
+ cleanupStep { PrintlnStrategy .reset() }
114
104
// Shutdown all thread pools
115
- shutdownPoolsAfterTest()
105
+ cleanupStep { shutdownPoolsAfterTest() }
116
106
// Check that are now leftover threads
117
- runCatching {
118
- checkTestThreads(threadsBefore)
119
- }.onFailure {
120
- reportError(it)
121
- }
107
+ cleanupStep { checkTestThreads(threadsBefore) }
122
108
// Restore original uncaught exception handler after the main shutdown sequence
123
109
Thread .setDefaultUncaughtExceptionHandler(originalUncaughtExceptionHandler)
124
110
if (uncaughtExceptions.isNotEmpty()) {
125
- error( " Expected no uncaught exceptions, but got $uncaughtExceptions " )
111
+ reportError( IllegalStateException ( " Expected no uncaught exceptions, but got $uncaughtExceptions " ) )
126
112
}
127
- // The very last action -- throw error if any was detected
113
+ // The very last action -- throw all the detected errors
128
114
errorCatching.close()
129
115
}
130
116
@@ -164,6 +150,81 @@ actual open class TestBase(
164
150
protected suspend fun currentDispatcher () = coroutineContext[ContinuationInterceptor ]!!
165
151
}
166
152
153
+ private object PrintlnStrategy {
154
+ /* *
155
+ * Installs a custom [PrintStream] instead of [System.out] to capture all the output and throw an exception if
156
+ * any was detected.
157
+ *
158
+ * Removes the previously set println handler and throws the exceptions detected by it.
159
+ * If [disableOutCheck] is set, this is the only effect.
160
+ */
161
+ fun configure (disableOutCheck : Boolean ) {
162
+ val systemOut = System .out
163
+ if (systemOut is TestOutputStream ) {
164
+ try {
165
+ systemOut.remove()
166
+ } catch (e: AssertionError ) {
167
+ throw AssertionError (" The previous TestOutputStream contained " , e)
168
+ }
169
+ }
170
+ if (! disableOutCheck) {
171
+ // Invariant: at most one indirection level in `TestOutputStream`.
172
+ System .setOut(TestOutputStream (actualSystemOut))
173
+ }
174
+ }
175
+
176
+ /* *
177
+ * Removes the custom [PrintStream] and throws an exception if any output was detected.
178
+ */
179
+ fun reset () {
180
+ (System .out as ? TestOutputStream )?.remove()
181
+ }
182
+
183
+ /* *
184
+ * The [PrintStream] representing the actual stdout, ignoring the replacement [TestOutputStream].
185
+ */
186
+ val actualSystemOut: PrintStream get() = when (val out = System .out ) {
187
+ is TestOutputStream -> out .previousOut
188
+ else -> out
189
+ }
190
+
191
+ private class TestOutputStream (
192
+ /*
193
+ * System.out that we redefine in order to catch any debugging/diagnostics
194
+ * 'println' from main source set.
195
+ * NB: We do rely on the name 'previousOut' in the FieldWalker in order to skip its
196
+ * processing
197
+ */
198
+ val previousOut : PrintStream ,
199
+ private val myOutputStream : MyOutputStream = MyOutputStream (),
200
+ ) : PrintStream(myOutputStream) {
201
+
202
+ fun remove () {
203
+ System .setOut(previousOut)
204
+ if (myOutputStream.firstPrintStacktace.get() != null ) {
205
+ throw AssertionError (
206
+ " Detected a println. The captured output is: <<<${myOutputStream.capturedOutput} >>>" ,
207
+ myOutputStream.firstPrintStacktace.get()
208
+ )
209
+ }
210
+ }
211
+
212
+ private class MyOutputStream (): OutputStream() {
213
+ val capturedOutput = ByteArrayOutputStream ()
214
+
215
+ val firstPrintStacktace = AtomicReference <Throwable ?>(null )
216
+
217
+ override fun write (b : Int ) {
218
+ if (firstPrintStacktace.get() == null ) {
219
+ firstPrintStacktace.compareAndSet(null , IllegalStateException ())
220
+ }
221
+ capturedOutput.write(b)
222
+ }
223
+ }
224
+
225
+ }
226
+ }
227
+
167
228
@Suppress(" INVISIBLE_MEMBER" , " INVISIBLE_REFERENCE" )
168
229
fun initPoolsBeforeTest () {
169
230
DefaultScheduler .usePrivateScheduler()
0 commit comments