[Native][tests] Use TEAMCITY test logger instead of GTEST one
That's because currently K/N tests do not report ignored tests with GTEST logger, but do so with TEAMCITY logger.
This commit is contained in:
@@ -12,6 +12,7 @@ dependencies {
|
|||||||
testImplementation(project(":kotlin-reflect"))
|
testImplementation(project(":kotlin-reflect"))
|
||||||
testImplementation(intellijCore())
|
testImplementation(intellijCore())
|
||||||
testImplementation(commonDependency("commons-lang:commons-lang"))
|
testImplementation(commonDependency("commons-lang:commons-lang"))
|
||||||
|
testImplementation(commonDependency("org.jetbrains.teamcity:serviceMessages"))
|
||||||
testImplementation(project(":kotlin-compiler-runner-unshaded"))
|
testImplementation(project(":kotlin-compiler-runner-unshaded"))
|
||||||
testImplementation(projectTests(":compiler:tests-common"))
|
testImplementation(projectTests(":compiler:tests-common"))
|
||||||
testImplementation(projectTests(":compiler:tests-common-new"))
|
testImplementation(projectTests(":compiler:tests-common-new"))
|
||||||
|
|||||||
+3
-2
@@ -66,9 +66,10 @@ internal sealed interface TestRunParameter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
object WithGTestLogger : TestRunParameter {
|
object WithTCTestLogger : TestRunParameter {
|
||||||
override fun applyTo(programArgs: MutableList<String>) {
|
override fun applyTo(programArgs: MutableList<String>) {
|
||||||
programArgs += "--ktest_logger=GTEST"
|
programArgs += "--ktest_logger=TEAMCITY"
|
||||||
|
programArgs += "--ktest_no_exit_code"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -169,12 +169,12 @@ internal class TestRunProvider(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
TestKind.STANDALONE -> listOfNotNull(
|
TestKind.STANDALONE -> listOfNotNull(
|
||||||
TestRunParameter.WithGTestLogger,
|
TestRunParameter.WithTCTestLogger,
|
||||||
testFunction?.let(TestRunParameter::WithFunctionFilter),
|
testFunction?.let(TestRunParameter::WithFunctionFilter),
|
||||||
expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
|
expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
|
||||||
)
|
)
|
||||||
TestKind.REGULAR -> listOfNotNull(
|
TestKind.REGULAR -> listOfNotNull(
|
||||||
TestRunParameter.WithGTestLogger,
|
TestRunParameter.WithTCTestLogger,
|
||||||
testFunction?.let(TestRunParameter::WithFunctionFilter) ?: TestRunParameter.WithPackageFilter(nominalPackageName),
|
testFunction?.let(TestRunParameter::WithFunctionFilter) ?: TestRunParameter.WithPackageFilter(nominalPackageName),
|
||||||
expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
|
expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
|
||||||
)
|
)
|
||||||
|
|||||||
+7
-5
@@ -9,6 +9,7 @@ import org.jetbrains.kotlin.konan.blackboxtest.support.LoggedData
|
|||||||
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
|
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertEquals
|
||||||
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
|
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
|
||||||
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
|
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
|
||||||
|
import org.opentest4j.TestAbortedException
|
||||||
|
|
||||||
internal abstract class AbstractRunner<R> {
|
internal abstract class AbstractRunner<R> {
|
||||||
protected abstract fun buildRun(): AbstractRun
|
protected abstract fun buildRun(): AbstractRun
|
||||||
@@ -29,11 +30,12 @@ internal abstract class AbstractRunner<R> {
|
|||||||
|
|
||||||
resultHandler.handle()
|
resultHandler.handle()
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
if (t is AssertionError)
|
when (t) {
|
||||||
throw t
|
is AssertionError, is TestAbortedException -> throw t
|
||||||
else {
|
else -> {
|
||||||
// An unexpected failure.
|
// An unexpected failure.
|
||||||
handleUnexpectedFailure(t)
|
handleUnexpectedFailure(t)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-18
@@ -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.parseGTestReport
|
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestName
|
||||||
|
import org.jetbrains.kotlin.konan.blackboxtest.support.util.parseTCTestReport
|
||||||
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
|
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
|
||||||
|
import org.junit.jupiter.api.Assumptions.assumeFalse
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
internal class LocalTestRunner(
|
internal class LocalTestRunner(
|
||||||
@@ -48,37 +50,49 @@ 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.WithGTestLogger>()) {
|
if (testRun.runParameters.has<TestRunParameter.WithTCTestLogger>()) {
|
||||||
verifyTestWithGTestRunner()
|
verifyTCTestLoggerOutput()
|
||||||
} else {
|
} else {
|
||||||
verifyPlainTest()
|
verifyPlainOutput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyTestWithGTestRunner() {
|
private fun verifyTCTestLoggerOutput() {
|
||||||
val testReport = parseGTestReport(runResult.output.stdOut)
|
val testReport = parseTCTestReport(runResult.output.stdOut)
|
||||||
|
|
||||||
verifyExpectation(!testReport.isEmpty()) { "No tests have been executed." }
|
verifyExpectation(!testReport.isEmpty()) { "No tests have been found." }
|
||||||
|
|
||||||
val passedTests = testReport.getPassedTests()
|
|
||||||
verifyExpectation(passedTests.isNotEmpty()) { "No passed tests." }
|
|
||||||
|
|
||||||
testRun.runParameters.get<TestRunParameter.WithFilter> {
|
testRun.runParameters.get<TestRunParameter.WithFilter> {
|
||||||
val excessiveTests = passedTests.filter { testName -> !testMatches(testName) }
|
verifyNoSuchTests(
|
||||||
verifyExpectation(excessiveTests.isEmpty()) { "Excessive tests have been executed: $excessiveTests." }
|
testReport.passedTests.filter { testName -> !testMatches(testName) },
|
||||||
|
"Excessive tests have been executed"
|
||||||
|
)
|
||||||
|
|
||||||
|
verifyNoSuchTests(
|
||||||
|
testReport.ignoredTests.filter { testName -> !testMatches(testName) },
|
||||||
|
"Excessive tests have been ignored"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val failedTests = testReport.getFailedTests().size
|
verifyNoSuchTests(testReport.failedTests, "There are failed tests")
|
||||||
verifyExpectation(0, failedTests) { "There are $failedTests failed tests." }
|
|
||||||
|
|
||||||
verifyOutputData(mergedOutput = testReport.cleanStdOut + runResult.output.stdErr)
|
assumeFalse(testReport.ignoredTests.isNotEmpty() && testReport.passedTests.isEmpty(), "Test case is disabled")
|
||||||
|
|
||||||
|
verifyNonTestOutput { testReport.nonTestOutput + runResult.output.stdErr }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun verifyPlainTest() = verifyOutputData(mergedOutput = runResult.output.stdOut + runResult.output.stdErr)
|
private fun verifyNoSuchTests(tests: Collection<TestName>, subject: String) = verifyExpectation(tests.isEmpty()) {
|
||||||
|
buildString {
|
||||||
|
append(subject).appendLine(":")
|
||||||
|
tests.forEach { append(" - ").appendLine(it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun verifyOutputData(mergedOutput: String) {
|
private fun verifyPlainOutput() = verifyNonTestOutput { runResult.output.stdOut + runResult.output.stdErr }
|
||||||
|
|
||||||
|
private fun verifyNonTestOutput(nonTestOutput: () -> String) {
|
||||||
testRun.runParameters.get<TestRunParameter.WithExpectedOutputData> {
|
testRun.runParameters.get<TestRunParameter.WithExpectedOutputData> {
|
||||||
verifyExpectation(convertLineSeparators(expectedOutputDataFile.readText()), convertLineSeparators(mergedOutput)) {
|
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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
-146
@@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
|
||||||
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.jetbrains.kotlin.konan.blackboxtest.support.util
|
|
||||||
|
|
||||||
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageFQN
|
|
||||||
import org.jetbrains.kotlin.konan.blackboxtest.support.TestFunction
|
|
||||||
import org.jetbrains.kotlin.konan.blackboxtest.support.util.GTestListingParseState.*
|
|
||||||
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
|
|
||||||
|
|
||||||
internal typealias TestName = String
|
|
||||||
internal typealias TestStatus = String
|
|
||||||
|
|
||||||
internal class GTestReport(
|
|
||||||
private val testStatuses: Map<TestStatus, Collection<TestName>>,
|
|
||||||
val cleanStdOut: String
|
|
||||||
) {
|
|
||||||
fun getPassedTests(): Collection<TestName> = testStatuses[STATUS_OK].orEmpty()
|
|
||||||
fun getFailedTests(): Collection<TestName> = (testStatuses - STATUS_OK).flatMap { it.value }
|
|
||||||
|
|
||||||
fun isEmpty(): Boolean = testStatuses.isEmpty()
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val STATUS_OK = "OK"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parses GTest reports like this:
|
|
||||||
*
|
|
||||||
* [==========] Running 2 tests from 1 test cases.
|
|
||||||
* [----------] Global test environment set-up.
|
|
||||||
* [----------] 2 tests from sample.test.SampleTestKt
|
|
||||||
* [ RUN ] sample.test.SampleTestKt.one
|
|
||||||
* [ OK ] sample.test.SampleTestKt.one (0 ms)
|
|
||||||
* [ RUN ] sample.test.SampleTestKt.two
|
|
||||||
* [ OK ] sample.test.SampleTestKt.two (0 ms)
|
|
||||||
* [----------] 2 tests from sample.test.SampleTestKt (0 ms total)
|
|
||||||
*
|
|
||||||
* [----------] Global test environment tear-down
|
|
||||||
* [==========] 2 tests from 1 test cases ran. (0 ms total)
|
|
||||||
* [ PASSED ] 2 tests.
|
|
||||||
*/
|
|
||||||
internal fun parseGTestReport(stdOut: String): GTestReport {
|
|
||||||
val testStatuses = hashMapOf<TestStatus, MutableSet<TestName>>()
|
|
||||||
val cleanStdOut = StringBuilder()
|
|
||||||
|
|
||||||
var expectStatusLine = false
|
|
||||||
stdOut.lineSequence().forEachIndexed { index, line ->
|
|
||||||
when {
|
|
||||||
index == 0 && line.startsWith(STDLIB_TESTS_IGNORED_LINE_PREFIX) -> Unit
|
|
||||||
expectStatusLine -> {
|
|
||||||
val matcher = STATUS_LINE_REGEX.matchEntire(line)
|
|
||||||
if (matcher != null) {
|
|
||||||
// Read the line with test status.
|
|
||||||
val testStatus = matcher.groupValues[1]
|
|
||||||
val testName = matcher.groupValues[2]
|
|
||||||
testStatuses.getOrPut(testStatus) { hashSetOf() } += testName
|
|
||||||
expectStatusLine = false
|
|
||||||
} else {
|
|
||||||
// If current line is not a status line then it could be only the line with the process' output.
|
|
||||||
cleanStdOut.appendLine(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
line.matches(RUN_LINE_REGEX) -> {
|
|
||||||
expectStatusLine = true // Next line contains either test status.
|
|
||||||
}
|
|
||||||
else -> Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return GTestReport(testStatuses, cleanStdOut.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts [TestFunction]s from GTest listing.
|
|
||||||
*
|
|
||||||
* Example:
|
|
||||||
* sample.test.SampleTestKt.
|
|
||||||
* one
|
|
||||||
* two
|
|
||||||
*
|
|
||||||
* yields TestFunction(sample.test, one) and TestFunction(sample.test, two).
|
|
||||||
*/
|
|
||||||
internal fun parseGTestListing(rawGTestListing: String): Collection<TestFunction> = buildList {
|
|
||||||
var state: GTestListingParseState = Begin
|
|
||||||
|
|
||||||
rawGTestListing.lineSequence().forEachIndexed { index, line ->
|
|
||||||
fun parseError(message: String): Nothing = fail {
|
|
||||||
buildString {
|
|
||||||
appendLine("$message at line #$index: \"$line\"")
|
|
||||||
appendLine()
|
|
||||||
appendLine("Full listing:")
|
|
||||||
appendLine(rawGTestListing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
state = when {
|
|
||||||
index == 0 && line.startsWith(STDLIB_TESTS_IGNORED_LINE_PREFIX) -> state
|
|
||||||
line.isBlank() -> when (state) {
|
|
||||||
is NewTest, is End -> End
|
|
||||||
else -> parseError("Unexpected empty line")
|
|
||||||
}
|
|
||||||
line[0].isWhitespace() -> when (val s = state) {
|
|
||||||
is HasPackageName -> {
|
|
||||||
this += TestFunction(s.packageName, line.trim())
|
|
||||||
NewTest(s.packageName)
|
|
||||||
}
|
|
||||||
else -> parseError("Test name encountered before test suite name")
|
|
||||||
}
|
|
||||||
else -> when (state) {
|
|
||||||
is Begin, is NewTest -> {
|
|
||||||
val packageParts = line.trimEnd().removeSuffix(".").split('.')
|
|
||||||
if (packageParts.isEmpty()) parseError("Malformed test suite name")
|
|
||||||
|
|
||||||
// Drop the last part because it is related to class name (or file-class name).
|
|
||||||
// TODO: How to handle nested classes?
|
|
||||||
val packageName = packageParts.dropLast(1).joinToString(".")
|
|
||||||
|
|
||||||
NewTestSuite(packageName)
|
|
||||||
}
|
|
||||||
else -> parseError("Unexpected test suite name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val RUN_LINE_REGEX = Regex("""^\[\s+RUN\s+]\s+.*""")
|
|
||||||
private val STATUS_LINE_REGEX = Regex("""^\[\s+([A-Z]+)\s+]\s+(\S+)\s+.*""")
|
|
||||||
|
|
||||||
// The very first line of stdlib test output may contain seed of Random. Such line should be ignored.
|
|
||||||
private const val STDLIB_TESTS_IGNORED_LINE_PREFIX = "Seed: "
|
|
||||||
|
|
||||||
private sealed interface GTestListingParseState {
|
|
||||||
object Begin : GTestListingParseState
|
|
||||||
object End : GTestListingParseState
|
|
||||||
|
|
||||||
interface HasPackageName : GTestListingParseState {
|
|
||||||
val packageName: PackageFQN
|
|
||||||
}
|
|
||||||
|
|
||||||
class NewTestSuite(override val packageName: PackageFQN) : HasPackageName
|
|
||||||
class NewTest(override val packageName: PackageFQN) : HasPackageName
|
|
||||||
}
|
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.konan.blackboxtest.support.util
|
||||||
|
|
||||||
|
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageFQN
|
||||||
|
import org.jetbrains.kotlin.konan.blackboxtest.support.TestFunction
|
||||||
|
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
|
||||||
|
import org.jetbrains.kotlin.konan.blackboxtest.support.util.GTestListingParseState as State
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts [TestFunction]s from GTest listing.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* sample.test.SampleTestKt.
|
||||||
|
* one
|
||||||
|
* two
|
||||||
|
*
|
||||||
|
* yields TestFunction(sample.test, one) and TestFunction(sample.test, two).
|
||||||
|
*/
|
||||||
|
internal fun parseGTestListing(rawGTestListing: String): Collection<TestFunction> = buildList {
|
||||||
|
var state: State = State.Begin
|
||||||
|
|
||||||
|
rawGTestListing.lineSequence().forEachIndexed { index, line ->
|
||||||
|
fun parseError(message: String): Nothing = fail {
|
||||||
|
buildString {
|
||||||
|
appendLine("$message at line #$index: \"$line\"")
|
||||||
|
appendLine()
|
||||||
|
appendLine("Full listing:")
|
||||||
|
appendLine(rawGTestListing)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state = when {
|
||||||
|
index == 0 && line.startsWith(STDLIB_TESTS_IGNORED_LINE_PREFIX) -> state
|
||||||
|
line.isBlank() -> when (state) {
|
||||||
|
is State.NewTest, is State.End -> State.End
|
||||||
|
else -> parseError("Unexpected empty line")
|
||||||
|
}
|
||||||
|
line[0].isWhitespace() -> when (val s = state) {
|
||||||
|
is State.HasPackageName -> {
|
||||||
|
this += TestFunction(s.packageName, line.trim())
|
||||||
|
State.NewTest(s.packageName)
|
||||||
|
}
|
||||||
|
else -> parseError("Test name encountered before test suite name")
|
||||||
|
}
|
||||||
|
else -> when (state) {
|
||||||
|
is State.Begin, is State.NewTest -> {
|
||||||
|
val packageParts = line.trimEnd().removeSuffix(".").split('.')
|
||||||
|
if (packageParts.isEmpty()) parseError("Malformed test suite name")
|
||||||
|
|
||||||
|
// Drop the last part because it is related to class name (or file-class name).
|
||||||
|
// TODO: How to handle nested classes?
|
||||||
|
val packageName = packageParts.dropLast(1).joinToString(".")
|
||||||
|
|
||||||
|
State.NewTestSuite(packageName)
|
||||||
|
}
|
||||||
|
else -> parseError("Unexpected test suite name")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface GTestListingParseState {
|
||||||
|
object Begin : State
|
||||||
|
object End : State
|
||||||
|
class NewTestSuite(override val packageName: PackageFQN) : State, HasPackageName
|
||||||
|
class NewTest(override val packageName: PackageFQN) : State, HasPackageName
|
||||||
|
|
||||||
|
interface HasPackageName {
|
||||||
|
val packageName: PackageFQN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The very first line of stdlib test output may contain seed of Random. Such line should be ignored.
|
||||||
|
private const val STDLIB_TESTS_IGNORED_LINE_PREFIX = "Seed: "
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2010-2021 JetBrains s.r.o. and Kotlin Programming Language contributors.
|
||||||
|
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.jetbrains.kotlin.konan.blackboxtest.support.util
|
||||||
|
|
||||||
|
import jetbrains.buildServer.messages.serviceMessages.*
|
||||||
|
import org.jetbrains.kotlin.konan.blackboxtest.support.FunctionName
|
||||||
|
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
|
||||||
|
import org.jetbrains.kotlin.utils.addToStdlib.cast
|
||||||
|
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
|
||||||
|
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TCTestReportParseState as State
|
||||||
|
import java.text.ParseException
|
||||||
|
|
||||||
|
internal typealias TestName = String
|
||||||
|
|
||||||
|
internal class TestReport(
|
||||||
|
val passedTests: Collection<TestName>,
|
||||||
|
val failedTests: Collection<TestName>,
|
||||||
|
val ignoredTests: Collection<TestName>,
|
||||||
|
val nonTestOutput: String
|
||||||
|
) {
|
||||||
|
fun isEmpty(): Boolean = passedTests.isEmpty() && failedTests.isEmpty() && ignoredTests.isEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses TC test reports like this:
|
||||||
|
*
|
||||||
|
* ##teamcity[testSuiteStarted name='sample.test.SampleTestKt' locationHint='ktest:suite://sample.test.SampleTestKt']
|
||||||
|
* ##teamcity[testStarted name='one' locationHint='ktest:test://sample.test.SampleTestKt.one']
|
||||||
|
* ##teamcity[testFinished name='one' duration='0']
|
||||||
|
* ##teamcity[testIgnored name='two']
|
||||||
|
* ##teamcity[testSuiteFinished name='sample.test.SampleTestKt']
|
||||||
|
*/
|
||||||
|
internal fun parseTCTestReport(rawTCTestOutput: String): TestReport {
|
||||||
|
val callback = TCTestMessageParserCallback()
|
||||||
|
ServiceMessagesParser().parse(rawTCTestOutput, callback)
|
||||||
|
|
||||||
|
assertTrue(callback.errors.isEmpty()) {
|
||||||
|
buildString {
|
||||||
|
appendLine("Failed to parse TC test output.")
|
||||||
|
callback.errors.forEach { error ->
|
||||||
|
appendLine()
|
||||||
|
append("Error: ").appendLine(error)
|
||||||
|
}
|
||||||
|
appendLine()
|
||||||
|
appendLine("Test output:")
|
||||||
|
append(rawTCTestOutput)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return callback.getReport()
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TCTestMessageParserCallback : ServiceMessageParserCallback {
|
||||||
|
private var afterMessage = false
|
||||||
|
private var state: State = State.Begin
|
||||||
|
|
||||||
|
val passedTests = mutableListOf<TestName>()
|
||||||
|
val failedTests = mutableListOf<TestName>()
|
||||||
|
val ignoredTests = mutableListOf<TestName>()
|
||||||
|
|
||||||
|
val nonTestOutput = StringBuilder()
|
||||||
|
val errors = mutableListOf<String>()
|
||||||
|
|
||||||
|
override fun regularText(text: String) {
|
||||||
|
val actualText = if (afterMessage) {
|
||||||
|
if (text.startsWith("\r\n"))
|
||||||
|
text.removePrefix("\r\n")
|
||||||
|
else
|
||||||
|
text.removePrefix("\n")
|
||||||
|
} else
|
||||||
|
text
|
||||||
|
|
||||||
|
nonTestOutput.append(actualText)
|
||||||
|
afterMessage = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun serviceMessage(message: ServiceMessage) {
|
||||||
|
fun unexpectedMessage(): State {
|
||||||
|
addError {
|
||||||
|
"""
|
||||||
|
Unexpected TC test message: "$message"
|
||||||
|
State: $state
|
||||||
|
""".trimIndent()
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
state = when (message) {
|
||||||
|
is TestSuiteStarted -> when (state) {
|
||||||
|
is State.Begin,
|
||||||
|
is State.TestSuiteFinished -> State.TestSuiteStarted(message.suiteName)
|
||||||
|
else -> unexpectedMessage()
|
||||||
|
}
|
||||||
|
is TestSuiteFinished -> when (state) {
|
||||||
|
is State.TestSuiteStarted,
|
||||||
|
is State.TestIgnored,
|
||||||
|
is State.TestFinished -> State.TestSuiteFinished
|
||||||
|
else -> unexpectedMessage()
|
||||||
|
}
|
||||||
|
is TestIgnored -> when (state) {
|
||||||
|
is State.TestSuiteStarted,
|
||||||
|
is State.TestIgnored,
|
||||||
|
is State.TestFinished -> State.TestIgnored(state.testSuite, message.functionName).also { ignoredTests += it.testName }
|
||||||
|
else -> unexpectedMessage()
|
||||||
|
}
|
||||||
|
is TestStarted -> when (state) {
|
||||||
|
is State.TestSuiteStarted,
|
||||||
|
is State.TestIgnored,
|
||||||
|
is State.TestFinished -> State.TestStarted(state.testSuite, message.testName)
|
||||||
|
else -> unexpectedMessage()
|
||||||
|
}
|
||||||
|
is TestFailed -> when (val s = state) {
|
||||||
|
is State.TestStarted -> {
|
||||||
|
nonTestOutput.append(message.stacktrace)
|
||||||
|
State.TestFailed(s.testSuite, message.testName).also { failedTests += it.testName }
|
||||||
|
}
|
||||||
|
else -> unexpectedMessage()
|
||||||
|
}
|
||||||
|
is TestFinished -> when (state) {
|
||||||
|
is State.TestStarted -> State.TestFinished(state.testSuite, message.testName).also { passedTests += it.testName }
|
||||||
|
is State.TestFailed -> State.TestFinished(state.testSuite, message.testName)
|
||||||
|
else -> unexpectedMessage()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
addError { "Unsupported TC test message: $message" }
|
||||||
|
state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
afterMessage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun parseException(e: ParseException, text: String) {
|
||||||
|
errors += buildString {
|
||||||
|
append("Failed to parse TC test message: \"").append(text).appendLine("\"")
|
||||||
|
appendLine(e.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addError(error: () -> String) {
|
||||||
|
errors += error()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getReport(): TestReport {
|
||||||
|
// 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 }
|
||||||
|
|
||||||
|
return TestReport(passedTests, failedTests, ignoredTests, nonTestOutput.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed interface TCTestReportParseState {
|
||||||
|
object Begin : State
|
||||||
|
|
||||||
|
class TestSuiteStarted(val testSuiteName: String) : State
|
||||||
|
object TestSuiteFinished : State
|
||||||
|
|
||||||
|
sealed class TestState(val testSuite: TestSuiteStarted, val functionName: FunctionName) : State {
|
||||||
|
val testName: TestName get() = "${testSuite.testSuiteName}.$functionName"
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestIgnored(testSuite: TestSuiteStarted, functionName: FunctionName) : TestState(testSuite, functionName)
|
||||||
|
class TestStarted(testSuite: TestSuiteStarted, functionName: FunctionName) : TestState(testSuite, functionName)
|
||||||
|
class TestFailed(testSuite: TestSuiteStarted, functionName: FunctionName) : TestState(testSuite, functionName)
|
||||||
|
class TestFinished(testSuite: TestSuiteStarted, functionName: FunctionName) : TestState(testSuite, functionName)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val State.testSuite: State.TestSuiteStarted
|
||||||
|
get() = if (this is State.TestSuiteStarted) this else cast<State.TestState>().testSuite
|
||||||
|
|
||||||
|
private val BaseTestMessage.functionName: FunctionName get() = testName
|
||||||
Reference in New Issue
Block a user