From 5beec6b54392007e953e57fd7dd6b4e73487453c Mon Sep 17 00:00:00 2001 From: Dmitriy Dolovov Date: Thu, 4 May 2023 15:41:36 +0200 Subject: [PATCH] [Native][tests] Avoid failing with IO exceptions when the process under test is intentionally killed --- .../runner/AbstractLocalProcessRunner.kt | 39 +++++++++++++++++-- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/AbstractLocalProcessRunner.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/AbstractLocalProcessRunner.kt index ba7399f2393..592b7a848aa 100644 --- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/AbstractLocalProcessRunner.kt +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/runner/AbstractLocalProcessRunner.kt @@ -14,6 +14,10 @@ import org.jetbrains.kotlin.konan.blackboxtest.support.runner.UnfilteredProcessO import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestOutputFilter import org.junit.jupiter.api.Assertions.fail import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream +import java.util.concurrent.atomic.AtomicBoolean import kotlin.time.* internal abstract class AbstractLocalProcessRunner(protected val checks: TestRunChecks) : AbstractRunner() { @@ -35,11 +39,19 @@ internal abstract class AbstractLocalProcessRunner(protected val checks: Test val process: Process val hasFinishedOnTime: Boolean + // Don't ignore IO errors that happen just after the process is started. + val ignoreIOErrorsInProcessOutput = AtomicBoolean(false) + val duration = measureTime { process = ProcessBuilder(programArgs).directory(executable.executableFile.parentFile).start() customizeProcess(process) - unfilteredOutputReader = launchReader(unfilteredOutput, process) + unfilteredOutputReader = launchReader( + unfilteredOutput, + processStdout = process.inputStream, + processStderr = process.errorStream, + ignoreIOErrorsInProcessOutput + ) hasFinishedOnTime = process.waitFor( executionTimeout.toLong(DurationUnit.MILLISECONDS), @@ -56,6 +68,7 @@ internal abstract class AbstractLocalProcessRunner(protected val checks: Test unfilteredOutputReader.join() // Wait until all streams are drained. exitCode } catch (_: IllegalThreadStateException) { // Still not destroyed. + ignoreIOErrorsInProcessOutput.set(true) // Ignore IO errors caused by the closed streams of the killed process. unfilteredOutputReader.cancel() // Cancel it. No need to read streams, actually. process.destroyForcibly() // kill -9 null @@ -146,9 +159,27 @@ private class UnfilteredProcessOutput { ) companion object { - fun CoroutineScope.launchReader(unfilteredOutput: UnfilteredProcessOutput, process: Process): Job = launch { - launch { process.inputStream.copyTo(unfilteredOutput.stdOut) } - launch { process.errorStream.copyTo(unfilteredOutput.stdErr) } + fun CoroutineScope.launchReader( + unfilteredOutput: UnfilteredProcessOutput, + processStdout: InputStream, + processStderr: InputStream, + ignoreIOErrors: AtomicBoolean + ): Job = launch { + fun InputStream.safeCopyTo(output: OutputStream) { + try { + copyTo(output) + } catch (e: IOException) { + if (ignoreIOErrors.get()) { // Note: Just checking `!process.isAlive` seems to be not reliable in concurrent environment. + // The IO exception might be caused by the closed stream due to process death. Just ignore. + } else { + // The process is still alive. Some I/O error happened, which is better to rethrow. + throw e + } + } + } + + launch { processStdout.safeCopyTo(unfilteredOutput.stdOut) } + launch { processStderr.safeCopyTo(unfilteredOutput.stdErr) } } } }