[Native][tests] Rework PackageName entity

This commit is contained in:
Dmitriy Dolovov
2021-12-03 02:04:18 +03:00
parent a98d1d2b11
commit 7abfe4d47b
15 changed files with 112 additions and 76 deletions
@@ -7,10 +7,11 @@ package org.jetbrains.kotlin.konan.blackboxtest
import com.intellij.testFramework.TestDataFile
import org.jetbrains.kotlin.konan.blackboxtest.support.*
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TreeNode
import org.jetbrains.kotlin.konan.blackboxtest.support.util.getAbsoluteFile
import org.jetbrains.kotlin.konan.blackboxtest.support.util.joinPackageNames
import org.jetbrains.kotlin.konan.blackboxtest.support.util.prependPackageName
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageName
import org.jetbrains.kotlin.konan.blackboxtest.support.TestCaseId
import org.jetbrains.kotlin.konan.blackboxtest.support.TestRun
import org.jetbrains.kotlin.konan.blackboxtest.support.TestRunProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.util.*
import org.junit.jupiter.api.DynamicNode
import org.junit.jupiter.api.DynamicTest.dynamicTest
import org.junit.jupiter.api.extension.ExtendWith
@@ -89,7 +90,7 @@ abstract class AbstractNativeBlackBoxTest {
// }
// }
buildList {
fun TreeNode<TestRun>.processItems(parentPackageSegment: String) {
fun TreeNode<TestRun>.processItems(parentPackageSegment: PackageName) {
val ownPackageSegment = joinPackageNames(parentPackageSegment, packageSegment)
items.mapTo(this@buildList) { testRun ->
val displayName = testRun.displayName.prependPackageName(ownPackageSegment)
@@ -99,7 +100,7 @@ abstract class AbstractNativeBlackBoxTest {
children.forEach { it.processItems(ownPackageSegment) }
}
testRunNodes.forEach { testRunNode -> testRunNode.processItems("") }
testRunNodes.forEach { testRunNode -> testRunNode.processItems(PackageName.EMPTY) }
}
private fun performTestRun(testRun: TestRun) {
@@ -5,6 +5,7 @@
package org.jetbrains.kotlin.konan.blackboxtest
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageName
import org.jetbrains.kotlin.konan.blackboxtest.support.util.compressedName
import org.jetbrains.kotlin.konan.blackboxtest.support.util.compressedPackageName
import org.jetbrains.kotlin.konan.blackboxtest.support.util.compressedSimpleName
@@ -90,4 +91,9 @@ class TestInfra_CompressedNamesTest {
"foo.bar.baz.foo.bar.baz.foo.bar.baz.foo.bar.baz.foo.bar.baz".compressedPackageName
)
}
companion object {
private val String.compressedPackageName
get() = PackageName(this).compressedPackageName
}
}
@@ -0,0 +1,24 @@
/*
* 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
/**
* Represents a package name.
*/
internal class PackageName private constructor(private val fqn: String, val segments: List<String>) {
constructor(segments: List<String>) : this(segments.joinToString("."), segments)
constructor(fqn: String) : this(fqn, if (fqn.isNotEmpty()) fqn.split('.') else emptyList())
fun isEmpty(): Boolean = segments.isEmpty()
override fun toString() = fqn
override fun equals(other: Any?) = fqn == (other as? PackageName)?.fqn
override fun hashCode() = fqn.hashCode()
companion object {
val EMPTY = PackageName("", emptyList())
}
}
@@ -15,13 +15,12 @@ import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.test.services.JUnit5Assertions.fail
import java.io.File
internal typealias PackageFQN = String
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: PackageFQN, val functionName: FunctionName)
internal data class TestFunction(val packageName: PackageName, val functionName: FunctionName)
/**
* Represents a single file that will be supplied to the compiler.
@@ -179,7 +178,7 @@ internal class TestCase(
val kind: TestKind,
val modules: Set<TestModule.Exclusive>,
val freeCompilerArgs: TestCompilerArgs,
val nominalPackageName: PackageFQN,
val nominalPackageName: PackageName,
val expectedOutputDataFile: File?,
val extras: Extras
) {
@@ -113,7 +113,7 @@ internal class TestCompilationFactory(private val settings: Settings) {
private fun multiModuleArtifactFile(modules: Collection<TestModule>, extension: String): File {
var filesCount = 0
var hash = 0
val uniquePackageNames = hashSetOf<PackageFQN>()
val uniquePackageNames = hashSetOf<PackageName>()
modules.forEach { module ->
module.files.forEach { file ->
@@ -132,7 +132,7 @@ internal class TestCompilationFactory(private val settings: Settings) {
repeat(4 - prefix.length) { append('0') }
append(prefix).append('-')
if (commonPackageName != null)
if (!commonPackageName.isEmpty())
append(commonPackageName.compressedPackageName).append('-')
append(prettyHash(hash))
@@ -143,7 +143,7 @@ internal class TestCompilationFactory(private val settings: Settings) {
return artifactDirForPackageName(commonPackageName).resolve(artifactFileName)
}
private fun artifactDirForPackageName(packageName: PackageFQN?): File {
private fun artifactDirForPackageName(packageName: PackageName?): File {
val baseDir = settings.get<Binaries>().testBinariesDir
val outputDir = if (packageName != null) baseDir.resolve(packageName.compressedPackageName) else baseDir
@@ -5,7 +5,8 @@
package org.jetbrains.kotlin.konan.blackboxtest.support
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import org.jetbrains.kotlin.konan.blackboxtest.support.util.TestName
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertFalse
import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull
import java.io.File
@@ -28,9 +29,9 @@ internal sealed interface TestRunParameter {
abstract fun testMatches(testName: String): Boolean
}
class WithPackageFilter(packageName: PackageFQN) : WithFilter() {
class WithPackageFilter(packageName: PackageName) : WithFilter() {
init {
assertTrue(packageName.isNotEmpty())
assertFalse(packageName.isEmpty())
}
private val packagePrefix = "$packageName."
@@ -39,11 +40,11 @@ internal sealed interface TestRunParameter {
programArgs += "--ktest_filter=$packagePrefix*"
}
override fun testMatches(testName: String) = testName.startsWith(packagePrefix)
override fun testMatches(testName: TestName) = testName.startsWith(packagePrefix)
}
class WithFunctionFilter(val testFunction: TestFunction) : WithFilter() {
private val packagePrefix = if (testFunction.packageName.isNotEmpty()) "${testFunction.packageName}." else ""
private val packagePrefix = if (testFunction.packageName.isEmpty()) "" else "${testFunction.packageName}."
override fun applyTo(programArgs: MutableList<String>) {
programArgs += "--ktest_regex_filter=${packagePrefix.replace(".", "\\.")}([^\\.]+)\\.${testFunction.functionName}"
@@ -16,10 +16,10 @@ import org.jetbrains.kotlin.konan.blackboxtest.support.runner.LocalTestFunctionE
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.*
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
@@ -199,7 +199,7 @@ internal class TestRunProvider(
private fun Collection<TestFunction>.filterIrrelevant(testCase: TestCase) =
if (testCase.kind == TestKind.REGULAR)
filter { testFunction -> testFunction.packageName.isSameOrSubpackageOf(testCase.nominalPackageName) }
filter { testFunction -> testFunction.packageName.startsWith(testCase.nominalPackageName) }
else
this
@@ -279,7 +279,7 @@ private class ExtTestDataFile(
private fun patchPackageNames(isStandaloneTest: Boolean) = with(structure) {
if (isStandaloneTest) return // Don't patch packages for standalone tests.
val basePackageName = FqName(testDataFileSettings.nominalPackageName)
val basePackageName = FqName(testDataFileSettings.nominalPackageName.toString())
val oldPackageNames: Set<FqName> = filesToTransform.mapToSet { it.packageFqName }
val oldToNewPackageNameMapping: Map<FqName, FqName> = oldPackageNames.associateWith { oldPackageName ->
@@ -650,7 +650,7 @@ private class ExtTestDataFileSettings(
val optInsForCompiler: Set<String>,
val expectActualLinker: Boolean,
val generatedSourcesDir: File,
val nominalPackageName: PackageFQN
val nominalPackageName: PackageName
)
private typealias SharedModuleGenerator = (sharedModulesDir: File) -> TestModule.Shared?
@@ -58,7 +58,7 @@ internal class PredefinedTestCaseGroupProvider(annotation: PredefinedTestCases)
modules = setOf(module),
freeCompilerArgs = predefinedTestCase.freeCompilerArgs
.parseCompilerArgs { "Failed to parse free compiler arguments for test case $testCaseId" },
nominalPackageName = testCaseId.uniqueName,
nominalPackageName = PackageName(testCaseId.uniqueName),
expectedOutputDataFile = null,
extras = WithTestRunnerExtras
)
@@ -221,7 +221,7 @@ internal class StandardTestCaseGroupProvider(private val settings: Settings) : T
}
companion object {
private fun fixPackageNames(testModules: Collection<TestModule.Exclusive>, basePackageName: PackageFQN, testDataFile: File) {
private fun fixPackageNames(testModules: Collection<TestModule.Exclusive>, basePackageName: PackageName, testDataFile: File) {
testModules.forEach { testModule ->
testModule.files.forEach { testFile ->
val firstMeaningfulLine = testFile.text.dropNonMeaningfulLines().firstOrNull()
@@ -230,7 +230,7 @@ internal class StandardTestCaseGroupProvider(private val settings: Settings) : T
val existingPackageName = firstMeaningfulLine?.getExistingPackageName()
if (existingPackageName != null) {
// Validate it.
assertTrue(existingPackageName.isSameOrSubpackageOf(basePackageName)) {
assertTrue(existingPackageName.startsWith(basePackageName)) {
val location = Location(testDataFile, firstMeaningfulLine.number)
"""
$location: Invalid package name declaration found: $firstMeaningfulLine
@@ -6,7 +6,7 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.util
import org.apache.commons.lang.StringUtils.splitByCharacterTypeCamelCase
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageFQN
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageName
import org.jetbrains.kotlin.konan.target.Architecture
import org.jetbrains.kotlin.konan.target.Family
import org.jetbrains.kotlin.konan.target.KonanTarget
@@ -48,9 +48,9 @@ internal val Architecture.compressedName: String
internal val Class<*>.compressedSimpleName: String
get() = splitByCharacterTypeCamelCase(simpleName).joinToString("") { it.take(3) }
internal val PackageFQN.compressedPackageName: String
internal val PackageName.compressedPackageName: String
get() {
val sanitizedName = replace('.', '_')
val sanitizedName = segments.joinToString("_")
return if (sanitizedName.length > COMPRESSED_PACKAGE_FQN_MAX_LENGTH) {
val suffix = "-" + prettyHash(sanitizedName.hashCode())
sanitizedName.substring(0, COMPRESSED_PACKAGE_FQN_MAX_LENGTH - suffix.length) + suffix
@@ -5,15 +5,14 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.util
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageFQN
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageName
import org.jetbrains.kotlin.renderer.KeywordStringsGenerated.KEYWORDS
import org.jetbrains.kotlin.test.services.JUnit5Assertions.assertTrue
import java.io.File
import java.nio.file.Path
import kotlin.io.path.name
import kotlin.math.min
internal fun computePackageName(testDataBaseDir: File, testDataFile: File): PackageFQN {
internal fun computePackageName(testDataBaseDir: File, testDataFile: File): PackageName {
assertTrue(testDataFile.startsWith(testDataBaseDir)) {
"""
The file is outside of the directory.
@@ -26,13 +25,13 @@ internal fun computePackageName(testDataBaseDir: File, testDataFile: File): Pack
.relativeTo(testDataBaseDir)
.resolve(testDataFile.nameWithoutExtension)
.toPath()
.map(Path::name)
.joinToString(".") { packagePart ->
if (packagePart in KEYWORDS)
"_${packagePart}_"
.map { pathSegment ->
val packageSegment = pathSegment.name
if (packageSegment in KEYWORDS)
"_${packageSegment}_"
else
buildString {
packagePart.forEachIndexed { index, ch ->
packageSegment.forEachIndexed { index, ch ->
if (ch.isJavaIdentifierPart()) {
if (index == 0 && !ch.isJavaIdentifierStart()) {
// If the first character is not suitable for start, escape it with '_'.
@@ -46,36 +45,42 @@ internal fun computePackageName(testDataBaseDir: File, testDataFile: File): Pack
}
}
}
.let(::PackageName)
}
internal fun Set<PackageFQN>.findCommonPackageName(): PackageFQN? = when (size) {
0 -> null
internal fun Set<PackageName>.findCommonPackageName(): PackageName = when (size) {
0 -> PackageName.EMPTY
1 -> first()
else -> map { packageName: PackageFQN ->
packageName.split('.')
}.reduce { commonPackageNameParts: List<String>, packageNameParts: List<String> ->
ArrayList<String>(min(commonPackageNameParts.size, packageNameParts.size)).apply {
val i = commonPackageNameParts.iterator()
val j = packageNameParts.iterator()
else -> map { packageName -> packageName.segments }
.reduce { commonSegments: List<String>, segments: List<String> ->
buildList(min(commonSegments.size, segments.size)) {
val i = commonSegments.iterator()
val j = segments.iterator()
while (i.hasNext() && j.hasNext()) {
val packageNamePart = i.next()
if (packageNamePart == j.next()) add(packageNamePart) else break
while (i.hasNext() && j.hasNext()) {
val segment = i.next()
if (segment == j.next()) add(segment) else break
}
}
}
}.takeIf { it.isNotEmpty() }?.joinToString(".")
.let(::PackageName)
}
internal fun joinPackageNames(a: PackageFQN, b: PackageFQN): PackageFQN = when {
internal fun joinPackageNames(a: PackageName, b: PackageName): PackageName = when {
a.isEmpty() -> b
b.isEmpty() -> a
else -> "$a.$b"
else -> PackageName(a.segments + b.segments)
}
internal fun String.prependPackageName(packageName: PackageFQN): String =
internal fun String.prependPackageName(packageName: PackageName): 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] == '.')
internal fun PackageName.startsWith(other: PackageName): Boolean {
if (segments.size < other.segments.size) return false
for (i in other.segments.indices) {
if (segments[i] != other.segments[i]) return false
}
return true
}
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.util
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageFQN
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageName
internal typealias SourceText = CharSequence
internal typealias SourceLine = String
@@ -17,10 +17,11 @@ internal data class NumberedSourceLine(val number: Int, val text: SourceLine) :
internal fun SourceText.hasAnythingButComments(): Boolean =
dropNonMeaningfulLines().firstOrNull() != null
internal fun NumberedSourceLine.getExistingPackageName(): PackageFQN? =
internal fun NumberedSourceLine.getExistingPackageName(): PackageName? =
text.substringAfter("package ", missingDelimiterValue = "")
.trimStart()
.takeIf(String::isNotEmpty)
?.let(::PackageName)
internal fun SourceText.dropNonMeaningfulLines(): Sequence<NumberedSourceLine> {
var inMultilineComment = false
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.util
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageFQN
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageName
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
@@ -48,14 +48,12 @@ internal fun parseGTestListing(rawGTestListing: String): Collection<TestFunction
}
else -> when (state) {
is State.Begin, is State.NewTest -> {
val packageParts = line.trimEnd().removeSuffix(".").split('.')
if (packageParts.isEmpty()) parseError("Malformed test suite name")
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?
val packageName = packageParts.dropLast(1).joinToString(".")
State.NewTestSuite(packageName)
State.NewTestSuite(PackageName(packageSegments.dropLast(1)))
}
else -> parseError("Unexpected test suite name")
}
@@ -66,11 +64,11 @@ internal fun parseGTestListing(rawGTestListing: String): Collection<TestFunction
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
class NewTestSuite(override val packageName: PackageName) : State, HasPackageName
class NewTest(override val packageName: PackageName) : State, HasPackageName
interface HasPackageName {
val packageName: PackageFQN
val packageName: PackageName
}
}
@@ -5,10 +5,10 @@
package org.jetbrains.kotlin.konan.blackboxtest.support.util
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageFQN
import org.jetbrains.kotlin.konan.blackboxtest.support.PackageName
internal interface TreeNode<T> {
val packageSegment: String
val packageSegment: PackageName
val items: Collection<T>
val children: Collection<TreeNode<T>>
@@ -16,15 +16,15 @@ internal interface TreeNode<T> {
fun <T> oneLevel(vararg items: T) = oneLevel(listOf(*items))
fun <T> oneLevel(items: Iterable<T>): List<TreeNode<T>> = listOf(object : TreeNode<T> {
override val packageSegment get() = ""
override val packageSegment get() = PackageName.EMPTY
override val items = items.toList()
override val children get() = emptyList<TreeNode<T>>()
})
}
}
internal fun <T, R> Collection<T>.buildTree(extractPackageName: (T) -> PackageFQN, transform: (T) -> R): Collection<TreeNode<R>> {
val groupedItems: Map<PackageFQN, List<R>> = groupBy(extractPackageName).mapValues { (_, items) -> items.map(transform) }
internal fun <T, R> Collection<T>.buildTree(extractPackageName: (T) -> PackageName, transform: (T) -> R): Collection<TreeNode<R>> {
val groupedItems: Map<PackageName, List<R>> = groupBy(extractPackageName).mapValues { (_, items) -> items.map(transform) }
// Fast pass.
when (groupedItems.size) {
@@ -33,13 +33,14 @@ internal fun <T, R> Collection<T>.buildTree(extractPackageName: (T) -> PackageFQ
}
// Long pass.
val root = TreeBuilder<R>("")
val root = TreeBuilder<R>(PackageName.EMPTY)
// Populate the tree.
groupedItems.forEach { (packageFQN, items) ->
groupedItems.forEach { (packageName, items) ->
var node = root
packageFQN.split('.').forEach { packageSegment ->
node = node.childrenMap.computeIfAbsent(packageSegment) { TreeBuilder(packageSegment) }
packageName.segments.forEach { packageSegment ->
val packageSegmentAsName = PackageName(listOf(packageSegment))
node = node.childrenMap.computeIfAbsent(packageSegmentAsName) { TreeBuilder(packageSegmentAsName) }
}
node.items += items
}
@@ -54,9 +55,9 @@ internal fun <T, R> Collection<T>.buildTree(extractPackageName: (T) -> PackageFQ
meaningfulNode.childrenMap.values
}
private class TreeBuilder<T>(override var packageSegment: String) : TreeNode<T> {
private class TreeBuilder<T>(override var packageSegment: PackageName) : TreeNode<T> {
override val items = mutableListOf<T>()
val childrenMap = hashMapOf<String, TreeBuilder<T>>()
val childrenMap = hashMapOf<PackageName, TreeBuilder<T>>()
override val children: Collection<TreeBuilder<T>> get() = childrenMap.values
}