[Native][tests] Filter test output to suppress TEAMCITY-related messages

This commit is contained in:
Dmitriy Dolovov
2021-12-03 00:06:10 +03:00
parent 09dbd43b97
commit bb01c4cbbb
7 changed files with 67 additions and 45 deletions
@@ -203,11 +203,13 @@ internal abstract class LoggedData {
appendDuration(runResult.duration) appendDuration(runResult.duration)
appendLine() appendLine()
appendLine("========== BEGIN: STDOUT ==========") appendLine("========== BEGIN: STDOUT ==========")
if (runResult.output.stdOut.isNotEmpty()) appendLine(runResult.output.stdOut.trimEnd()) val stdOut = runResult.processOutput.stdOut.filteredOutput
if (stdOut.isNotEmpty()) appendLine(stdOut.trimEnd())
appendLine("========== END: STDOUT ==========") appendLine("========== END: STDOUT ==========")
appendLine() appendLine()
appendLine("========== BEGIN: STDERR ==========") appendLine("========== BEGIN: STDERR ==========")
if (runResult.output.stdErr.isNotEmpty()) appendLine(runResult.output.stdErr.trimEnd()) val stdErr = runResult.processOutput.stdErr
if (stdErr.isNotEmpty()) appendLine(stdErr.trimEnd())
appendLine("========== END: STDERR ==========") appendLine("========== END: STDERR ==========")
return this return this
} }
@@ -7,6 +7,7 @@ package org.jetbrains.kotlin.konan.blackboxtest.support.runner
import org.jetbrains.kotlin.konan.blackboxtest.support.TestExecutable import org.jetbrains.kotlin.konan.blackboxtest.support.TestExecutable
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.AbstractRunner.AbstractRun import org.jetbrains.kotlin.konan.blackboxtest.support.runner.AbstractRunner.AbstractRun
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestOutputFilter
import org.jetbrains.kotlin.konan.blackboxtest.support.util.readOutput import org.jetbrains.kotlin.konan.blackboxtest.support.util.readOutput
import kotlin.time.* import kotlin.time.*
@@ -14,6 +15,7 @@ internal abstract class AbstractLocalProcessRunner<R>(private val executionTimeo
protected abstract val visibleProcessName: String protected abstract val visibleProcessName: String
protected abstract val executable: TestExecutable protected abstract val executable: TestExecutable
protected abstract val programArgs: List<String> protected abstract val programArgs: List<String>
protected abstract val outputFilter: TestOutputFilter
protected open fun customizeProcess(process: Process) = Unit protected open fun customizeProcess(process: Process) = Unit
@@ -33,7 +35,7 @@ internal abstract class AbstractLocalProcessRunner<R>(private val executionTimeo
val (process, hasFinishedOnTime) = result val (process, hasFinishedOnTime) = result
// Don't use blocking read from stdout/stderr on non-finished process. If the process is hanging this would result in hanging test. // Don't use blocking read from stdout/stderr on non-finished process. If the process is hanging this would result in hanging test.
val output = process.readOutput(nonBlocking = !hasFinishedOnTime) val output = process.readOutput(outputFilter, nonBlocking = !hasFinishedOnTime)
if (hasFinishedOnTime) { if (hasFinishedOnTime) {
val exitCode: Int = process.exitValue() val exitCode: Int = process.exitValue()
@@ -8,6 +8,7 @@ package org.jetbrains.kotlin.konan.blackboxtest.support.runner
import org.jetbrains.kotlin.konan.blackboxtest.support.LoggedData import org.jetbrains.kotlin.konan.blackboxtest.support.LoggedData
import org.jetbrains.kotlin.konan.blackboxtest.support.TestExecutable import org.jetbrains.kotlin.konan.blackboxtest.support.TestExecutable
import org.jetbrains.kotlin.konan.blackboxtest.support.TestFunction import org.jetbrains.kotlin.konan.blackboxtest.support.TestFunction
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestOutputFilter
import org.jetbrains.kotlin.konan.blackboxtest.support.util.parseGTestListing import org.jetbrains.kotlin.konan.blackboxtest.support.util.parseGTestListing
import org.jetbrains.kotlin.test.services.JUnit5Assertions import org.jetbrains.kotlin.test.services.JUnit5Assertions
import kotlin.time.Duration import kotlin.time.Duration
@@ -18,6 +19,7 @@ internal class LocalTestFunctionExtractor(
) : AbstractLocalProcessRunner<Collection<TestFunction>>(executionTimeout) { ) : AbstractLocalProcessRunner<Collection<TestFunction>>(executionTimeout) {
override val visibleProcessName get() = "Test function extractor" override val visibleProcessName get() = "Test function extractor"
override val programArgs = listOf(executable.executableFile.path, "--ktest_list_tests") override val programArgs = listOf(executable.executableFile.path, "--ktest_list_tests")
override val outputFilter get() = TestOutputFilter.NO_FILTERING
override fun getLoggedParameters() = LoggedData.TestRunParameters( override fun getLoggedParameters() = LoggedData.TestRunParameters(
compilerCall = executable.loggedCompilerCall, compilerCall = executable.loggedCompilerCall,
@@ -37,6 +39,6 @@ internal class LocalTestFunctionExtractor(
runResult: RunResult.Completed runResult: RunResult.Completed
) : AbstractLocalProcessRunner<Collection<TestFunction>>.ResultHandler(runResult) { ) : AbstractLocalProcessRunner<Collection<TestFunction>>.ResultHandler(runResult) {
override fun getLoggedRun() = LoggedData.TestRun(getLoggedParameters(), runResult) override fun getLoggedRun() = LoggedData.TestRun(getLoggedParameters(), runResult)
override fun doHandle() = parseGTestListing(runResult.output.stdOut) override fun doHandle() = parseGTestListing(runResult.processOutput.stdOut.filteredOutput)
} }
} }
@@ -7,8 +7,10 @@ package org.jetbrains.kotlin.konan.blackboxtest.support.runner
import com.intellij.openapi.util.text.StringUtilRt.convertLineSeparators import com.intellij.openapi.util.text.StringUtilRt.convertLineSeparators
import org.jetbrains.kotlin.konan.blackboxtest.support.* import org.jetbrains.kotlin.konan.blackboxtest.support.*
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TCTestOutputFilter
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestName import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestName
import org.jetbrains.kotlin.konan.blackboxtest.support.util.parseTCTestReport import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestOutputFilter
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestReport
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.junit.jupiter.api.Assumptions.assumeFalse import org.junit.jupiter.api.Assumptions.assumeFalse
import kotlin.time.Duration import kotlin.time.Duration
@@ -25,6 +27,9 @@ internal class LocalTestRunner(
testRun.runParameters.forEach { it.applyTo(this) } testRun.runParameters.forEach { it.applyTo(this) }
} }
override val outputFilter: TestOutputFilter
get() = if (testRun.runParameters.has<TestRunParameter.WithTCTestLogger>()) TCTestOutputFilter else TestOutputFilter.NO_FILTERING
override fun getLoggedParameters() = LoggedData.TestRunParameters( override fun getLoggedParameters() = LoggedData.TestRunParameters(
compilerCall = executable.loggedCompilerCall, compilerCall = executable.loggedCompilerCall,
testCaseId = testRun.testCaseId, testCaseId = testRun.testCaseId,
@@ -50,15 +55,14 @@ internal class LocalTestRunner(
override fun getLoggedRun() = LoggedData.TestRun(getLoggedParameters(), runResult) override fun getLoggedRun() = LoggedData.TestRun(getLoggedParameters(), runResult)
override fun doHandle() { override fun doHandle() {
if (testRun.runParameters.has<TestRunParameter.WithTCTestLogger>()) { verifyTestReport(runResult.processOutput.stdOut.testReport)
verifyTCTestLoggerOutput()
} else { val mergedOutput = runResult.processOutput.stdOut.filteredOutput + runResult.processOutput.stdErr
verifyPlainOutput() verifyNonTestOutput(mergedOutput)
}
} }
private fun verifyTCTestLoggerOutput() { private fun verifyTestReport(testReport: TestReport?) {
val testReport = parseTCTestReport(runResult.output.stdOut) if (testReport == null) return
verifyExpectation(!testReport.isEmpty()) { "No tests have been found." } verifyExpectation(!testReport.isEmpty()) { "No tests have been found." }
@@ -77,22 +81,18 @@ internal class LocalTestRunner(
verifyNoSuchTests(testReport.failedTests, "There are failed tests") verifyNoSuchTests(testReport.failedTests, "There are failed tests")
assumeFalse(testReport.ignoredTests.isNotEmpty() && testReport.passedTests.isEmpty(), "Test case is disabled") assumeFalse(testReport.ignoredTests.isNotEmpty() && testReport.passedTests.isEmpty(), "Test case is disabled")
verifyNonTestOutput { testReport.nonTestOutput + runResult.output.stdErr }
} }
private fun verifyNoSuchTests(tests: Collection<TestName>, subject: String) = verifyExpectation(tests.isEmpty()) { private fun verifyNoSuchTests(tests: Collection<TestName>, subject: String) = verifyExpectation(tests.isEmpty()) {
buildString { buildString {
append(subject).appendLine(":") append(subject).append(':')
tests.forEach { append(" - ").appendLine(it) } tests.forEach { appendLine().append(" - ").append(it) }
} }
} }
private fun verifyPlainOutput() = verifyNonTestOutput { runResult.output.stdOut + runResult.output.stdErr } private fun verifyNonTestOutput(nonTestOutput: String) {
private fun verifyNonTestOutput(nonTestOutput: () -> String) {
testRun.runParameters.get<TestRunParameter.WithExpectedOutputData> { testRun.runParameters.get<TestRunParameter.WithExpectedOutputData> {
verifyExpectation(convertLineSeparators(expectedOutputDataFile.readText()), convertLineSeparators(nonTestOutput())) { verifyExpectation(convertLineSeparators(expectedOutputDataFile.readText()), convertLineSeparators(nonTestOutput)) {
"Tested process output mismatch. See \"TEST STDOUT\" and \"EXPECTED OUTPUT DATA FILE\" below." "Tested process output mismatch. See \"TEST STDOUT\" and \"EXPECTED OUTPUT DATA FILE\" below."
} }
} }
@@ -5,25 +5,26 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.runner package org.jetbrains.kotlin.konan.blackboxtest.support.runner
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestOutputFilter
import kotlin.time.Duration import kotlin.time.Duration
internal sealed interface RunResult { internal sealed interface RunResult {
val exitCode: Int? val exitCode: Int?
val duration: Duration val duration: Duration
val output: ProcessOutput val processOutput: ProcessOutput
data class Completed( data class Completed(
override val exitCode: Int, override val exitCode: Int,
override val duration: Duration, override val duration: Duration,
override val output: ProcessOutput override val processOutput: ProcessOutput
) : RunResult ) : RunResult
data class TimeoutExceeded( data class TimeoutExceeded(
val timeout: Duration, val timeout: Duration,
override val exitCode: Int?, override val exitCode: Int?,
override val duration: Duration, override val duration: Duration,
override val output: ProcessOutput override val processOutput: ProcessOutput
) : RunResult ) : RunResult
} }
internal class ProcessOutput(val stdOut: String, val stdErr: String) internal class ProcessOutput(val stdOut: TestOutputFilter.FilteredOutput, val stdErr: String)
@@ -32,12 +32,12 @@ internal fun InputStream.readBytesNonBlocking(): ByteArray {
return result.toByteArray() return result.toByteArray()
} }
internal fun Process.readOutput(nonBlocking: Boolean): ProcessOutput { internal fun Process.readOutput(outputFilter: TestOutputFilter, nonBlocking: Boolean): ProcessOutput {
val stdOut = if (nonBlocking) inputStream.readBytesNonBlocking() else inputStream.readBytes() val stdOut = if (nonBlocking) inputStream.readBytesNonBlocking() else inputStream.readBytes()
val stdErr = if (nonBlocking) errorStream.readBytesNonBlocking() else errorStream.readBytes() val stdErr = if (nonBlocking) errorStream.readBytesNonBlocking() else errorStream.readBytes()
return ProcessOutput( return ProcessOutput(
stdOut = stdOut.toString(Charsets.UTF_8), stdOut = outputFilter.filter(stdOut.toString(Charsets.UTF_8)),
stdErr = stdErr.toString(Charsets.UTF_8) stdErr = stdErr.toString(Charsets.UTF_8)
) )
} }
@@ -18,14 +18,25 @@ internal typealias TestName = String
internal class TestReport( internal class TestReport(
val passedTests: Collection<TestName>, val passedTests: Collection<TestName>,
val failedTests: Collection<TestName>, val failedTests: Collection<TestName>,
val ignoredTests: Collection<TestName>, val ignoredTests: Collection<TestName>
val nonTestOutput: String
) { ) {
fun isEmpty(): Boolean = passedTests.isEmpty() && failedTests.isEmpty() && ignoredTests.isEmpty() fun isEmpty(): Boolean = passedTests.isEmpty() && failedTests.isEmpty() && ignoredTests.isEmpty()
} }
internal interface TestOutputFilter {
fun filter(testOutput: String): FilteredOutput
class FilteredOutput(val filteredOutput: String, val testReport: TestReport?)
companion object {
val NO_FILTERING = object : TestOutputFilter {
override fun filter(testOutput: String) = FilteredOutput(testOutput, null)
}
}
}
/** /**
* Parses TC test reports like this: * Processed TC test reports like this:
* *
* ##teamcity[testSuiteStarted name='sample.test.SampleTestKt' locationHint='ktest:suite://sample.test.SampleTestKt'] * ##teamcity[testSuiteStarted name='sample.test.SampleTestKt' locationHint='ktest:suite://sample.test.SampleTestKt']
* ##teamcity[testStarted name='one' locationHint='ktest:test://sample.test.SampleTestKt.one'] * ##teamcity[testStarted name='one' locationHint='ktest:test://sample.test.SampleTestKt.one']
@@ -33,24 +44,30 @@ internal class TestReport(
* ##teamcity[testIgnored name='two'] * ##teamcity[testIgnored name='two']
* ##teamcity[testSuiteFinished name='sample.test.SampleTestKt'] * ##teamcity[testSuiteFinished name='sample.test.SampleTestKt']
*/ */
internal fun parseTCTestReport(rawTCTestOutput: String): TestReport { internal object TCTestOutputFilter : TestOutputFilter {
val callback = TCTestMessageParserCallback() override fun filter(testOutput: String): TestOutputFilter.FilteredOutput {
ServiceMessagesParser().parse(rawTCTestOutput, callback) val callback = TCTestMessageParserCallback()
ServiceMessagesParser().parse(testOutput, callback)
callback.finish()
assertTrue(callback.errors.isEmpty()) { assertTrue(callback.errors.isEmpty()) {
buildString { buildString {
appendLine("Failed to parse TC test output.") appendLine("Failed to parse TC test output.")
callback.errors.forEach { error -> callback.errors.forEach { error ->
appendLine()
append("Error: ").appendLine(error)
}
appendLine() appendLine()
append("Error: ").appendLine(error) appendLine("Full test output:")
append(testOutput)
} }
appendLine()
appendLine("Test output:")
append(rawTCTestOutput)
} }
}
return callback.getReport() return TestOutputFilter.FilteredOutput(
filteredOutput = callback.nonTestOutput.toString(),
testReport = TestReport(callback.passedTests, callback.failedTests, callback.ignoredTests)
)
}
} }
private class TCTestMessageParserCallback : ServiceMessageParserCallback { private class TCTestMessageParserCallback : ServiceMessageParserCallback {
@@ -143,11 +160,9 @@ private class TCTestMessageParserCallback : ServiceMessageParserCallback {
errors += error() errors += error()
} }
fun getReport(): TestReport { fun finish() {
// The last test state is "TestStarted" this likely means that the test process terminated during test execution (SIGSEGV, etc). // The last test state is "TestStarted" this likely means that the test process terminated during test execution (SIGSEGV, etc).
state.safeAs<State.TestStarted>()?.let { failedTests += it.testName } state.safeAs<State.TestStarted>()?.let { failedTests += it.testName }
return TestReport(passedTests, failedTests, ignoredTests, nonTestOutput.toString())
} }
} }