[Native][tests] Support parsing dumped test listing

^KT-50316
This commit is contained in:
Dmitriy Dolovov
2022-02-11 16:56:04 +03:00
parent 5a26f58b24
commit 5500e3a332
2 changed files with 84 additions and 23 deletions
@@ -31,29 +31,48 @@ internal class PackageName private constructor(private val fqn: String, val segm
* [packagePartClassName] - package-part class name (if there is any) * [packagePartClassName] - package-part class name (if there is any)
* [functionName] - name of test function * [functionName] - name of test function
*/ */
internal data class TestName(private val fqn: String) { internal class TestName {
private val fqn: String
val packageName: PackageName val packageName: PackageName
val packagePartClassName: String? val packagePartClassName: String?
val functionName: String val functionName: String
init { constructor(purePackageSegments: List<String>, classNames: List<String>, functionName: String) {
this.functionName = functionName
val segments = purePackageSegments.toMutableList()
if (classNames.lastOrNull().isPackagePartClassName()) {
packagePartClassName = classNames.last()
segments += classNames.dropLast(1)
} else {
packagePartClassName = null
segments += classNames
}
packageName = PackageName(segments)
fqn = buildString {
if (!packageName.isEmpty()) append(packageName).append('.')
if (packagePartClassName != null) append(packagePartClassName).append('.')
append(functionName)
}
}
constructor(fqn: String) {
this.fqn = fqn
val segments = fqn.split('.').toMutableList() val segments = fqn.split('.').toMutableList()
functionName = segments.removeLast() functionName = segments.removeLast()
packagePartClassName = if (segments.lastOrNull().isPackagePartClassName()) segments.removeLast() else null
val maybePackagePartClassSegment = segments.lastOrNull()
packagePartClassName = if (maybePackagePartClassSegment?.firstOrNull()?.isEffectivelyUpperCase() == true
&& maybePackagePartClassSegment.endsWith("Kt")
) {
segments.removeLast()
} else
null
packageName = PackageName(segments) packageName = PackageName(segments)
} }
override fun toString() = fqn override fun toString() = fqn
override fun equals(other: Any?) = fqn == (other as? TestName)?.fqn
override fun hashCode() = fqn.hashCode()
companion object { companion object {
private fun Char.isEffectivelyUpperCase() = if (isUpperCase()) true else !isLowerCase() private fun Char.isEffectivelyUpperCase() = if (isUpperCase()) true else !isLowerCase()
private fun String?.isPackagePartClassName() = this != null && firstOrNull()?.isEffectivelyUpperCase() == true && endsWith("Kt")
} }
} }
@@ -10,6 +10,48 @@ import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.utils.addToStdlib.cast import org.jetbrains.kotlin.utils.addToStdlib.cast
import org.jetbrains.kotlin.utils.addToStdlib.safeAs import org.jetbrains.kotlin.utils.addToStdlib.safeAs
/**
* Extracts [TestName]s from the test listing produced immediately during the compilation (turned on with
* "-Xdump-tests-to=" compiler flag).
*
* Example:
* sample/test/SampleTestKt:one
* sample/test/SampleTestKt:two
*
* yields TestName(packageName = "sample.test", packagePartClassName = "SampleTestKt", functionName = "one")
* and TestName(packageName = "sample.test", packagePartClassName = "SampleTestKt", functionName = "two")
*/
internal object DumpedTestListing {
fun parse(listing: String): Collection<TestName> {
val lines = listing.lines()
var emptyLineEncountered = false
return lines.mapIndexedNotNull { index: Int, line: String ->
fun parseError(message: String): Nothing = parseError(message, index, line, listing)
when {
line.isBlank() -> {
emptyLineEncountered = true
null
}
emptyLineEncountered -> parseError("Unexpected empty line")
else -> {
val (packageAndClass, functionName) = line.trim()
.split(':')
.takeIf { items -> items.size == 2 && items.none(String::isBlank) }
?: parseError("Malformed test name")
with(packageAndClass.split('/')) {
val classNames = last().split('.')
val packageSegments = dropLast(1)
TestName(packageSegments, classNames, functionName)
}
}
}
}
}
}
/** /**
* Extracts [TestName]s from GTest listing. * Extracts [TestName]s from GTest listing.
* *
@@ -23,20 +65,11 @@ import org.jetbrains.kotlin.utils.addToStdlib.safeAs
*/ */
internal object GTestListing { internal object GTestListing {
fun parse(listing: String): Collection<TestName> = buildList { fun parse(listing: String): Collection<TestName> = buildList {
fun parseError(message: String, index: Int, line: String): Nothing = fail {
buildString {
appendLine("$message at line #$index: \"$line\"")
appendLine()
appendLine("Full listing:")
appendLine(listing)
}
}
var state: ParseState = ParseState.Begin var state: ParseState = ParseState.Begin
val lines = listing.lines() val lines = listing.lines()
lines.forEachIndexed { index, line -> lines.forEachIndexed { index, line ->
fun parseError(message: String): Nothing = parseError(message, index, line) fun parseError(message: String): Nothing = parseError(message, index, line, listing)
state = when { state = when {
line.startsWith(STDLIB_TESTS_IGNORED_LINE_PREFIX) && state is ParseState.Begin -> ParseState.Begin line.startsWith(STDLIB_TESTS_IGNORED_LINE_PREFIX) && state is ParseState.Begin -> ParseState.Begin
@@ -63,7 +96,7 @@ internal object GTestListing {
} }
if (state is ParseState.NewTestSuite) if (state is ParseState.NewTestSuite)
parseError("Test name expected before test suite name", lines.lastIndex, lines.last()) parseError("Test name expected before test suite name", lines.lastIndex, lines.last(), listing)
} }
private sealed interface ParseState { private sealed interface ParseState {
@@ -80,3 +113,12 @@ internal object GTestListing {
// The very first line of stdlib test output may contain seed of Random. Such line should be ignored. // 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 const val STDLIB_TESTS_IGNORED_LINE_PREFIX = "Seed: "
} }
private fun parseError(message: String, index: Int, line: String, listing: String): Nothing = fail {
buildString {
appendLine("$message at line #$index: \"$line\"")
appendLine()
appendLine("Full listing:")
appendLine(listing)
}
}