diff --git a/gradle/versions.properties b/gradle/versions.properties index 10cb4161ea9..228ad4c3204 100644 --- a/gradle/versions.properties +++ b/gradle/versions.properties @@ -46,6 +46,7 @@ versions.kotlinx-coroutines-core-jvm=1.5.0 versions.kotlinx-coroutines-core=1.5.0 versions.kotlinx-metadata-jvm=0.6.0 +versions.kotlinx-metadata-klib=0.0.1-dev-10 versions.kotlinx-serialization-json=1.3.3 versions.ktor-network=1.4.0 versions.ktor-utils=1.4.0 diff --git a/native/native.tests/build.gradle.kts b/native/native.tests/build.gradle.kts index 56aae3aa06c..215c0ea81dc 100644 --- a/native/native.tests/build.gradle.kts +++ b/native/native.tests/build.gradle.kts @@ -21,6 +21,7 @@ dependencies { testImplementation(project(":native:kotlin-native-utils")) testImplementation(project(":native:executors")) testApiJUnit5() + testImplementation(commonDependency("org.jetbrains.kotlinx", "kotlinx-metadata-klib")) testImplementation(commonDependency("org.jetbrains.kotlinx", "kotlinx-coroutines-core")) { isTransitive = false } testRuntimeOnly(commonDependency("org.jetbrains.intellij.deps:trove4j")) diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/KT59030WorkaroundTest.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/KT59030WorkaroundTest.kt new file mode 100644 index 00000000000..a26f1aebee9 --- /dev/null +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/KT59030WorkaroundTest.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2010-2023 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 kotlinx.metadata.KmAnnotation +import kotlinx.metadata.KmAnnotationArgument +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmDeclarationContainer +import kotlinx.metadata.klib.KlibModuleMetadata +import kotlinx.metadata.klib.annotations +import org.jetbrains.kotlin.backend.common.serialization.* +import org.jetbrains.kotlin.backend.common.serialization.proto.* +import org.jetbrains.kotlin.backend.common.serialization.proto.IrDeclaration.DeclaratorCase.* +import org.jetbrains.kotlin.konan.blackboxtest.support.EnforcedHostTarget +import org.jetbrains.kotlin.konan.blackboxtest.support.TestCase +import org.jetbrains.kotlin.konan.blackboxtest.support.TestCompilerArgs +import org.jetbrains.kotlin.konan.blackboxtest.support.TestKind +import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationArtifact +import org.jetbrains.kotlin.konan.blackboxtest.support.compilation.TestCompilationResult.Companion.assertSuccess +import org.jetbrains.kotlin.konan.blackboxtest.support.group.UsePartialLinkage +import org.jetbrains.kotlin.konan.blackboxtest.support.settings.CacheMode +import org.jetbrains.kotlin.konan.blackboxtest.support.settings.CacheMode.WithStaticCache +import org.jetbrains.kotlin.konan.file.unzipTo +import org.jetbrains.kotlin.konan.file.zipDirAs +import org.jetbrains.kotlin.library.* +import org.jetbrains.kotlin.library.impl.KotlinLibraryLayoutForWriter +import org.jetbrains.kotlin.library.impl.MetadataWriterImpl +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import java.io.File +import org.jetbrains.kotlin.konan.file.File as KFile + +// See KT-59030. +@Tag("partial-linkage") +@EnforcedHostTarget +@UsePartialLinkage(UsePartialLinkage.Mode.ENABLED_WITH_ERROR) +class KT59030WorkaroundTest : AbstractNativeSimpleTest() { + // This test relies on static caches. So, run it along with other PL tests but only when caches are enabled. + @BeforeEach + fun assumeOnlyStaticCacheEverywhere() { + val cacheMode = testRunSettings.get() + assumeTrue(cacheMode is WithStaticCache) + assumeTrue(cacheMode.useStaticCacheForUserLibraries) + } + + @Test + fun kt59030() { + val library = cinteropToLibrary( + targets = targets, + defFile = File(DEF_FILE_PATH), + outputDir = buildDir, + freeCompilerArgs = TestCompilerArgs.EMPTY + ).assertSuccess().resultingArtifact + spoilDeprecatedAnnotationsInLibrary(library) + + compileToExecutable( + generateTestCaseWithSingleFile( + sourceFile = File(MAIN_FILE_PATH), + testKind = TestKind.STANDALONE_NO_TR, + extras = TestCase.NoTestRunnerExtras("main") + ), + library.asLibraryDependency() + ).assertSuccess() + } + + private fun spoilDeprecatedAnnotationsInLibrary(klib: TestCompilationArtifact.KLIB) { + // Make a backup. + val oldLibraryFile = KFile(with(klib.klibFile) { parentFile.newDir("__backup__").resolve(name).path }) + val newLibraryFile = KFile(klib.klibFile.path) + newLibraryFile.renameTo(oldLibraryFile) + + // Unzip the new library. + val newLibraryTmpDir = KFile(newLibraryFile.path + ".tmp") + oldLibraryFile.unzipTo(newLibraryTmpDir) + + // Read the library. + val oldLibrary = resolveSingleFileKlib(oldLibraryFile, strategy = ToolingSingleFileKlibResolveStrategy) + val newLibraryLayout = KotlinLibraryLayoutForWriter(newLibraryFile, newLibraryTmpDir) + + // Patch the library. + spoilDeprecatedAnnotationsInMetadata(oldLibrary, newLibraryLayout) + + // Zip and clean-up. + newLibraryTmpDir.zipDirAs(newLibraryFile) + newLibraryTmpDir.deleteRecursively() + } + + companion object { + private const val TEST_DATA_DIR = "kotlin-native/backend.native/tests/interop/basics" + const val DEF_FILE_PATH = "${TEST_DATA_DIR}/cvectors.def" + const val MAIN_FILE_PATH = "${TEST_DATA_DIR}/vectors.kt" + + private const val DEPRECATED_CLASS_NAME = "kotlin/Deprecated" + private const val REPLACE_WITH_ARG = "replaceWith" + private const val EXPRESSION_ARG = "expression" + + private fun File.newDir(name: String): File = resolve(name).apply { mkdirs() } + + private fun spoilDeprecatedAnnotationsInMetadata( + oldLibrary: KotlinLibrary, + newLibraryLayout: KotlinLibraryLayoutForWriter, + ) { + // Read the metadata. + val moduleMetadata = KlibModuleMetadata.read( + object : KlibModuleMetadata.MetadataLibraryProvider { + override val moduleHeaderData get() = oldLibrary.moduleHeaderData + override fun packageMetadataParts(fqName: String) = oldLibrary.packageMetadataParts(fqName) + override fun packageMetadata(fqName: String, partName: String) = oldLibrary.packageMetadata(fqName, partName) + } + ) + + // Patch the metadata. + moduleMetadata.fragments.forEach { fragment -> + fragment.pkg?.let(this::spoilDeprecatedAnnotationsInMetadataContainer) + fragment.classes.forEach(this::spoilDeprecatedAnnotationsInMetadataClass) + } + + // Write back the metadata. + val serializedMetadata = with(moduleMetadata.write()) { + SerializedMetadata(module = header, fragments, fragmentNames) + } + + newLibraryLayout.metadataDir.deleteRecursively() // Drop old metadata. + MetadataWriterImpl(newLibraryLayout).addMetadata(serializedMetadata) // Write new metadata. + } + + private fun spoilDeprecatedAnnotationsInMetadataContainer(container: KmDeclarationContainer) { + container.functions.forEach { spoilDeprecatedAnnotationsInMetadataAnnotationList(it.annotations) } + container.properties.forEach { spoilDeprecatedAnnotationsInMetadataAnnotationList(it.annotations) } + container.typeAliases.forEach { spoilDeprecatedAnnotationsInMetadataAnnotationList(it.annotations) } + } + + private fun spoilDeprecatedAnnotationsInMetadataClass(clazz: KmClass) { + spoilDeprecatedAnnotationsInMetadataAnnotationList(clazz.annotations) + clazz.constructors.forEach { spoilDeprecatedAnnotationsInMetadataAnnotationList(it.annotations) } + spoilDeprecatedAnnotationsInMetadataContainer(clazz) + } + + private fun spoilDeprecatedAnnotationsInMetadataAnnotationList(annotations: MutableList) { + annotations.replaceAll { annotation -> + if (annotation.className == DEPRECATED_CLASS_NAME) spoilDeprecatedAnnotationInMetadata(annotation) else annotation + } + } + + private fun spoilDeprecatedAnnotationInMetadata(deprecated: KmAnnotation): KmAnnotation = + deprecated.copy( + arguments = deprecated.arguments.mapValues { (argName, argValue) -> + if (argName == REPLACE_WITH_ARG) spoilReplaceWithAnnotationInMetadata(argValue.unwrap()).wrap() else argValue + } + ) + + private fun spoilReplaceWithAnnotationInMetadata(replaceWith: KmAnnotation): KmAnnotation = + replaceWith.copy( + arguments = replaceWith.arguments.filterKeys { argName -> argName != EXPRESSION_ARG } + ) + + private fun KmAnnotationArgument<*>.unwrap(): KmAnnotation = (this as KmAnnotationArgument.AnnotationValue).value + private fun KmAnnotation.wrap(): KmAnnotationArgument.AnnotationValue = KmAnnotationArgument.AnnotationValue(this) + } +} diff --git a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/NativeSimpleTestUtils.kt b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/NativeSimpleTestUtils.kt index 3d0fe884d7b..861587e3a5e 100644 --- a/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/NativeSimpleTestUtils.kt +++ b/native/native.tests/tests/org/jetbrains/kotlin/konan/blackboxtest/NativeSimpleTestUtils.kt @@ -20,8 +20,6 @@ import org.jetbrains.kotlin.test.InTextDirectivesUtils import org.junit.jupiter.api.Assumptions import java.io.File -private val DEFAULT_EXTRAS = TestCase.WithTestRunnerExtras(TestRunnerType.DEFAULT) - internal abstract class ArtifactBuilder( val test: AbstractNativeSimpleTest, val rootDir: File, @@ -100,6 +98,7 @@ internal class ExecutableBuilder( } internal val AbstractNativeSimpleTest.buildDir: File get() = testRunSettings.get().testBinariesDir +internal val AbstractNativeSimpleTest.targets: KotlinNativeTargets get() = testRunSettings.get() internal fun TestCompilationArtifact.KLIB.asLibraryDependency() = ExistingDependency(this, TestCompilationDependencyType.Library) @@ -130,6 +129,22 @@ internal fun AbstractNativeSimpleTest.compileToLibrary( return compilationResult.resultingArtifact } +internal fun AbstractNativeSimpleTest.cinteropToLibrary( + targets: KotlinNativeTargets, + defFile: File, + outputDir: File, + freeCompilerArgs: TestCompilerArgs +): TestCompilationResult { + val testCase: TestCase = generateCInteropTestCaseFromSingleDefFile(defFile, freeCompilerArgs) + return CInteropCompilation( + classLoader = testRunSettings.get(), + targets = targets, + freeCompilerArgs = freeCompilerArgs, + defFile = testCase.modules.single().files.single().location, + expectedArtifact = getLibraryArtifact(testCase, outputDir) + ).result +} + internal class CompiledExecutable( val testCase: TestCase, val compilationResult: TestCompilationResult.Success @@ -191,7 +206,51 @@ internal fun AbstractNativeSimpleTest.generateTestCaseWithSingleModule( freeCompilerArgs = freeCompilerArgs, nominalPackageName = PackageName.EMPTY, checks = TestRunChecks.Default(testRunSettings.get().executionTimeout), - extras = DEFAULT_EXTRAS + extras = TestCase.WithTestRunnerExtras(TestRunnerType.DEFAULT) + ).apply { + initialize(null, null) + } +} + +internal fun AbstractNativeSimpleTest.generateTestCaseWithSingleFile( + sourceFile: File, + freeCompilerArgs: TestCompilerArgs = TestCompilerArgs.EMPTY, + testKind: TestKind = TestKind.STANDALONE, + extras: TestCase.Extras = TestCase.WithTestRunnerExtras(TestRunnerType.DEFAULT) +): TestCase { + val moduleName: String = sourceFile.name ?: LAUNCHER_MODULE_NAME + val module = TestModule.Exclusive(moduleName, emptySet(), emptySet(), emptySet()) + module.files += TestFile.createCommitted(sourceFile, module) + + return TestCase( + id = TestCaseId.Named(moduleName), + kind = testKind, + modules = setOf(module), + freeCompilerArgs = freeCompilerArgs, + nominalPackageName = PackageName.EMPTY, + checks = TestRunChecks.Default(testRunSettings.get().executionTimeout), + extras = extras + ).apply { + initialize(null, null) + } +} + +internal fun AbstractNativeSimpleTest.generateCInteropTestCaseFromSingleDefFile( + defFile: File, + freeCompilerArgs: TestCompilerArgs, +): TestCase { + val moduleName: String = defFile.name + val module = TestModule.Exclusive(moduleName, emptySet(), emptySet(), emptySet()) + module.files += TestFile.createCommitted(defFile, module) + + return TestCase( + id = TestCaseId.Named(moduleName), + kind = TestKind.STANDALONE, + modules = setOf(module), + freeCompilerArgs = freeCompilerArgs, + nominalPackageName = PackageName.EMPTY, + checks = TestRunChecks.Default(testRunSettings.get().executionTimeout), + extras = TestCase.WithTestRunnerExtras(TestRunnerType.DEFAULT) ).apply { initialize(null, null) } @@ -220,7 +279,7 @@ private fun AbstractNativeSimpleTest.compileToExecutable( settings = testRunSettings, freeCompilerArgs = testCase.freeCompilerArgs, sourceModules = testCase.modules, - extras = DEFAULT_EXTRAS, + extras = testCase.extras, dependencies = dependencies, expectedArtifact = getExecutableArtifact() )