[K/N][KLIB][Tests]: support for klib binary compatibility tests on K/N

The impl supports both regular and caching testing modes.
Also add Gradle target for klib binary compatibility tests.
This commit is contained in:
Marat Akhin
2022-09-02 20:21:56 +02:00
committed by teamcity
parent 1594dd6d67
commit d6230a2e7b
5 changed files with 363 additions and 45 deletions
@@ -51,54 +51,70 @@ abstract class AbstractKlibBinaryCompatibilityTest : KotlinTestWithEnvironment()
}
}
fun doTest(filePath: String, expectedResult: String) {
val file = File(filePath)
val fileContent = KtTestUtil.doLoadFile(file)
val inputFiles = TestFiles.createTestFiles(
file.name,
fileContent,
object : TestFiles.TestFileFactory<TestModule, TestFile> {
override fun createFile(module: TestModule?, fileName: String, text: String, directives: Directives) =
module?.let {
TestFile(module, fileName, text, directives)
} ?: error("Expected a module for $fileName in $filePath")
override fun createModule(name: String, dependencies: List<String>, friends: List<String>, abiVersions: List<Int>) =
TestModule(name, dependencies, friends)
},
true,
true
)
val modules = inputFiles
.map { it.module }.distinct()
.map { it.name to it }.toMap()
val orderedModules = DFS.topologicalOrder(modules.values) { module ->
module.dependenciesSymbols.mapNotNull { modules[it] }
}
val mainModuleName = DEFAULT_MODULE
val mainModule = modules[mainModuleName] ?: error("No module with name \"$mainModuleName\"")
orderedModules.reversed().filterNot { it === mainModule }.forEach {
produceKlib(it, 1)
if (it.hasVersions) {
produceKlib(it, 2)
}
}
produceProgram(mainModule)
runProgram(mainModule, expectedResult)
}
fun doTest(filePath: String, expectedResult: String) = doTest(
filePath,
expectedResult,
produceKlib = ::produceKlib,
produceAndRunProgram = ::produceAndRunProgram,
)
abstract fun produceKlib(module: TestModule, version: Int)
abstract fun produceProgram(module: TestModule)
abstract fun runProgram(module: TestModule, expectedResult: String)
open fun produceAndRunProgram(module: TestModule, expectedResult: String) {
produceProgram(module)
runProgram(module, expectedResult)
}
companion object {
val TEST_FUNCTION = "box"
val DEFAULT_MODULE = "main"
const val TEST_FUNCTION = "box"
const val DEFAULT_MODULE = "main"
fun doTest(
filePath: String,
expectedResult: String,
produceKlib: (TestModule, Int) -> Unit,
produceAndRunProgram: (TestModule, String) -> Unit,
) {
val file = File(filePath)
val fileContent = KtTestUtil.doLoadFile(file)
val inputFiles = TestFiles.createTestFiles(
file.name,
fileContent,
object : TestFiles.TestFileFactory<TestModule, TestFile> {
override fun createFile(module: TestModule?, fileName: String, text: String, directives: Directives) =
module?.let {
TestFile(module, fileName, text, directives)
} ?: error("Expected a module for $fileName in $filePath")
override fun createModule(name: String, dependencies: List<String>, friends: List<String>, abiVersions: List<Int>) =
TestModule(name, dependencies, friends)
},
true,
true
)
val modules = inputFiles
.map { it.module }.distinct()
.map { it.name to it }.toMap()
val orderedModules = DFS.topologicalOrder(modules.values) { module ->
module.dependenciesSymbols.mapNotNull { modules[it] }
}
val mainModuleName = DEFAULT_MODULE
val mainModule = modules[mainModuleName] ?: error("No module with name \"$mainModuleName\"")
orderedModules.reversed().filterNot { it === mainModule }.forEach {
produceKlib(it, 1)
if (it.hasVersions) {
produceKlib(it, 2)
}
}
produceAndRunProgram(mainModule, expectedResult)
}
}
}
+1
View File
@@ -169,6 +169,7 @@ val codegenBoxTest = nativeTest("codegenBoxTest", "codegen")
val stdlibTest = nativeTest("stdlibTest", "stdlib")
val kotlinTestLibraryTest = nativeTest("kotlinTestLibraryTest", "kotlin-test")
val klibAbiTest = nativeTest("klibAbiTest", "klib-abi")
val klibBinaryCompatibilityTest = nativeTest("klibBinaryCompatibilityTest", "klib-binary-compatibility")
// "test" task is created by convention. We can't just remove it. Let's enable it in developer's environment, so it can be used
// to run any test from IDE or from console, but disable it at TeamCity where it is not supposed to be ever used.
@@ -10,6 +10,7 @@ import org.jetbrains.kotlin.generators.model.annotation
import org.jetbrains.kotlin.konan.blackboxtest.AbstractNativeBlackBoxTest
import org.jetbrains.kotlin.konan.blackboxtest.AbstractNativeCodegenBoxTest
import org.jetbrains.kotlin.konan.blackboxtest.AbstractNativeKlibABITest
import org.jetbrains.kotlin.konan.blackboxtest.AbstractNativeKlibBinaryCompatibilityTest
import org.jetbrains.kotlin.konan.blackboxtest.support.group.UseExtTestCaseGroupProvider
import org.jetbrains.kotlin.konan.blackboxtest.support.group.UseStandardTestCaseGroupProvider
import org.jetbrains.kotlin.test.TargetBackend
@@ -49,6 +50,15 @@ fun main() {
model("klibABI/", pattern = "^([^_](.+))$", recursive = false)
}
}
// KLIB binary compatibility tests.
testGroup("native/native.tests/tests-gen", "compiler/testData") {
testClass<AbstractNativeKlibBinaryCompatibilityTest>(
suiteTestClassName = "KlibBinaryCompatibilityTestGenerated"
) {
model("binaryCompatibility/klibEvolution", recursive = false)
}
}
}
}
@@ -0,0 +1,291 @@
/*
* Copyright 2010-2022 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
import com.intellij.testFramework.TestDataFile
import org.jetbrains.kotlin.compatibility.binary.AbstractKlibBinaryCompatibilityTest
import org.jetbrains.kotlin.konan.blackboxtest.support.*
import org.jetbrains.kotlin.konan.blackboxtest.support.TestFile
import org.jetbrains.kotlin.konan.blackboxtest.support.TestModule
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.*
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationArtifact.KLIB
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationArtifact.KLIBStaticCache
import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationResult.Companion.assertSuccess
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.TestExecutable
import org.jetbrains.kotlin.konan.blackboxtest.support.runner.TestRunChecks
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.CacheMode
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.KotlinNativeTargets
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.SimpleTestDirectories
import org.jetbrains.kotlin.konan.blackboxtest.support.settings.Timeouts
import org.jetbrains.kotlin.konan.blackboxtest.support.util.*
import org.jetbrains.kotlin.utils.DFS
import org.junit.jupiter.api.Tag
import java.io.File
import kotlin.math.max
import org.jetbrains.kotlin.compatibility.binary.TestFile as BinaryCompatibilityTestFile
import org.jetbrains.kotlin.compatibility.binary.TestModule as BinaryCompatibilityTestModule
@Tag("klib-binary-compatibility")
abstract class AbstractNativeKlibBinaryCompatibilityTest : AbstractNativeSimpleTest() {
protected fun runTest(@TestDataFile testPath: String): Unit = AbstractKlibBinaryCompatibilityTest.doTest(
filePath = testPath,
expectedResult = "OK",
produceKlib = ::buildKlib,
produceAndRunProgram = ::buildAndExecuteBinary
)
private fun buildKlib(
binaryCompatibilityTestModule: BinaryCompatibilityTestModule,
version: Int
) {
val module = binaryCompatibilityTestModule.toLightDependencyWithVersion(version)
val moduleDependencies = collectDependencies(
binaryCompatibilityTestModule.dependenciesSymbols,
withVersion = version
)
val klibFile = module.klibFile
val testCase = mkTestCase(module, COMPILER_ARGS_FOR_KLIB)
val compilation = LibraryCompilation(
settings = testRunSettings,
freeCompilerArgs = testCase.freeCompilerArgs,
sourceModules = testCase.modules,
dependencies = moduleDependencies.map { it.klibFile.toKlib().toDependency() },
expectedArtifact = klibFile.toKlib()
)
compilation.trigger()
updateModule(module, moduleDependencies)
}
private fun buildAndExecuteBinary(
mainTestModule: BinaryCompatibilityTestModule,
expectedResult: String
) {
val binaryVersion = moduleVersions.values.maxOrNull() ?: 2
buildKlib(mainTestModule, binaryVersion)
val (binarySourceDir, binaryOutputDir) = listOf(BINARY_SOURCE_DIR_NAME, BINARY_OUTPUT_DIR_NAME).map {
buildDir.resolve(LAUNCHER_MODULE_NAME).resolve(it).apply { mkdirs() }
}
val launcherModule = LightDependencyWithVersion(LAUNCHER_MODULE_NAME, binaryVersion).apply {
module.files += TestFile.createUncommitted(
location = binarySourceDir.resolve(LAUNCHER_FILE_NAME),
module = module,
text = generateBoxFunctionLauncher("box", expectedResult)
)
}
val launcherDependencies = collectDependencies(mainTestModule.name, binaryVersion)
val executableFile = binaryOutputDir.resolve(
"app." + testRunSettings.get<KotlinNativeTargets>().testTarget.family.exeSuffix
)
val cachedDependencies = if (staticCacheRequiredForEveryLibrary) {
latestDependencies.map { (module, deps) ->
buildCacheForKlib(module, deps)
module.klibFile.toStaticCacheArtifact().toDependency()
}
} else {
emptyList()
}
val testCase = mkTestCase(launcherModule, COMPILER_ARGS_FOR_STATIC_CACHE_AND_EXECUTABLE)
val compilation = ExecutableCompilation(
settings = testRunSettings,
freeCompilerArgs = testCase.freeCompilerArgs,
sourceModules = testCase.modules,
extras = testCase.extras,
dependencies = launcherDependencies.map {
it.klibFile.toKlib().toDependency()
} + cachedDependencies,
expectedArtifact = TestCompilationArtifact.Executable(executableFile)
)
val compilationResult = compilation.trigger()
val executable = TestExecutable.fromCompilationResult(testCase, compilationResult)
runExecutableAndVerify(testCase, executable)
}
private fun buildCacheForKlib(
module: LightDependencyWithVersion,
moduleDependencies: Collection<LightDependencyWithVersion>
) {
val klib = module.klibFile
val compilation = StaticCacheCompilation(
settings = testRunSettings,
freeCompilerArgs = COMPILER_ARGS_FOR_STATIC_CACHE_AND_EXECUTABLE,
options = StaticCacheCompilation.Options.Regular,
dependencies = moduleDependencies.map {
it.klibFile.toStaticCacheArtifact().toDependency()
} + klib.toKlib().toDependency(),
expectedArtifact = klib.toStaticCacheArtifact()
)
compilation.trigger()
}
private inner class LightDependencyWithVersion(
val name: String, val version: Int
) {
val module: TestModule.Exclusive = TestModule.Exclusive(
name = name,
directDependencySymbols = emptySet(),
directFriendSymbols = emptySet()
)
val localBuildDir: File =
buildDir.resolveModuleWithVersion(name, version).apply { mkdirs() }
val klibFile: File =
buildDir.resolveKlibFileWithVersion(name, version).apply { mkdirs() }
//region Poor person's inner data class
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as LightDependencyWithVersion
if (name != other.name) return false
if (version != other.version) return false
return true
}
override fun hashCode(): Int {
var result = name.hashCode()
result = 31 * result + version.hashCode()
return result
}
override fun toString(): String = "LightDependencyWithVersion($name:$version)"
//endregion
}
private val moduleVersions = mutableMapOf<String, Int>()
private val moduleDependencies = mutableMapOf<String, Set<String>>()
private fun updateModule(
module: LightDependencyWithVersion,
dependencies: Collection<LightDependencyWithVersion>
) {
moduleVersions[module.name] = max(
moduleVersions.getOrDefault(module.name, 0),
module.version
)
moduleDependencies[module.name] = union(
moduleDependencies.getOrElse(module.name) { emptySet() },
dependencies.mapToSet { it.name }
)
}
private val latestDependencies: Map<LightDependencyWithVersion, Collection<LightDependencyWithVersion>>
get() {
val res: MutableMap<LightDependencyWithVersion, Collection<LightDependencyWithVersion>> = mutableMapOf()
for ((name, version) in moduleVersions) {
val module = LightDependencyWithVersion(name, version)
val symbols = moduleDependencies[name] ?: continue
res[module] = symbols.map { LightDependencyWithVersion(it, moduleVersions[it]!!) }
}
return res
}
private fun collectDependencies(
rootDependency: String,
withVersion: Int
): Collection<LightDependencyWithVersion> =
collectDependencies(listOf(rootDependency), withVersion)
private fun collectDependencies(
rootDependencies: Collection<String>,
withVersion: Int
): Collection<LightDependencyWithVersion> {
fun findLightDependency(dependency: String): LightDependencyWithVersion? {
val latestVersion = moduleVersions[dependency] ?: return null
return LightDependencyWithVersion(dependency, withVersion.coerceAtMost(latestVersion))
}
return DFS.topologicalOrder(rootDependencies.mapNotNull(::findLightDependency)) { module ->
val moduleDependencies = moduleDependencies[module.name] ?: return@topologicalOrder emptyList()
moduleDependencies.mapNotNull { name -> findLightDependency(name) }
}
}
private fun BinaryCompatibilityTestFile.toUncommittedIn(
extra: LightDependencyWithVersion
): TestFile<TestModule.Exclusive> = TestFile.createUncommitted(
location = extra.localBuildDir.resolve(name),
module = extra.module,
text = content
)
private fun BinaryCompatibilityTestModule.toLightDependencyWithVersion(
version: Int
): LightDependencyWithVersion {
val extra = LightDependencyWithVersion(name, version)
versionFiles(version).forEach { extra.module.files += it.toUncommittedIn(extra) }
return extra
}
private fun mkTestCase(
extra: LightDependencyWithVersion,
compilerArgs: TestCompilerArgs
): TestCase = TestCase(
id = TestCaseId.Named(extra.name),
kind = TestKind.STANDALONE,
modules = setOf(extra.module),
freeCompilerArgs = compilerArgs,
nominalPackageName = PackageName.EMPTY,
checks = TestRunChecks.Default(testRunSettings.get<Timeouts>().executionTimeout),
extras = DEFAULT_EXTRAS
).apply {
initialize(null)
}
private val buildDir: File
get() = testRunSettings.get<SimpleTestDirectories>().testBuildDir
private val staticCacheRequiredForEveryLibrary: Boolean
get() = testRunSettings.get<CacheMode>().staticCacheRequiredForEveryLibrary
companion object {
private val COMPILER_ARGS_FOR_KLIB = TestCompilerArgs.EMPTY
private val COMPILER_ARGS_FOR_STATIC_CACHE_AND_EXECUTABLE = TestCompilerArgs.EMPTY
private val DEFAULT_EXTRAS = TestCase.WithTestRunnerExtras(TestRunnerType.DEFAULT)
private const val BINARY_SOURCE_DIR_NAME = "sources"
private const val BINARY_OUTPUT_DIR_NAME = "outputs"
}
}
private fun File.resolveModuleWithVersion(moduleName: String, version: Int): File =
resolve(moduleName).resolve("$version")
private fun File.resolveKlibFileWithVersion(moduleName: String, version: Int): File =
resolveModuleWithVersion(moduleName, version).resolve("${moduleName}.klib")
private fun File.toKlib(): KLIB = KLIB(this)
private fun File.toStaticCacheArtifact() = KLIBStaticCache(
cacheDir = parentFile.resolve(STATIC_CACHE_DIR_NAME).apply { mkdirs() },
klib = KLIB(this)
)
private fun KLIB.toDependency() =
ExistingDependency(this, TestCompilationDependencyType.Library)
private fun KLIBStaticCache.toDependency() =
ExistingDependency(this, TestCompilationDependencyType.LibraryStaticCache)
private fun <T : TestCompilationArtifact> BasicCompilation<T>.trigger(): TestCompilationResult.Success<out T> =
result.assertSuccess()
private fun <T> union(set: Set<T>, otherSet: Set<T>) = set.union(otherSet)
@@ -32,12 +32,12 @@ internal fun computeGeneratedSourcesDir(testDataBaseDir: File, testDataFile: Fil
.resolve(testDataFile.nameWithoutExtension)
}
internal fun generateBoxFunctionLauncher(entryPointFunctionFQN: String): String =
internal fun generateBoxFunctionLauncher(entryPointFunctionFQN: String, expectedResult: String = "OK"): String =
"""
@kotlin.test.Test
fun runTest() {
val result = @Suppress("OPT_IN_USAGE_ERROR") $entryPointFunctionFQN()
kotlin.test.assertEquals("OK", result, "Test failed with: ${'$'}result")
kotlin.test.assertEquals("$expectedResult", result, "Test failed with: ${'$'}result")
}
""".trimIndent()