[Native][tests] Drop TestFunction/FunctionName, introduce TestName instead

This commit is contained in:
Dmitriy Dolovov
2021-12-03 12:19:20 +03:00
parent 7abfe4d47b
commit cdbbc70116
8 changed files with 103 additions and 97 deletions
@@ -22,3 +22,38 @@ internal class PackageName private constructor(private val fqn: String, val segm
val EMPTY = PackageName("", emptyList())
}
}
/**
* Represents a single test name (i.e. a function annotated with [kotlin.test.Test]) inside of a [TestFile].
*
* [packageName] - containing package name. For the sake of simplification [packageName] may also include top-level and nested class names,
* but it does not include package-part class name (i.e. "SomethingKt").
* [packagePartClassName] - package-part class name (if there is any)
* [functionName] - name of test function
*/
internal data class TestName(private val fqn: String) {
val packageName: PackageName
val packagePartClassName: String?
val functionName: String
init {
val segments = fqn.split('.').toMutableList()
functionName = segments.removeLast()
val maybePackagePartClassSegment = segments.lastOrNull()
packagePartClassName = if (maybePackagePartClassSegment?.firstOrNull()?.isEffectivelyUpperCase() == true
&& maybePackagePartClassSegment.endsWith("Kt")
) {
segments.removeLast()
} else
null
packageName = PackageName(segments)
}
override fun toString() = fqn
companion object {
private fun Char.isEffectivelyUpperCase() = if (isUpperCase()) true else !isLowerCase()
}
}
@@ -15,13 +15,6 @@ import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import java.io.File
internal typealias FunctionName = String
/**
* Represents a single test function (i.e. a function annotated with [kotlin.test.Test]) inside of a [TestFile].
*/
internal data class TestFunction(val packageName: PackageName, val functionName: FunctionName)
/**
* Represents a single file that will be supplied to the compiler.
*/
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.konan.blackboxtest.support
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestName
import org.jetbrains.kotlin.konan.blackboxtest.support.util.startsWith
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertFalse
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File
@@ -26,45 +26,27 @@ internal sealed interface TestRunParameter {
fun applyTo(programArgs: MutableList<String>)
sealed class WithFilter : TestRunParameter {
abstract fun testMatches(testName: String): Boolean
abstract fun testMatches(testName: TestName): Boolean
}
class WithPackageFilter(packageName: PackageName) : WithFilter() {
class WithPackageFilter(private val packageName: PackageName) : WithFilter() {
init {
assertFalse(packageName.isEmpty())
}
private val packagePrefix = "$packageName."
override fun applyTo(programArgs: MutableList<String>) {
programArgs += "--ktest_filter=$packagePrefix*"
programArgs += "--ktest_filter=$packageName.*"
}
override fun testMatches(testName: TestName) = testName.startsWith(packagePrefix)
override fun testMatches(testName: TestName) = testName.packageName.startsWith(packageName)
}
class WithFunctionFilter(val testFunction: TestFunction) : WithFilter() {
private val packagePrefix = if (testFunction.packageName.isEmpty()) "" else "${testFunction.packageName}."
class WithTestFilter(val testName: TestName) : WithFilter() {
override fun applyTo(programArgs: MutableList<String>) {
programArgs += "--ktest_regex_filter=${packagePrefix.replace(".", "\\.")}([^\\.]+)\\.${testFunction.functionName}"
programArgs += "--ktest_filter=$testName"
}
override fun testMatches(testName: String): Boolean {
val remainder = if (packagePrefix.isNotEmpty()) {
if (!testName.startsWith(packagePrefix)) return false
testName.substringAfter(packagePrefix)
} else
testName
val suffix = remainder
.split('.')
.takeIf { it.size == 2 }
?.last()
?: return false
return suffix == testFunction.functionName
}
override fun testMatches(testName: TestName) = this.testName == testName
}
object WithTCTestLogger : TestRunParameter {
@@ -12,7 +12,7 @@ import org.jetbrains.kotlin.konan.blackboxtest.support.TestCase.NoTestRunnerExtr
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.LocalTestNameExtractor
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
@@ -32,7 +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>>()
private val cachedTestNames = ThreadSafeCache<TestCompilationCacheKey, Collection<TestName>>()
fun setProcessors(testDataFile: File, sourceTransformers: List<(String) -> String>) {
testCaseGroupProvider.setPreprocessors(testDataFile, sourceTransformers)
@@ -64,7 +64,7 @@ internal class TestRunProvider(
* }
*/
fun getSingleTestRun(testCaseId: TestCaseId): TestRun = withTestExecutable(testCaseId) { testCase, executable, _ ->
val runParameters = getRunParameters(testCase, testFunction = null)
val runParameters = getRunParameters(testCase, testName = null)
TestRun(displayName = /* Unimportant. Used only in dynamic tests. */ "", executable, runParameters, testCase.id)
}
@@ -101,24 +101,24 @@ internal class TestRunProvider(
fun getTestRuns(
testCaseId: TestCaseId
): Collection<TreeNode<TestRun>> = withTestExecutable(testCaseId) { testCase, executable, cacheKey ->
fun createTestRun(testRunName: String, testFunction: TestFunction?): TestRun {
val runParameters = getRunParameters(testCase, testFunction)
fun createTestRun(testRunName: String, testName: TestName?): TestRun {
val runParameters = getRunParameters(testCase, testName)
return TestRun(testRunName, executable, runParameters, testCase.id)
}
when (testCase.kind) {
TestKind.STANDALONE_NO_TR -> {
val testRunName = testCase.extras<NoTestRunnerExtras>().entryPoint.substringAfterLast('.')
val testRun = createTestRun(testRunName, testFunction = null)
val testRun = createTestRun(testRunName, testName = null)
TreeNode.oneLevel(testRun)
}
TestKind.REGULAR, TestKind.STANDALONE -> {
val testFunctions = cachedTestFunctions.computeIfAbsent(cacheKey) {
extractTestFunctions(executable)
val testNames = cachedTestNames.computeIfAbsent(cacheKey) {
extractTestNames(executable)
}.filterIrrelevant(testCase)
testFunctions.buildTree(TestFunction::packageName) { testFunction ->
createTestRun(testFunction.functionName, testFunction)
testNames.buildTree(TestName::packageName) { testName ->
createTestRun(testName.functionName, testName)
}
}
}
@@ -159,10 +159,10 @@ internal class TestRunProvider(
return action(testCase, executable, cacheKey)
}
private fun getRunParameters(testCase: TestCase, testFunction: TestFunction?): List<TestRunParameter> = with(testCase) {
private fun getRunParameters(testCase: TestCase, testName: TestName?): List<TestRunParameter> = with(testCase) {
when (kind) {
TestKind.STANDALONE_NO_TR -> {
assertTrue(testFunction == null)
assertTrue(testName == null)
listOfNotNull(
extras<NoTestRunnerExtras>().inputDataFile?.let(TestRunParameter::WithInputData),
expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
@@ -170,12 +170,12 @@ internal class TestRunProvider(
}
TestKind.STANDALONE -> listOfNotNull(
TestRunParameter.WithTCTestLogger,
testFunction?.let(TestRunParameter::WithFunctionFilter),
testName?.let(TestRunParameter::WithTestFilter),
expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
)
TestKind.REGULAR -> listOfNotNull(
TestRunParameter.WithTCTestLogger,
testFunction?.let(TestRunParameter::WithFunctionFilter) ?: TestRunParameter.WithPackageFilter(nominalPackageName),
testName?.let(TestRunParameter::WithTestFilter) ?: TestRunParameter.WithPackageFilter(nominalPackageName),
expectedOutputDataFile?.let(TestRunParameter::WithExpectedOutputData)
)
}
@@ -189,17 +189,17 @@ internal class TestRunProvider(
runningAtNonHostTarget()
}
// Currently, only local test function extractor is supported.
private fun extractTestFunctions(executable: TestExecutable): Collection<TestFunction> = with(settings.get<GlobalSettings>()) {
// Currently, only local test name extractor is supported.
private fun extractTestNames(executable: TestExecutable): Collection<TestName> = with(settings.get<GlobalSettings>()) {
if (target == hostTarget)
LocalTestFunctionExtractor(executable, executionTimeout).run()
LocalTestNameExtractor(executable, executionTimeout).run()
else
runningAtNonHostTarget()
}
private fun Collection<TestFunction>.filterIrrelevant(testCase: TestCase) =
private fun Collection<TestName>.filterIrrelevant(testCase: TestCase) =
if (testCase.kind == TestKind.REGULAR)
filter { testFunction -> testFunction.packageName.startsWith(testCase.nominalPackageName) }
filter { testName -> testName.packageName.startsWith(testCase.nominalPackageName) }
else
this
@@ -7,17 +7,17 @@ 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.TestName
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestOutputFilter
import org.jetbrains.kotlin.konan.blackboxtest.support.util.parseGTestListing
import org.jetbrains.kotlin.test.services.JUnit5Assertions
import kotlin.time.Duration
internal class LocalTestFunctionExtractor(
internal class LocalTestNameExtractor(
override val executable: TestExecutable,
executionTimeout: Duration
) : AbstractLocalProcessRunner<Collection<TestFunction>>(executionTimeout) {
override val visibleProcessName get() = "Test function extractor"
) : AbstractLocalProcessRunner<Collection<TestName>>(executionTimeout) {
override val visibleProcessName get() = "Test name extractor"
override val programArgs = listOf(executable.executableFile.path, "--ktest_list_tests")
override val outputFilter get() = TestOutputFilter.NO_FILTERING
@@ -32,12 +32,12 @@ internal class LocalTestFunctionExtractor(
override fun handleUnexpectedFailure(t: Throwable) = JUnit5Assertions.fail {
LoggedData.TestRunUnexpectedFailure(getLoggedParameters(), t)
.withErrorMessage("Test function extraction failed with unexpected exception.")
.withErrorMessage("Test name extraction failed with unexpected exception.")
}
inner class ResultHandler(
runResult: RunResult.Completed
) : AbstractLocalProcessRunner<Collection<TestFunction>>.ResultHandler(runResult) {
) : AbstractLocalProcessRunner<Collection<TestName>>.ResultHandler(runResult) {
override fun getLoggedRun() = LoggedData.TestRun(getLoggedParameters(), runResult)
override fun doHandle() = parseGTestListing(runResult.processOutput.stdOut.filteredOutput)
}
@@ -8,7 +8,6 @@ package org.jetbrains.kotlin.konan.blackboxtest.support.runner
import com.intellij.openapi.util.text.StringUtilRt.convertLineSeparators
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.TestOutputFilter
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestReport
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
@@ -5,31 +5,33 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.util
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageName
import org.jetbrains.kotlin.konan.blackboxtest.support.TestFunction
import org.jetbrains.kotlin.konan.blackboxtest.support.TestName
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import org.jetbrains.kotlin.utils.addToStdlib.cast
import org.jetbrains.kotlin.utils.addToStdlib.safeAs
import org.jetbrains.kotlin.konan.blackboxtest.support.util.GTestListingParseState as State
/**
* Extracts [TestFunction]s from GTest listing.
* Extracts [TestName]s from GTest listing.
*
* Example:
* sample.test.SampleTestKt.
* one
* two
*
* yields TestFunction(sample.test, one) and TestFunction(sample.test, two).
* yields TestName(packageName = "sample.test", packagePartClassName = "SampleTestKt", functionName = "one")
* and TestName(packageName = "sample.test", packagePartClassName = "SampleTestKt", functionName = "two")
*/
internal fun parseGTestListing(rawGTestListing: String): Collection<TestFunction> = buildList {
internal fun parseGTestListing(listing: String): Collection<TestName> = buildList {
var state: State = State.Begin
rawGTestListing.lineSequence().forEachIndexed { index, line ->
listing.lineSequence().forEachIndexed { index, line ->
fun parseError(message: String): Nothing = fail {
buildString {
appendLine("$message at line #$index: \"$line\"")
appendLine()
appendLine("Full listing:")
appendLine(rawGTestListing)
appendLine(listing)
}
}
@@ -39,21 +41,18 @@ internal fun parseGTestListing(rawGTestListing: String): Collection<TestFunction
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)
line[0].isWhitespace() -> when (state) {
is State.NewTestSuite,
is State.NewTest -> {
val testSuite = state.testSuite
this += TestName(testSuite.testSuiteNameWithDotSuffix + line.trim())
State.NewTest(testSuite)
}
else -> parseError("Test name encountered before test suite name")
}
else -> when (state) {
is State.Begin, is State.NewTest -> {
val packageSegments = line.trimEnd().removeSuffix(".").split('.')
if (packageSegments.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?
State.NewTestSuite(PackageName(packageSegments.dropLast(1)))
State.NewTestSuite(line.trimEnd())
}
else -> parseError("Unexpected test suite name")
}
@@ -64,13 +63,13 @@ internal fun parseGTestListing(rawGTestListing: String): Collection<TestFunction
private sealed interface GTestListingParseState {
object Begin : State
object End : State
class NewTestSuite(override val packageName: PackageName) : State, HasPackageName
class NewTest(override val packageName: PackageName) : State, HasPackageName
interface HasPackageName {
val packageName: PackageName
}
class NewTestSuite(val testSuiteNameWithDotSuffix: String) : State
class NewTest(val testSuite: NewTestSuite) : State
}
private inline val State.testSuite: State.NewTestSuite
get() = safeAs<State.NewTestSuite>() ?: cast<State.NewTest>().testSuite
// 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: "
@@ -6,15 +6,13 @@
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.konan.blackboxtest.support.TestName
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>,
@@ -120,25 +118,25 @@ private class TCTestMessageParserCallback : ServiceMessageParserCallback {
is TestIgnored -> when (state) {
is State.TestSuiteStarted,
is State.TestIgnored,
is State.TestFinished -> State.TestIgnored(state.testSuite, message.functionName).also { ignoredTests += it.testName }
is State.TestFinished -> State.TestIgnored(state.testSuite, message.simpleTestName).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)
is State.TestFinished -> State.TestStarted(state.testSuite, message.simpleTestName)
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 }
State.TestFailed(s.testSuite, message.simpleTestName).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)
is State.TestStarted -> State.TestFinished(state.testSuite, message.simpleTestName).also { passedTests += it.testName }
is State.TestFailed -> State.TestFinished(state.testSuite, message.simpleTestName)
else -> unexpectedMessage()
}
else -> {
@@ -172,17 +170,17 @@ private sealed interface TCTestReportParseState {
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"
sealed class TestState(val testSuite: TestSuiteStarted, val simpleTestName: String) : State {
val testName: TestName get() = TestName("${testSuite.testSuiteName}.$simpleTestName")
}
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)
class TestIgnored(testSuite: TestSuiteStarted, simpleTestName: String) : TestState(testSuite, simpleTestName)
class TestStarted(testSuite: TestSuiteStarted, simpleTestName: String) : TestState(testSuite, simpleTestName)
class TestFailed(testSuite: TestSuiteStarted, simpleTestName: String) : TestState(testSuite, simpleTestName)
class TestFinished(testSuite: TestSuiteStarted, simpleTestName: String) : TestState(testSuite, simpleTestName)
}
private val State.testSuite: State.TestSuiteStarted
private inline val State.testSuite: State.TestSuiteStarted
get() = if (this is State.TestSuiteStarted) this else cast<State.TestState>().testSuite
private val BaseTestMessage.functionName: FunctionName get() = testName
private inline val BaseTestMessage.simpleTestName get() = testName