[Native][tests] Extract test function names from test executables

This commit is contained in:
Dmitriy Dolovov
2021-12-01 14:33:22 +03:00
parent 87873f9824
commit 179d324444
12 changed files with 259 additions and 178 deletions
@@ -23,8 +23,8 @@ internal abstract class LoggedData {
protected abstract fun computeText(): String
final override fun toString() = text
fun withErrorMessageHeader(errorMessageHeader: String): String = buildString {
appendLine(errorMessageHeader)
fun withErrorMessage(errorMessage: String): String = buildString {
appendLine(errorMessage)
appendLine()
appendLine(this@LoggedData)
}
@@ -90,27 +90,31 @@ internal abstract class LoggedData {
class TestRunParameters(
private val compilerCall: CompilerCall,
private val testCaseId: TestCaseId,
private val testCaseId: TestCaseId?,
private val runArgs: Iterable<String>,
private val runParameters: List<TestRunParameter>
private val runParameters: List<TestRunParameter>?
) : LoggedData() {
override fun computeText() = buildString {
if (testCaseId is TestCaseId.TestDataFile) {
appendLine("TEST DATA FILE:")
appendLine(testCaseId.file)
} else {
appendLine("TEST CASE ID:")
appendLine(testCaseId)
when {
testCaseId is TestCaseId.TestDataFile -> {
appendLine("TEST DATA FILE:")
appendLine(testCaseId.file)
appendLine()
}
testCaseId != null -> {
appendLine("TEST CASE ID:")
appendLine(testCaseId)
appendLine()
}
}
appendLine()
appendArguments("TEST RUN ARGUMENTS:", runArgs)
appendLine()
runParameters.get<TestRunParameter.WithInputData> {
runParameters?.get<TestRunParameter.WithInputData> {
appendLine("INPUT DATA FILE:")
appendLine(inputDataFile)
appendLine()
}
runParameters.get<TestRunParameter.WithExpectedOutputData> {
runParameters?.get<TestRunParameter.WithExpectedOutputData> {
appendLine("EXPECTED OUTPUT DATA FILE:")
appendLine(expectedOutputDataFile)
appendLine()
@@ -198,13 +202,13 @@ internal abstract class LoggedData {
appendLine("- Exit code: ${runResult.exitCode ?: "<unknown>"}")
appendDuration(runResult.duration)
appendLine()
appendLine("========== BEGIN: TEST STDOUT ==========")
appendLine("========== BEGIN: STDOUT ==========")
if (runResult.output.stdOut.isNotEmpty()) appendLine(runResult.output.stdOut.trimEnd())
appendLine("========== END: TEST STDOUT ==========")
appendLine("========== END: STDOUT ==========")
appendLine()
appendLine("========== BEGIN: TEST STDERR ==========")
appendLine("========== BEGIN: STDERR ==========")
if (runResult.output.stdErr.isNotEmpty()) appendLine(runResult.output.stdErr.trimEnd())
appendLine("========== END: TEST STDERR ==========")
appendLine("========== END: STDERR ==========")
return this
}
}
@@ -180,11 +180,7 @@ internal class TestCase(
) {
sealed interface Extras
class NoTestRunnerExtras(val entryPoint: String, val inputDataFile: File?) : Extras
class WithTestRunnerExtras(val testFunctions: Collection<TestFunction>) : Extras {
companion object {
val EMPTY = WithTestRunnerExtras(emptyList())
}
}
object WithTestRunnerExtras : Extras
init {
when (kind) {
@@ -181,7 +181,7 @@ internal sealed interface TestCompilationResult {
is DependencyFailures -> fail { describeDependencyFailures() }
}
private fun Failure.describeFailure() = loggedData.withErrorMessageHeader(
private fun Failure.describeFailure() = loggedData.withErrorMessage(
when (this@describeFailure) {
is CompilerFailure -> "Compilation failed."
is UnexpectedFailure -> "Compilation failed with unexpected exception."
@@ -9,16 +9,17 @@ package org.jetbrains.kotlin.konan.blackboxtest.support
import com.intellij.openapi.util.Disposer
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCase.NoTestRunnerExtras
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCase.WithTestRunnerExtras
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCompilationResult.Companion.assertSuccess
import org.jetbrains.kotlin.konan.blackboxtest.support.group.TestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.AbstractRunner
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.LocalTestFunctionExtractor
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.LocalTestRunner
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.GlobalSettings
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Settings
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ThreadSafeCache
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TreeNode
import org.jetbrains.kotlin.konan.blackboxtest.support.util.buildTree
import org.jetbrains.kotlin.konan.blackboxtest.support.util.isSameOrSubpackageOf
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.junit.jupiter.api.Assumptions.assumeTrue
@@ -31,6 +32,7 @@ internal class TestRunProvider(
) : ExtensionContext.Store.CloseableResource {
private val compilationFactory = TestCompilationFactory(settings)
private val cachedCompilations = ThreadSafeCache<TestCompilationCacheKey, TestCompilation>()
private val cachedTestFunctions = ThreadSafeCache<TestCompilationCacheKey, Collection<TestFunction>>()
fun setProcessors(testDataFile: File, sourceTransformers: List<(String) -> String>) {
testCaseGroupProvider.setPreprocessors(testDataFile, sourceTransformers)
@@ -61,7 +63,7 @@ internal class TestRunProvider(
* }
* }
*/
fun getSingleTestRun(testCaseId: TestCaseId): TestRun = withTestExecutable(testCaseId) { testCase, executable ->
fun getSingleTestRun(testCaseId: TestCaseId): TestRun = withTestExecutable(testCaseId) { testCase, executable, _ ->
val runParameters = getRunParameters(testCase, testFunction = null)
TestRun(displayName = /* Unimportant. Used only in dynamic tests. */ "", executable, runParameters, testCase.id)
}
@@ -96,7 +98,7 @@ internal class TestRunProvider(
* }
* }
*/
fun getTestRuns(testCaseId: TestCaseId): TreeNode<TestRun> = withTestExecutable(testCaseId) { testCase, executable ->
fun getTestRuns(testCaseId: TestCaseId): TreeNode<TestRun> = withTestExecutable(testCaseId) { testCase, executable, cacheKey ->
fun createTestRun(testRunName: String, testFunction: TestFunction?): TestRun {
val runParameters = getRunParameters(testCase, testFunction)
return TestRun(testRunName, executable, runParameters, testCase.id)
@@ -109,7 +111,10 @@ internal class TestRunProvider(
TreeNode.oneLevel(testRun)
}
TestKind.REGULAR, TestKind.STANDALONE -> {
val testFunctions = testCase.extras<WithTestRunnerExtras>().testFunctions
val testFunctions = cachedTestFunctions.computeIfAbsent(cacheKey) {
extractTestFunctions(executable)
}.filterIrrelevant(testCase)
testFunctions.buildTree(TestFunction::packageName) { testFunction ->
createTestRun(testFunction.functionName, testFunction)
}
@@ -117,7 +122,7 @@ internal class TestRunProvider(
}
}
private fun <T> withTestExecutable(testCaseId: TestCaseId, action: (TestCase, TestExecutable) -> T): T {
private fun <T> withTestExecutable(testCaseId: TestCaseId, action: (TestCase, TestExecutable, TestCompilationCacheKey) -> T): T {
settings.assertNotDisposed()
val testCaseGroup = testCaseGroupProvider.getTestCaseGroup(testCaseId.testCaseGroupId) ?: fail { "No test case for $testCaseId" }
@@ -125,36 +130,37 @@ internal class TestRunProvider(
val testCase = testCaseGroup.getByName(testCaseId) ?: fail { "No test case for $testCaseId" }
val testCompilation = when (testCase.kind) {
val (testCompilation, cacheKey) = when (testCase.kind) {
TestKind.STANDALONE, TestKind.STANDALONE_NO_TR -> {
// Create a separate compilation for each standalone test case.
val cacheKey = TestCompilationCacheKey.Standalone(testCaseId)
cachedCompilations.computeIfAbsent(cacheKey) {
val testCompilation = cachedCompilations.computeIfAbsent(cacheKey) {
compilationFactory.testCasesToExecutable(listOf(testCase))
}
testCompilation to cacheKey
}
TestKind.REGULAR -> {
// Group regular test cases by compiler arguments.
val cacheKey = TestCompilationCacheKey.Grouped(testCaseId.testCaseGroupId, testCase.freeCompilerArgs)
cachedCompilations.computeIfAbsent(cacheKey) {
val testCompilation = cachedCompilations.computeIfAbsent(cacheKey) {
val testCases = testCaseGroup.getRegularOnlyByCompilerArgs(testCase.freeCompilerArgs)
assertTrue(testCases.isNotEmpty())
compilationFactory.testCasesToExecutable(testCases)
}
testCompilation to cacheKey
}
}
val (executableFile, loggedCompilerCall) = testCompilation.result.assertSuccess() // <-- Compilation happens here.
val executable = TestExecutable(executableFile, loggedCompilerCall)
return action(testCase, executable)
return action(testCase, executable, cacheKey)
}
private fun getRunParameters(testCase: TestCase, testFunction: TestFunction?): List<TestRunParameter> = with(testCase) {
when (kind) {
TestKind.STANDALONE_NO_TR -> {
assertTrue(testFunction == null)
listOfNotNull(
extras<NoTestRunnerExtras>().inputDataFile?.let(TestRunParameter::WithInputData),
expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
@@ -175,16 +181,32 @@ internal class TestRunProvider(
// Currently, only local test runner is supported.
fun createRunner(testRun: TestRun): AbstractRunner<*> = with(settings.get<GlobalSettings>()) {
when (val target = target) {
hostTarget -> LocalTestRunner(testRun, executionTimeout)
else -> fail {
"""
Running at non-host target is not supported yet.
Compilation target: $target
Host target: $hostTarget
""".trimIndent()
}
}
if (target == hostTarget)
LocalTestRunner(testRun, executionTimeout)
else
runningAtNonHostTarget()
}
// Currently, only local test function extractor is supported.
private fun extractTestFunctions(executable: TestExecutable): Collection<TestFunction> = with(settings.get<GlobalSettings>()) {
if (target == hostTarget)
LocalTestFunctionExtractor(executable, executionTimeout).run()
else
runningAtNonHostTarget()
}
private fun Collection<TestFunction>.filterIrrelevant(testCase: TestCase) =
if (testCase.kind == TestKind.REGULAR)
filter { testFunction -> testFunction.packageName.isSameOrSubpackageOf(testCase.nominalPackageName) }
else
this
private fun GlobalSettings.runningAtNonHostTarget(): Nothing = fail {
"""
Running at non-host target is not supported yet.
Compilation target: $target
Host target: $hostTarget
""".trimIndent()
}
override fun close() {
@@ -594,7 +594,7 @@ private class ExtTestDataFile(
freeCompilerArgs = assembleFreeCompilerArgs(),
nominalPackageName = testDataFileSettings.nominalPackageName,
expectedOutputDataFile = null,
extras = WithTestRunnerExtras.EMPTY
extras = WithTestRunnerExtras
)
testCase.initialize(sharedModules::get)
@@ -11,6 +11,7 @@ import org.jetbrains.kotlin.konan.blackboxtest.support.TestCase.WithTestRunnerEx
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.GeneratedSources
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Settings
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.TestRoots
import org.jetbrains.kotlin.konan.blackboxtest.support.util.*
import org.jetbrains.kotlin.konan.blackboxtest.support.util.DEFAULT_FILE_NAME
import org.jetbrains.kotlin.konan.blackboxtest.support.util.ThreadSafeFactory
import org.jetbrains.kotlin.konan.blackboxtest.support.util.computeGeneratedSourcesDir
@@ -194,24 +195,9 @@ internal class StandardTestCaseGroupProvider(private val settings: Settings) : T
val expectedOutputDataFile = parseOutputDataFile(baseDir = testDataFile.parentFile, registeredDirectives, location)
val testKind = parseTestKind(registeredDirectives, location)
val extras = when (testKind) {
TestKind.REGULAR, TestKind.STANDALONE -> {
// Fix package declarations to avoid unintended conflicts between symbols with the same name in different test cases.
val needToFixPackageNames = testKind == TestKind.REGULAR
val testFunctions = checkExistingPackageNamesAndCollectTestFunctions(
testModules = testModules.values,
basePackageName = nominalPackageName,
fixPackageNames = needToFixPackageNames,
testDataFile = testDataFile
)
WithTestRunnerExtras(testFunctions)
}
TestKind.STANDALONE_NO_TR -> NoTestRunnerExtras(
entryPoint = parseEntryPoint(registeredDirectives, location),
inputDataFile = parseInputDataFile(baseDir = testDataFile.parentFile, registeredDirectives, location)
)
if (testKind == TestKind.REGULAR) {
// Fix package declarations to avoid unintended conflicts between symbols with the same name in different test cases.
fixPackageNames(testModules.values, nominalPackageName, testDataFile)
}
val testCase = TestCase(
@@ -221,7 +207,13 @@ internal class StandardTestCaseGroupProvider(private val settings: Settings) : T
freeCompilerArgs = freeCompilerArgs,
nominalPackageName = nominalPackageName,
expectedOutputDataFile = expectedOutputDataFile,
extras = extras
extras = if (testKind == TestKind.STANDALONE_NO_TR)
NoTestRunnerExtras(
entryPoint = parseEntryPoint(registeredDirectives, location),
inputDataFile = parseInputDataFile(baseDir = testDataFile.parentFile, registeredDirectives, location)
)
else
WithTestRunnerExtras
)
testCase.initialize(findSharedModule = null)
@@ -229,122 +221,33 @@ internal class StandardTestCaseGroupProvider(private val settings: Settings) : T
}
companion object {
private fun CharSequence.hasAnythingButComments(): Boolean {
return dropNonMeaningfulLines().firstOrNull() != null
}
private fun checkExistingPackageNamesAndCollectTestFunctions(
testModules: Collection<TestModule.Exclusive>,
basePackageName: PackageFQN,
fixPackageNames: Boolean,
testDataFile: File
): List<TestFunction> {
val testFunctions = mutableListOf<TestFunction>()
private fun fixPackageNames(testModules: Collection<TestModule.Exclusive>, basePackageName: PackageFQN, testDataFile: File) {
testModules.forEach { testModule ->
testModule.files.forEach { testFile ->
val meaningfulLines = testFile.text.dropNonMeaningfulLines()
val firstMeaningfulLine = testFile.text.dropNonMeaningfulLines().firstOrNull()
// Retrieve the package name if it is declared inside the test file.
val existingPackageName = meaningfulLines.firstOrNull()?.let { (lineNumber, firstMeaningfulLine) ->
getExistingPackageName(lineNumber, firstMeaningfulLine, basePackageName, testDataFile)
}
// Compute the package name to be added (if it should be added, of course).
val packageNameToAdd = if (fixPackageNames && existingPackageName == null) basePackageName else null
// Collect test functions.
val effectivePackageName = existingPackageName ?: packageNameToAdd ?: ""
collectTestFunctionsNames(meaningfulLines, testDataFile) { functionName ->
testFunctions += TestFunction(effectivePackageName, functionName)
}
// Add package declaration (if necessary).
if (packageNameToAdd != null) {
testFile.update { text -> "package $packageNameToAdd $text" }
}
}
}
return testFunctions
}
private fun getExistingPackageName(lineNumber: Int, line: String, basePackageName: PackageFQN, testDataFile: File): PackageFQN? {
val existingPackageName = line
.substringAfter("package ", missingDelimiterValue = "")
.trimStart()
.takeIf(String::isNotEmpty)
?: return null
assertTrue(
existingPackageName == basePackageName
|| (existingPackageName.length > basePackageName.length
&& existingPackageName.startsWith(basePackageName)
&& existingPackageName[basePackageName.length] == '.')
) {
"""
${Location(testDataFile, lineNumber)}: Invalid package name declaration found: $line
Expected: package $basePackageName
""".trimIndent()
}
return existingPackageName
}
private inline fun collectTestFunctionsNames(
meaningfulLines: Sequence<Pair<Int, String>>,
testDataFile: File,
crossinline consumeFunctionName: (FunctionName) -> Unit
) {
var expectingTestFunction = false
val extractFunctionName: (lineNumber: Int, line: String) -> Unit = { lineNumber, line ->
val match = FUNCTION_REGEX.matchEntire(line)
?: fail { "${Location(testDataFile, lineNumber)}: Can't parse name of test function: $line" }
val functionName = match.groupValues[1]
consumeFunctionName(functionName)
}
val processPossiblyAnnotatedLine: (lineNumber: Int, line: String) -> Unit = { lineNumber, line ->
if (line.startsWith('@'))
for (testAnnotation in TEST_ANNOTATIONS) {
if (line.startsWith(testAnnotation)) {
val remainder = line.substringAfter(testAnnotation).trimStart()
if (remainder.isNotEmpty()) extractFunctionName(lineNumber, remainder) else expectingTestFunction = true
break
val existingPackageName = firstMeaningfulLine?.getExistingPackageName()
if (existingPackageName != null) {
// Validate it.
assertTrue(
existingPackageName == basePackageName
|| (existingPackageName.length > basePackageName.length
&& existingPackageName.startsWith(basePackageName)
&& existingPackageName[basePackageName.length] == '.')
) {
val location = Location(testDataFile, firstMeaningfulLine.number)
"""
$location: Invalid package name declaration found: $firstMeaningfulLine
Expected: package $basePackageName
""".trimIndent()
}
} else {
// Add package declaration.
testFile.update { text -> "package $basePackageName $text" }
}
}
meaningfulLines.forEach { (lineNumber, line) ->
if (expectingTestFunction) {
extractFunctionName(lineNumber, line)
expectingTestFunction = false
} else
processPossiblyAnnotatedLine(lineNumber, line)
}
}
private fun CharSequence.dropNonMeaningfulLines(): Sequence<Pair<Int, String>> {
var inMultilineComment = false
return lineSequence()
.mapIndexed { lineNumber, line -> lineNumber to line.trim() }
.dropWhile { (_, trimmedLine) ->
when {
inMultilineComment -> inMultilineComment = !trimmedLine.endsWith("*/")
trimmedLine.startsWith("/*") -> inMultilineComment = true
trimmedLine.isMeaningfulLine() -> return@dropWhile false
}
return@dropWhile true
}
}
}
private fun String.isMeaningfulLine() = isNotEmpty() && !startsWith("//") && !startsWith("@file:")
private val TEST_ANNOTATIONS = listOf("@Test", "@kotlin.test.Test")
private val FUNCTION_REGEX = Regex("^\\s*fun\\s+(\\p{javaJavaIdentifierPart}+).*$")
}
}
@@ -22,7 +22,7 @@ internal abstract class AbstractRunner<R> {
val resultHandler = when (val runResult = run.run()) {
is RunResult.TimeoutExceeded -> fail {
LoggedData.TestRunTimeoutExceeded(getLoggedParameters(), runResult)
.withErrorMessageHeader("Timeout exceeded during test execution.")
.withErrorMessage("Timeout exceeded during test execution.")
}
is RunResult.Completed -> buildResultHandler(runResult)
}
@@ -45,12 +45,12 @@ internal abstract class AbstractRunner<R> {
abstract fun getLoggedRun(): LoggedData
abstract fun handle(): R
protected inline fun <T> verifyExpectation(expected: T, actual: T, crossinline errorMessageHeader: () -> String) {
assertEquals(expected, actual) { getLoggedRun().withErrorMessageHeader(errorMessageHeader()) }
protected inline fun <T> verifyExpectation(expected: T, actual: T, crossinline errorMessage: () -> String) {
assertEquals(expected, actual) { getLoggedRun().withErrorMessage(errorMessage()) }
}
protected inline fun verifyExpectation(shouldBeTrue: Boolean, crossinline errorMessageHeader: () -> String) {
assertTrue(shouldBeTrue) { getLoggedRun().withErrorMessageHeader(errorMessageHeader()) }
protected inline fun verifyExpectation(shouldBeTrue: Boolean, crossinline errorMessage: () -> String) {
assertTrue(shouldBeTrue) { getLoggedRun().withErrorMessage(errorMessage()) }
}
}
}
@@ -0,0 +1,42 @@
/*
* 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.runner
import org.jetbrains.kotlin.konan.blackboxtest.support.LoggedData
import org.jetbrains.kotlin.konan.blackboxtest.support.TestExecutable
import org.jetbrains.kotlin.konan.blackboxtest.support.TestFunction
import org.jetbrains.kotlin.konan.blackboxtest.support.util.parseGTestListing
import org.jetbrains.kotlin.test.services.JUnit5Assertions
import kotlin.time.Duration
internal class LocalTestFunctionExtractor(
override val executable: TestExecutable,
executionTimeout: Duration
) : AbstractLocalProcessRunner<Collection<TestFunction>>(executionTimeout) {
override val visibleProcessName get() = "Test function extractor"
override val programArgs = listOf(executable.executableFile.path, "--ktest_list_tests")
override fun getLoggedParameters() = LoggedData.TestRunParameters(
compilerCall = executable.loggedCompilerCall,
testCaseId = null,
runArgs = programArgs,
runParameters = null
)
override fun buildResultHandler(runResult: RunResult.Completed) = ResultHandler(runResult)
override fun handleUnexpectedFailure(t: Throwable) = JUnit5Assertions.fail {
LoggedData.TestRunUnexpectedFailure(getLoggedParameters(), t)
.withErrorMessage("Test function extraction failed with unexpected exception.")
}
inner class ResultHandler(
runResult: RunResult.Completed
) : AbstractLocalProcessRunner<Collection<TestFunction>>.ResultHandler(runResult) {
override fun getLoggedRun() = LoggedData.TestRun(getLoggedParameters(), runResult)
override fun doHandle() = parseGTestListing(runResult.output.stdOut)
}
}
@@ -41,7 +41,7 @@ internal class LocalTestRunner(
override fun handleUnexpectedFailure(t: Throwable) = fail {
LoggedData.TestRunUnexpectedFailure(getLoggedParameters(), t)
.withErrorMessageHeader("Test execution failed with unexpected exception.")
.withErrorMessage("Test execution failed with unexpected exception.")
}
inner class ResultHandler(runResult: RunResult.Completed) : AbstractLocalProcessRunner<Unit>.ResultHandler(runResult) {
@@ -5,6 +5,11 @@
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
@@ -68,5 +73,69 @@ internal fun parseGTestReport(stdOut: String): GTestReport {
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 {
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+.*""")
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
}
@@ -74,3 +74,8 @@ internal fun joinPackageNames(a: PackageFQN, b: PackageFQN): PackageFQN = when {
internal fun String.prependPackageName(packageName: PackageFQN): String =
if (packageName.isEmpty()) this else "$packageName.$this"
internal fun PackageFQN.isSameOrSubpackageOf(parentPackageName: PackageFQN): Boolean =
parentPackageName.isEmpty()
|| this == parentPackageName
|| (length > parentPackageName.length && startsWith(parentPackageName) && this[parentPackageName.length] == '.')
@@ -0,0 +1,40 @@
/*
* 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
internal typealias SourceText = CharSequence
internal typealias SourceLine = String
internal data class NumberedSourceLine(val number: Int, val text: SourceLine) : CharSequence by text {
override fun toString() = text
}
internal fun SourceText.hasAnythingButComments(): Boolean =
dropNonMeaningfulLines().firstOrNull() != null
internal fun NumberedSourceLine.getExistingPackageName(): PackageFQN? =
text.substringAfter("package ", missingDelimiterValue = "")
.trimStart()
.takeIf(String::isNotEmpty)
internal fun SourceText.dropNonMeaningfulLines(): Sequence<NumberedSourceLine> {
var inMultilineComment = false
return lineSequence()
.mapIndexed { lineNumber, line -> NumberedSourceLine(lineNumber, line.trim()) }
.dropWhile { (_, trimmedLine) ->
when {
inMultilineComment -> inMultilineComment = !trimmedLine.endsWith("*/")
trimmedLine.startsWith("/*") -> inMultilineComment = true
trimmedLine.isMeaningfulLine() -> return@dropWhile false
}
return@dropWhile true
}
}
private fun SourceLine.isMeaningfulLine() = isNotEmpty() && !startsWith("//") && !startsWith("@file:")