@@ -5,6 +5,7 @@ import java.io.*
55import java.util.*
66import kotlin.coroutines.*
77import kotlinx.coroutines.*
8+ import java.util.concurrent.atomic.AtomicReference
89import kotlin.test.*
910
1011actual val VERBOSE = try {
@@ -68,23 +69,9 @@ actual open class TestBase(
6869 private lateinit var threadsBefore: Set <Thread >
6970 private val uncaughtExceptions = Collections .synchronizedList(ArrayList <Throwable >())
7071 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- })
8472
8573 actual fun println (message : Any? ) {
86- if (disableOutCheck) kotlin.io.println (message)
87- else previousOut.println (message)
74+ PrintlnStrategy .actualSystemOut.println (message)
8875 }
8976
9077 @BeforeTest
@@ -97,34 +84,33 @@ actual open class TestBase(
9784 e.printStackTrace()
9885 uncaughtExceptions.add(e)
9986 }
100- if (! disableOutCheck) {
101- previousOut = System .out
102- System .setOut(TestOutputStream )
103- }
87+ PrintlnStrategy .configure(disableOutCheck)
10488 }
10589
10690 @AfterTest
10791 fun onCompletion () {
10892 // 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+ }
113100 }
101+ cleanupStep { checkFinishCall() }
102+ // Reset the output stream first
103+ cleanupStep { PrintlnStrategy .reset() }
114104 // Shutdown all thread pools
115- shutdownPoolsAfterTest()
105+ cleanupStep { shutdownPoolsAfterTest() }
116106 // Check that are now leftover threads
117- runCatching {
118- checkTestThreads(threadsBefore)
119- }.onFailure {
120- reportError(it)
121- }
107+ cleanupStep { checkTestThreads(threadsBefore) }
122108 // Restore original uncaught exception handler after the main shutdown sequence
123109 Thread .setDefaultUncaughtExceptionHandler(originalUncaughtExceptionHandler)
124110 if (uncaughtExceptions.isNotEmpty()) {
125- error( " Expected no uncaught exceptions, but got $uncaughtExceptions " )
111+ reportError( IllegalStateException ( " Expected no uncaught exceptions, but got $uncaughtExceptions " ) )
126112 }
127- // The very last action -- throw error if any was detected
113+ // The very last action -- throw all the detected errors
128114 errorCatching.close()
129115 }
130116
@@ -164,6 +150,81 @@ actual open class TestBase(
164150 protected suspend fun currentDispatcher () = coroutineContext[ContinuationInterceptor ]!!
165151}
166152
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+
167228@Suppress(" INVISIBLE_MEMBER" , " INVISIBLE_REFERENCE" )
168229fun initPoolsBeforeTest () {
169230 DefaultScheduler .usePrivateScheduler()
0 commit comments