From d6230a2e7bdf5d5ea12c1b778561352f350f4fe2 Mon Sep 17 00:00:00 2001 From: Marat Akhin Date: Fri, 2 Sep 2022 20:21:56 +0200 Subject: [PATCH] [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. --- .../AbstractKlibBinaryCompatibilityTest.kt | 102 +++--- native/native.tests/build.gradle.kts | 1 + .../generators/tests/GenerateNativeTests.kt | 10 + ...stractNativeKlibBinaryCompatibilityTest.kt | 291 ++++++++++++++++++ .../konan/blackboxtest/support/util/Files.kt | 4 +- 5 files changed, 363 insertions(+), 45 deletions(-) create mode 100644 native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/AbstractNativeKlibBinaryCompatibilityTest.kt diff --git a/compiler/tests-common/tests/org/jetbrains/kotlin/compatibility/binary/AbstractKlibBinaryCompatibilityTest.kt b/compiler/tests-common/tests/org/jetbrains/kotlin/compatibility/binary/AbstractKlibBinaryCompatibilityTest.kt index d177f52c68d..8ee0aeafdd7 100644 --- a/compiler/tests-common/tests/org/jetbrains/kotlin/compatibility/binary/AbstractKlibBinaryCompatibilityTest.kt +++ b/compiler/tests-common/tests/org/jetbrains/kotlin/compatibility/binary/AbstractKlibBinaryCompatibilityTest.kt @@ -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 { - 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, friends: List, abiVersions: List) = - 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 { + 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, friends: List, abiVersions: List) = + 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) + } } } diff --git a/native/native.tests/build.gradle.kts b/native/native.tests/build.gradle.kts index 2f072d9cb16..a77abb43777 100644 --- a/native/native.tests/build.gradle.kts +++ b/native/native.tests/build.gradle.kts @@ -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. diff --git a/native/native.tests/tests/org/jetbrains/kotlin/generators/tests/GenerateNativeTests.kt b/native/native.tests/tests/org/jetbrains/kotlin/generators/tests/GenerateNativeTests.kt index b611aa64374..426b2fa24c9 100644 --- a/native/native.tests/tests/org/jetbrains/kotlin/generators/tests/GenerateNativeTests.kt +++ b/native/native.tests/tests/org/jetbrains/kotlin/generators/tests/GenerateNativeTests.kt @@ -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( + suiteTestClassName = "KlibBinaryCompatibilityTestGenerated" + ) { + model("binaryCompatibility/klibEvolution", recursive = false) + } + } } } diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/AbstractNativeKlibBinaryCompatibilityTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/AbstractNativeKlibBinaryCompatibilityTest.kt new file mode 100644 index 00000000000..f3373a00e83 --- /dev/null +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/AbstractNativeKlibBinaryCompatibilityTest.kt @@ -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().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 + ) { + 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() + private val moduleDependencies = mutableMapOf>() + + private fun updateModule( + module: LightDependencyWithVersion, + dependencies: Collection + ) { + 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> + get() { + val res: MutableMap> = 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 = + collectDependencies(listOf(rootDependency), withVersion) + + private fun collectDependencies( + rootDependencies: Collection, + withVersion: Int + ): Collection { + 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 = 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().executionTimeout), + extras = DEFAULT_EXTRAS + ).apply { + initialize(null) + } + + private val buildDir: File + get() = testRunSettings.get().testBuildDir + private val staticCacheRequiredForEveryLibrary: Boolean + get() = testRunSettings.get().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 BasicCompilation.trigger(): TestCompilationResult.Success = + result.assertSuccess() + +private fun union(set: Set, otherSet: Set) = set.union(otherSet) diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/util/Files.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/util/Files.kt index e20bdd6c52e..5d5340cd67c 100644 --- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/util/Files.kt +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/support/util/Files.kt @@ -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()