Add JVM ABI K1/K2 consistency tests

This commit is contained in:
vladislav.grechko
2023-12-26 10:18:19 +00:00
committed by Space Team
parent 5a1fb78fcd
commit 34bac48541
1104 changed files with 88413 additions and 66 deletions
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.test
import org.jetbrains.kotlin.analyzer.common.CommonPlatformAnalyzerServices
import org.jetbrains.kotlin.config.CommonConfigurationKeys.USE_FIR
import org.jetbrains.kotlin.js.resolve.JsPlatformAnalyzerServices
import org.jetbrains.kotlin.platform.TargetPlatform
import org.jetbrains.kotlin.platform.isCommon
@@ -16,6 +17,9 @@ import org.jetbrains.kotlin.platform.konan.isNative
import org.jetbrains.kotlin.resolve.PlatformDependentAnalyzerServices
import org.jetbrains.kotlin.resolve.jvm.platform.JvmPlatformAnalyzerServices
import org.jetbrains.kotlin.resolve.konan.platform.NativePlatformAnalyzerServices
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.compilerConfigurationProvider
import org.jetbrains.kotlin.wasm.resolve.WasmJsPlatformAnalyzerServices
import java.io.File
@@ -44,4 +48,12 @@ fun TargetPlatform.getAnalyzerServices(): PlatformDependentAnalyzerServices {
isWasm() -> WasmJsPlatformAnalyzerServices
else -> error("Unknown target platform: $this")
}
}
fun <T> runWithEnablingFirUseOption(testServices: TestServices, module: TestModule, lambda: () -> T): T {
val compilerConfiguration = testServices.compilerConfigurationProvider.getCompilerConfiguration(module)
compilerConfiguration.put(USE_FIR, true)
val result = lambda()
compilerConfiguration.put(USE_FIR, false)
return result
}
@@ -10,6 +10,7 @@ import org.jetbrains.kotlin.test.directives.CodegenTestDirectives
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives
import org.jetbrains.kotlin.test.directives.model.DirectivesContainer
import org.jetbrains.kotlin.test.directives.model.ValueDirective
import org.jetbrains.kotlin.test.directives.tryRetrieveIgnoredInliner
import org.jetbrains.kotlin.test.model.AfterAnalysisChecker
import org.jetbrains.kotlin.test.model.FrontendKinds
import org.jetbrains.kotlin.test.services.TestServices
@@ -43,17 +44,11 @@ class BlackBoxInlinerCodegenSuppressor(testServices: TestServices) : AfterAnalys
private fun suppressForTargetFrontend(
failedAssertions: List<WrappedException>,
directive: ValueDirective<TargetInliner>
directive: ValueDirective<TargetInliner>,
): List<WrappedException>? {
val directiveName = directive.name
val ignoreDirectives = testServices.moduleStructure.allDirectives[directive]
if (ignoreDirectives.size > 1) {
throw IllegalArgumentException("Directive $directiveName should contains only one value")
}
val ignoreDirective = ignoreDirectives.singleOrNull()
val ignoreDirective = testServices.tryRetrieveIgnoredInliner(directive)
val enabledIrInliner = LanguageSettingsDirectives.ENABLE_JVM_IR_INLINER in testServices.moduleStructure.allDirectives
val unmuteError = listOf(AssertionError("Looks like this test can be unmuted. Please remove $directiveName directive.").wrap())
val unmuteError = listOf(AssertionError("Looks like this test can be unmuted. Please remove ${directive.name} directive.").wrap())
if (ignoreDirective == TargetInliner.IR && enabledIrInliner || ignoreDirective == TargetInliner.BYTECODE && !enabledIrInliner) {
return if (failedAssertions.isNotEmpty()) emptyList() else unmuteError
@@ -0,0 +1,193 @@
/*
* 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.test.backend.handlers
import com.google.common.collect.Sets
import org.jetbrains.kotlin.abicmp.defects.Location
import org.jetbrains.kotlin.abicmp.reports.*
import org.jetbrains.kotlin.abicmp.tag
import org.jetbrains.kotlin.abicmp.tasks.ClassTask
import org.jetbrains.kotlin.abicmp.tasks.checkerConfiguration
import org.jetbrains.kotlin.codegen.getClassFiles
import org.jetbrains.kotlin.test.backend.ir.AbiCheckerSuppressor
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives
import org.jetbrains.kotlin.test.model.*
import org.jetbrains.kotlin.test.services.JUnit5Assertions
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.moduleStructure
import org.jetbrains.kotlin.test.utils.withExtension
import org.jetbrains.org.objectweb.asm.ClassReader
import org.jetbrains.org.objectweb.asm.tree.ClassNode
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintWriter
class JvmAbiConsistencyHandler(testServices: TestServices) : AnalysisHandler<BinaryArtifacts.JvmFromK1AndK2>(testServices, true, true) {
override val artifactKind: TestArtifactKind<BinaryArtifacts.JvmFromK1AndK2>
get() = ArtifactKinds.JvmFromK1AndK2
private class ModuleReport(
val missingInK1: Set<String>,
val missingInK2: Set<String>,
val nonEmptyClassReports: MutableList<ClassReport>,
)
private class TestReport {
private val modulesWithNonEmptyReports = mutableMapOf<String, ModuleReport>()
fun addModuleReport(moduleName: String, report: ModuleReport) {
modulesWithNonEmptyReports[moduleName] = report
}
fun isEmpty(): Boolean = modulesWithNonEmptyReports.isEmpty()
fun dumpAsPlainText(): String {
fun TextTreeBuilderContext.reportMissing(frontendName: String, missing: Set<String>) {
if (missing.isEmpty()) return
node("Missing in $frontendName") {
node(missing.joinToString("\n"))
}
}
val outputStream = ByteArrayOutputStream()
val out = PrintWriter(outputStream)
dumpTree(out) {
modulesWithNonEmptyReports.forEach { (module, report) ->
node("MODULE $module") {
reportMissing("K1", report.missingInK1)
reportMissing("K2", report.missingInK2)
report.nonEmptyClassReports.forEach { classReport ->
with(classReport) { appendClassReport() }
}
}
}
}
return outputStream.toString()
}
// Might be used while debugging
@Suppress("unused")
fun dumpAsHtml(): String {
fun PrintWriter.reportMissing(frontendName: String, missing: Set<String>) {
if (missing.isEmpty()) return
println("Missing in $frontendName:")
tag("br")
println(missing.joinToString())
tag("br")
tag("br")
}
val outputStream = ByteArrayOutputStream()
PrintWriter(outputStream, true).use { out ->
out.tag("html") {
out.tag("head") {
out.tag("style", REPORT_CSS)
}
out.tag("body") {
modulesWithNonEmptyReports.forEach { (module, report) ->
out.tag("h2", "Module: $module")
out.reportMissing("K1", report.missingInK1)
out.reportMissing("K2", report.missingInK2)
report.nonEmptyClassReports.forEach { it.writeAsHtml(out) }
}
}
}
}
return outputStream.toString()
}
}
private val testReport = TestReport()
override fun processAfterAllModules(someAssertionWasFailed: Boolean) {
val txtDiffPath = testServices.moduleStructure.originalTestDataFiles.first().withExtension(".jvm_abi.txt")
val isDifferenceExplained = testServices.moduleStructure.allDirectives.contains(CodegenTestDirectives.JVM_ABI_K1_K2_DIFF)
if (!testReport.isEmpty()) {
JUnit5Assertions.assertEqualsToFile(
txtDiffPath,
testReport.dumpAsPlainText(),
sanitizer = { it },
differenceObtainedMessage = { "Actual K1/K2 JVM ABI difference differs from expected" },
fileNotFoundMessageTeamCity = fileNotFoundMessageBuilder(isTeamCityVersion = true, isDifferenceExplained),
fileNotFoundMessageLocal = fileNotFoundMessageBuilder(isTeamCityVersion = false, isDifferenceExplained)
)
if (!isDifferenceExplained) {
fail(
"K1/K2 JVM ABI difference obtained. Add ${CodegenTestDirectives.JVM_ABI_K1_K2_DIFF.name} directive with an explanation"
)
}
} else {
if (isDifferenceExplained && txtDiffPath.exists()) {
fail(
"No K1/K2 JVM ABI difference found. Remove ${CodegenTestDirectives.JVM_ABI_K1_K2_DIFF.name} directive and $txtDiffPath"
)
}
if (isDifferenceExplained) {
fail("No K1/K2 JVM ABI difference found. Remove ${CodegenTestDirectives.JVM_ABI_K1_K2_DIFF.name} directive")
}
if (txtDiffPath.exists()) {
fail("No K1/K2 JVM ABI difference found. Remove $txtDiffPath")
}
}
}
private fun fileNotFoundMessageBuilder(isTeamCityVersion: Boolean, isDiffExplained: Boolean): ((File) -> String) = {
buildString {
if (isTeamCityVersion) {
append("Expected data file did not exist `$it`")
} else {
append("Expected data file did not exist. Generating: $it")
}
if (!isDiffExplained) {
append("\n")
append("Also add ${CodegenTestDirectives.JVM_ABI_K1_K2_DIFF.name} directive with an explanation")
}
}
}
private fun fail(message: String) {
JUnit5Assertions.fail { message }
}
override fun processModule(module: TestModule, info: BinaryArtifacts.JvmFromK1AndK2) {
if (AbiCheckerSuppressor.ignoredByBackendOrInliner(testServices)) return
val classesFromK1 = info.fromK1.classFileFactory.getClassFiles().associate { it.relativePath to it.asByteArray() }
val classesFromK2 = info.fromK2.classFileFactory.getClassFiles().associate { it.relativePath to it.asByteArray() }
val missingInK2 = Sets.difference(classesFromK1.keys, classesFromK2.keys)
val missingInK1 = Sets.difference(classesFromK2.keys, classesFromK1.keys)
val commonClasses = Sets.intersection(classesFromK1.keys, classesFromK2.keys)
val nonEmptyClassReports = mutableListOf<ClassReport>()
commonClasses.forEach { classInternalName ->
val k1ClassNode = parseClassNode(classesFromK1[classInternalName]!!)
val k2ClassNode = parseClassNode(classesFromK2[classInternalName]!!)
val classReport = ClassReport(Location.Class("", classInternalName), classInternalName, "K1", "K2", DefectReport())
ClassTask(checkerConfiguration {
// Indication of constants is different in K2 as expected
// We simply turn the checker off to avoid producing an enormous number of difference reports
disable("class.metadata.property.hasConstant")
}, k1ClassNode, k2ClassNode, classReport).run()
if (classReport.isNotEmpty()) {
nonEmptyClassReports.add(classReport)
}
}
if (nonEmptyClassReports.isNotEmpty() || missingInK1.isNotEmpty() || missingInK2.isNotEmpty()) {
testReport.addModuleReport(module.name, ModuleReport(missingInK1, missingInK2, nonEmptyClassReports))
}
}
private fun parseClassNode(byteArray: ByteArray) =
ClassNode().also { ClassReader(ByteArrayInputStream(byteArray)).accept(it, ClassReader.SKIP_CODE) }
}
@@ -0,0 +1,60 @@
/*
* 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.test.backend.ir
import org.jetbrains.kotlin.test.WrappedException
import org.jetbrains.kotlin.test.backend.BlackBoxCodegenSuppressor
import org.jetbrains.kotlin.test.backend.TargetInliner
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives.IGNORE_BACKEND
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives.IGNORE_BACKEND_K1
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives.IGNORE_BACKEND_K2
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives.IGNORE_INLINER
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives.IGNORE_INLINER_K1
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives.IGNORE_INLINER_K2
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives.JVM_ABI_K1_K2_DIFF
import org.jetbrains.kotlin.test.directives.tryRetrieveIgnoredInliner
import org.jetbrains.kotlin.test.model.AfterAnalysisChecker
import org.jetbrains.kotlin.test.services.ServiceRegistrationData
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.moduleStructure
import org.jetbrains.kotlin.test.services.service
import org.jetbrains.kotlin.utils.bind
class AbiCheckerSuppressor(testServices: TestServices) : AfterAnalysisChecker(testServices) {
companion object {
fun ignoredByBackendOrInliner(testServices: TestServices) = testServices.ignoredByBackend || testServices.ignoredByInliner
}
override val additionalServices: List<ServiceRegistrationData>
get() = listOf(service(BlackBoxCodegenSuppressor::SuppressionChecker.bind(null)))
override fun suppressIfNeeded(failedAssertions: List<WrappedException>): List<WrappedException> {
val isDifferenceExplained = testServices.moduleStructure.allDirectives.contains(JVM_ABI_K1_K2_DIFF)
if (ignoredByBackendOrInliner(testServices)) {
if (isDifferenceExplained) {
return listOf(
AssertionError(
"No K1/K2 JVM ABI comparison done for this test. Please remove ${JVM_ABI_K1_K2_DIFF.name} directive."
).wrap()
)
}
return emptyList()
}
return failedAssertions
}
}
private val TestServices.suppressionChecker: BlackBoxCodegenSuppressor.SuppressionChecker by TestServices.testServiceAccessor()
private val TestServices.ignoredByBackend: Boolean
get() = listOf(IGNORE_BACKEND, IGNORE_BACKEND_K1, IGNORE_BACKEND_K2)
.any { suppressionChecker.failuresInModuleAreIgnored(moduleStructure.modules.first(), it).testMuted }
private val TestServices.ignoredByInliner: Boolean
get() = listOf(IGNORE_INLINER, IGNORE_INLINER_K1, IGNORE_INLINER_K2).any { tryRetrieveIgnoredInliner(it) == TargetInliner.BYTECODE }
@@ -0,0 +1,20 @@
/*
* 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.test.backend.ir
import org.jetbrains.kotlin.test.model.BackendKind
import org.jetbrains.kotlin.test.model.BackendKinds
import org.jetbrains.kotlin.test.model.ResultingArtifact
class IrBackendInputsFromK1AndK2(
val fromK1: IrBackendInput,
val fromK2: IrBackendInput,
) : ResultingArtifact.BackendInput<IrBackendInputsFromK1AndK2>() {
override val kind: BackendKind<IrBackendInputsFromK1AndK2>
get() = BackendKinds.IrBackendForK1AndK2
}
@@ -0,0 +1,32 @@
/*
* 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.test.backend.ir
import org.jetbrains.kotlin.test.model.*
import org.jetbrains.kotlin.test.runWithEnablingFirUseOption
import org.jetbrains.kotlin.test.services.TestServices
class K1AndK2JvmIrBackendFacade(testServices: TestServices) :
BackendFacade<IrBackendInputsFromK1AndK2, BinaryArtifacts.JvmFromK1AndK2>(
testServices,
BackendKinds.IrBackendForK1AndK2,
ArtifactKinds.JvmFromK1AndK2
) {
private val backendForClassicFrontend = JvmIrBackendFacade(testServices)
private val backendForFir = JvmIrBackendFacade(testServices)
override fun transform(module: TestModule, inputArtifact: IrBackendInputsFromK1AndK2): BinaryArtifacts.JvmFromK1AndK2? {
val fromClassicFrontend = backendForClassicFrontend.transform(module, inputArtifact.fromK1) ?: return null
val fromFir =
runWithEnablingFirUseOption(testServices, module) { backendForFir.transform(module, inputArtifact.fromK2) } ?: return null
return BinaryArtifacts.JvmFromK1AndK2(fromClassicFrontend, fromFir)
}
override fun shouldRunAnalysis(module: TestModule): Boolean {
return module.backendKind == BackendKinds.IrBackend && module.binaryKind == ArtifactKinds.JvmFromK1AndK2
}
}
@@ -14,6 +14,7 @@ import org.jetbrains.kotlin.test.builders.CompilerStepsNames.DESERIALIZED_IR_HAN
import org.jetbrains.kotlin.test.builders.CompilerStepsNames.FIR_HANDLERS_STEP_NAME
import org.jetbrains.kotlin.test.builders.CompilerStepsNames.JS_ARTIFACTS_HANDLERS_STEP_NAME
import org.jetbrains.kotlin.test.builders.CompilerStepsNames.JVM_ARTIFACTS_HANDLERS_STEP_NAME
import org.jetbrains.kotlin.test.builders.CompilerStepsNames.JVM_FROM_K1_AND_K2_ARTIFACTS_HANDLERS_STEP_NAME
import org.jetbrains.kotlin.test.builders.CompilerStepsNames.KLIB_ARTIFACTS_HANDLERS_STEP_NAME
import org.jetbrains.kotlin.test.builders.CompilerStepsNames.RAW_IR_HANDLERS_STEP_NAME
import org.jetbrains.kotlin.test.builders.CompilerStepsNames.WASM_ARTIFACTS_HANDLERS_STEP_NAME
@@ -43,6 +44,7 @@ object CompilerStepsNames {
const val JS_ARTIFACTS_HANDLERS_STEP_NAME = "js artifacts handlers"
const val WASM_ARTIFACTS_HANDLERS_STEP_NAME = "wasm artifacts handlers"
const val KLIB_ARTIFACTS_HANDLERS_STEP_NAME = "klib artifacts handlers"
const val JVM_FROM_K1_AND_K2_ARTIFACTS_HANDLERS_STEP_NAME = "jvm from K1 and K2 artifacts handlers"
}
@@ -109,6 +111,12 @@ inline fun TestConfigurationBuilder.jvmArtifactsHandlersStep(
namedHandlersStep(JVM_ARTIFACTS_HANDLERS_STEP_NAME, ArtifactKinds.Jvm, init)
}
inline fun TestConfigurationBuilder.jvmFromK1AndK2ArtifactsHandlersStep(
init: HandlersStepBuilder<BinaryArtifacts.JvmFromK1AndK2, ArtifactKinds.JvmFromK1AndK2>.() -> Unit = {},
) {
namedHandlersStep(JVM_FROM_K1_AND_K2_ARTIFACTS_HANDLERS_STEP_NAME, ArtifactKinds.JvmFromK1AndK2, init)
}
inline fun TestConfigurationBuilder.jsArtifactsHandlersStep(
init: HandlersStepBuilder<BinaryArtifacts.Js, ArtifactKinds.Js>.() -> Unit = {}
) {
@@ -175,3 +183,9 @@ inline fun TestConfigurationBuilder.configureKlibArtifactsHandlersStep(
) {
configureNamedHandlersStep(KLIB_ARTIFACTS_HANDLERS_STEP_NAME, ArtifactKinds.KLib, init)
}
inline fun TestConfigurationBuilder.configureJvmFromK1AndK2ArtifactHandlerStep(
init: HandlersStepBuilder<BinaryArtifacts.JvmFromK1AndK2, ArtifactKinds.JvmFromK1AndK2>.() -> Unit,
) {
configureNamedHandlersStep(JVM_FROM_K1_AND_K2_ARTIFACTS_HANDLERS_STEP_NAME, ArtifactKinds.JvmFromK1AndK2, init)
}
@@ -16,6 +16,8 @@ import org.jetbrains.kotlin.test.directives.model.SimpleDirectivesContainer
import org.jetbrains.kotlin.test.directives.model.ValueDirective
import org.jetbrains.kotlin.test.model.FrontendKinds
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.moduleStructure
object CodegenTestDirectives : SimpleDirectivesContainer() {
val IGNORE_BACKEND by enumDirective<TargetBackend>(
@@ -255,12 +257,17 @@ object CodegenTestDirectives : SimpleDirectivesContainer() {
Suppresses test if $ENABLE_IR_FAKE_OVERRIDE_GENERATION directive enabled
""".trimIndent()
)
val JVM_ABI_K1_K2_DIFF by stringDirective(
description = "Expect difference in JVM ABI between K1 and K2",
applicability = Global
)
}
fun extractIgnoredDirectiveForTargetBackend(
module: TestModule,
targetBackend: TargetBackend,
customIgnoreDirective: ValueDirective<TargetBackend>? = null
customIgnoreDirective: ValueDirective<TargetBackend>? = null,
): ValueDirective<TargetBackend>? =
when (module.frontendKind) {
FrontendKinds.ClassicFrontend -> CodegenTestDirectives.IGNORE_BACKEND_K1
@@ -280,3 +287,12 @@ fun extractIgnoredDirectiveForTargetBackend(
}
}
}
fun TestServices.tryRetrieveIgnoredInliner(directive: ValueDirective<TargetInliner>): TargetInliner? {
val directiveName = directive.name
val ignoreDirectives = moduleStructure.allDirectives[directive]
if (ignoreDirectives.size > 1) {
throw IllegalArgumentException("Directive $directiveName should contains only one value")
}
return ignoreDirectives.singleOrNull()
}
@@ -0,0 +1,36 @@
/*
* 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.test.frontend
import org.jetbrains.kotlin.test.frontend.classic.ClassicFrontendFacade
import org.jetbrains.kotlin.test.frontend.fir.FirFrontendFacade
import org.jetbrains.kotlin.test.model.FrontendFacade
import org.jetbrains.kotlin.test.model.FrontendKinds
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.runWithEnablingFirUseOption
import org.jetbrains.kotlin.test.services.ServiceRegistrationData
import org.jetbrains.kotlin.test.services.TestServices
import org.jetbrains.kotlin.test.services.jvm.compiledClassesManager
class K1AndK2FrontendFacade(
testServices: TestServices,
) : FrontendFacade<K1AndK2OutputArtifact>(testServices, FrontendKinds.ClassicAndFIR) {
override val additionalServices: List<ServiceRegistrationData>
get() = classicFrontendFacade.additionalServices + firFrontendFacade.additionalServices
private val classicFrontendFacade: ClassicFrontendFacade = ClassicFrontendFacade(testServices)
private val firFrontendFacade: FirFrontendFacade = FirFrontendFacade(testServices)
override fun analyze(module: TestModule): K1AndK2OutputArtifact {
testServices.compiledClassesManager.specifiedFrontendKind = FrontendKinds.ClassicFrontend
val k1Artifact = classicFrontendFacade.analyze(module)
testServices.compiledClassesManager.specifiedFrontendKind = FrontendKinds.FIR
val k2Artifact = runWithEnablingFirUseOption(testServices, module) { firFrontendFacade.analyze(module) }
return K1AndK2OutputArtifact(k1Artifact, k2Artifact)
}
}
@@ -0,0 +1,18 @@
/*
* 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.test.frontend
import org.jetbrains.kotlin.test.frontend.classic.ClassicFrontendOutputArtifact
import org.jetbrains.kotlin.test.frontend.fir.FirOutputArtifact
import org.jetbrains.kotlin.test.model.FrontendKind
import org.jetbrains.kotlin.test.model.FrontendKinds
import org.jetbrains.kotlin.test.model.ResultingArtifact
class K1AndK2OutputArtifact(val k1Artifact: ClassicFrontendOutputArtifact, val k2Artifact: FirOutputArtifact) :
ResultingArtifact.FrontendOutput<K1AndK2OutputArtifact>() {
override val kind: FrontendKind<K1AndK2OutputArtifact>
get() = FrontendKinds.ClassicAndFIR
}
@@ -0,0 +1,38 @@
/*
* 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.test.frontend
import org.jetbrains.kotlin.test.backend.ir.IrBackendInputsFromK1AndK2
import org.jetbrains.kotlin.test.frontend.classic.ClassicFrontend2IrConverter
import org.jetbrains.kotlin.test.frontend.fir.Fir2IrJvmResultsConverter
import org.jetbrains.kotlin.test.model.BackendKinds
import org.jetbrains.kotlin.test.model.Frontend2BackendConverter
import org.jetbrains.kotlin.test.model.FrontendKinds
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.runWithEnablingFirUseOption
import org.jetbrains.kotlin.test.services.TestServices
class K1AndK2ToIrConverter(testServices: TestServices) :
Frontend2BackendConverter<K1AndK2OutputArtifact, IrBackendInputsFromK1AndK2>(
testServices,
FrontendKinds.ClassicAndFIR,
BackendKinds.IrBackendForK1AndK2
) {
private val classicFrontend2IrConverter = ClassicFrontend2IrConverter(testServices)
private val fir2IrConverter = Fir2IrJvmResultsConverter(testServices)
override fun transform(module: TestModule, inputArtifact: K1AndK2OutputArtifact): IrBackendInputsFromK1AndK2? {
val irFromClassic = classicFrontend2IrConverter.transform(module, inputArtifact.k1Artifact)
val irFromFir =
runWithEnablingFirUseOption(testServices, module) { fir2IrConverter.transform(module, inputArtifact.k2Artifact) } ?: return null
return IrBackendInputsFromK1AndK2(irFromClassic, irFromFir)
}
override fun shouldRunAnalysis(module: TestModule): Boolean {
return module.backendKind == BackendKinds.IrBackend
}
}
@@ -11,7 +11,6 @@ import com.intellij.openapi.project.Project
import org.jetbrains.kotlin.container.StorageComponentContainer
import org.jetbrains.kotlin.container.useInstance
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.descriptors.impl.ModuleDescriptorImpl
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.resolve.CompilerEnvironment
import org.jetbrains.kotlin.resolve.ModulePath
@@ -86,7 +85,11 @@ internal class MultiplatformSeparateAnalysisConfiguration(
if (dependencies.isEmpty()) return
for (dependency in dependencies) {
val dependencyModule = dependencyProvider.getTestModule(dependency.moduleName)
val artifact = dependencyProvider.getArtifact(dependencyModule, FrontendKinds.ClassicFrontend)
val artifact = if (module.frontendKind == FrontendKinds.ClassicAndFIR) {
dependencyProvider.getArtifact(dependencyModule, FrontendKinds.ClassicAndFIR).k1Artifact
} else {
dependencyProvider.getArtifact(dependencyModule, FrontendKinds.ClassicFrontend)
}
/*
We need create KtFiles again with new project because otherwise we can access to some caches using
old project as key which may leads to missing services in core environment
@@ -25,6 +25,11 @@ object BinaryArtifacts {
get() = ArtifactKinds.Jvm
}
class JvmFromK1AndK2(val fromK1: Jvm, val fromK2: Jvm) : ResultingArtifact.Binary<JvmFromK1AndK2>() {
override val kind: BinaryKind<JvmFromK1AndK2>
get() = ArtifactKinds.JvmFromK1AndK2
}
sealed class Js : ResultingArtifact.Binary<Js>() {
abstract val outputFile: File
override val kind: BinaryKind<Js>
@@ -8,17 +8,21 @@ package org.jetbrains.kotlin.test.model
import org.jetbrains.kotlin.test.TargetBackend
import org.jetbrains.kotlin.test.backend.classic.ClassicBackendInput
import org.jetbrains.kotlin.test.backend.ir.IrBackendInput
import org.jetbrains.kotlin.test.backend.ir.IrBackendInputsFromK1AndK2
import org.jetbrains.kotlin.test.frontend.K1AndK2OutputArtifact
import org.jetbrains.kotlin.test.frontend.classic.ClassicFrontendOutputArtifact
import org.jetbrains.kotlin.test.frontend.fir.FirOutputArtifact
object FrontendKinds {
object ClassicFrontend : FrontendKind<ClassicFrontendOutputArtifact>("ClassicFrontend")
object FIR : FrontendKind<FirOutputArtifact>("FIR")
object ClassicAndFIR : FrontendKind<K1AndK2OutputArtifact>("ClassicAndFIR")
fun fromString(string: String): FrontendKind<*>? {
return when (string) {
"ClassicFrontend" -> ClassicFrontend
"FIR" -> FIR
"ClassicAndFIR" -> ClassicAndFIR
else -> null
}
}
@@ -35,6 +39,11 @@ object BackendKinds {
*/
object IrBackend : BackendKind<IrBackendInput>("IrBackend")
/**
* The artifact kind representing two IRs, generated by K1 and K2 frontends respectively
*/
object IrBackendForK1AndK2 : BackendKind<IrBackendInputsFromK1AndK2>("TwoIrBackends")
/**
* The artifact kind representing IR deserialized from a klib.
*/
@@ -58,6 +67,7 @@ object BackendKinds {
object ArtifactKinds {
object Jvm : BinaryKind<BinaryArtifacts.Jvm>("JVM")
object JvmFromK1AndK2 : BinaryKind<BinaryArtifacts.JvmFromK1AndK2>("JvmFromK1AndK2")
object Js : BinaryKind<BinaryArtifacts.Js>("JS")
object Native : BinaryKind<BinaryArtifacts.Native>("Native")
object Wasm : BinaryKind<BinaryArtifacts.Wasm>("Wasm")
@@ -70,6 +80,7 @@ object ArtifactKinds {
"Native" -> Native
"Wasm" -> Wasm
"KLib" -> KLib
"JvmFromK1AndK2" -> JvmFromK1AndK2
else -> null
}
}
@@ -0,0 +1,89 @@
/*
* 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.test.runners
import org.jetbrains.kotlin.config.JvmTarget
import org.jetbrains.kotlin.test.FirParser
import org.jetbrains.kotlin.test.TargetBackend
import org.jetbrains.kotlin.test.TestJdkKind
import org.jetbrains.kotlin.test.backend.handlers.JvmAbiConsistencyHandler
import org.jetbrains.kotlin.test.backend.ir.AbiCheckerSuppressor
import org.jetbrains.kotlin.test.backend.ir.K1AndK2JvmIrBackendFacade
import org.jetbrains.kotlin.test.builders.TestConfigurationBuilder
import org.jetbrains.kotlin.test.builders.configureJvmFromK1AndK2ArtifactHandlerStep
import org.jetbrains.kotlin.test.builders.jvmFromK1AndK2ArtifactsHandlersStep
import org.jetbrains.kotlin.test.directives.*
import org.jetbrains.kotlin.test.frontend.K1AndK2ToIrConverter
import org.jetbrains.kotlin.test.frontend.K1AndK2FrontendFacade
import org.jetbrains.kotlin.test.model.FrontendKinds
import org.jetbrains.kotlin.test.runners.codegen.commonServicesConfigurationForCodegenAndDebugTest
import org.jetbrains.kotlin.test.services.configuration.JavaForeignAnnotationType
import org.jetbrains.kotlin.test.services.configuration.JvmForeignAnnotationsConfigurator
open class AbstractJvmAbiConsistencyTest :
AbstractKotlinCompilerWithTargetBackendTest(TargetBackend.JVM_IR),
RunnerWithTargetBackendForTestGeneratorMarker {
override fun TestConfigurationBuilder.configuration() {
commonServicesConfigurationForCodegenAndDebugTest(FrontendKinds.ClassicAndFIR)
defaultDirectives {
FirDiagnosticsDirectives.FIR_PARSER with FirParser.Psi
}
forTestsMatching("compiler/testData/codegen/bytecodeText/*") {
defaultDirectives {
+ConfigurationDirectives.WITH_STDLIB
+JvmEnvironmentConfigurationDirectives.WITH_REFLECT
}
}
forTestsMatching("compiler/testData/codegen/boxModernJdk/*") {
defaultDirectives {
+ConfigurationDirectives.WITH_STDLIB
+CodegenTestDirectives.USE_JAVAC_BASED_ON_JVM_TARGET
+CodegenTestDirectives.IGNORE_DEXING
}
}
forTestsMatching("compiler/testData/codegen/boxModernJdk/testsWithJava11/*") {
defaultDirectives {
JvmEnvironmentConfigurationDirectives.JDK_KIND with TestJdkKind.FULL_JDK_11
JvmEnvironmentConfigurationDirectives.JVM_TARGET with JvmTarget.JVM_11
}
}
forTestsMatching("compiler/testData/codegen/boxModernJdk/testsWithJava17/*") {
defaultDirectives {
JvmEnvironmentConfigurationDirectives.JDK_KIND with TestJdkKind.FULL_JDK_17
JvmEnvironmentConfigurationDirectives.JVM_TARGET with JvmTarget.JVM_17
}
}
forTestsMatching("compiler/testData/codegen/box/javaInterop/foreignAnnotationsTests/tests/*") {
defaultDirectives {
ForeignAnnotationsDirectives.ANNOTATIONS_PATH with JavaForeignAnnotationType.Annotations
}
useConfigurators(::JvmForeignAnnotationsConfigurator)
}
useAfterAnalysisCheckers(
::AbiCheckerSuppressor
)
facadeStep(::K1AndK2FrontendFacade)
facadeStep(::K1AndK2ToIrConverter)
facadeStep(::K1AndK2JvmIrBackendFacade)
jvmFromK1AndK2ArtifactsHandlersStep {}
configureJvmFromK1AndK2ArtifactHandlerStep {
useHandlers(::JvmAbiConsistencyHandler)
}
}
}
@@ -44,7 +44,7 @@ fun <F : ResultingArtifact.FrontendOutput<F>, B : ResultingArtifact.BackendInput
jvmArtifactsHandlersStep(init = {})
}
private fun TestConfigurationBuilder.commonServicesConfigurationForCodegenAndDebugTest(targetFrontend: FrontendKind<*>) {
fun TestConfigurationBuilder.commonServicesConfigurationForCodegenAndDebugTest(targetFrontend: FrontendKind<*>) {
globalDefaults {
frontend = targetFrontend
targetPlatform = JvmPlatforms.defaultJvmPlatform
@@ -18,22 +18,39 @@ object JUnit5Assertions : AssertionsService() {
val isTeamCityBuild: Boolean = System.getenv("TEAMCITY_VERSION") != null
override fun assertEqualsToFile(expectedFile: File, actual: String, sanitizer: (String) -> String, message: () -> String) {
assertEqualsToFile(
expectedFile,
actual,
sanitizer,
differenceObtainedMessage = message,
fileNotFoundMessageTeamCity = { "Expected data file did not exist `$expectedFile`" },
fileNotFoundMessageLocal = { "Expected data file did not exist. Generating: $expectedFile" })
}
fun assertEqualsToFile(
expectedFile: File,
actual: String,
sanitizer: (String) -> String,
differenceObtainedMessage: () -> String,
fileNotFoundMessageTeamCity: (File) -> String,
fileNotFoundMessageLocal: (File) -> String,
) {
try {
val actualText = actual.trim { it <= ' ' }.convertLineSeparators().trimTrailingWhitespacesAndAddNewlineAtEOF()
if (!expectedFile.exists()) {
if (isTeamCityBuild) {
org.junit.jupiter.api.fail("Expected data file did not exist `$expectedFile`")
org.junit.jupiter.api.fail(fileNotFoundMessageTeamCity(expectedFile))
} else {
expectedFile.parentFile.mkdirs()
expectedFile.writeText(actualText)
org.junit.jupiter.api.fail("Expected data file did not exist. Generating: $expectedFile")
org.junit.jupiter.api.fail(fileNotFoundMessageLocal(expectedFile))
}
}
val expected = expectedFile.readText().convertLineSeparators()
val expectedText = expected.trim { it <= ' ' }.trimTrailingWhitespacesAndAddNewlineAtEOF()
if (sanitizer.invoke(expectedText) != sanitizer.invoke(actualText)) {
throw FileComparisonFailure(
"${message()}: ${expectedFile.name}",
"${differenceObtainedMessage()}: ${expectedFile.name}",
expected, actual, expectedFile.absolutePath
)
}
@@ -349,7 +349,7 @@ class ModuleStructureExtractorImpl(
targetBackend = targetBackend,
frontendKind = currentModuleFrontendKind ?: defaultsProvider.defaultFrontend,
backendKind = BackendKinds.fromTargetBackend(targetBackend),
binaryKind = defaultsProvider.defaultArtifactKind ?: targetPlatform.toArtifactKind(),
binaryKind = defaultsProvider.defaultArtifactKind ?: targetPlatform.toArtifactKind(frontendKind),
files = filesOfCurrentModule,
allDependencies = dependenciesOfCurrentModule,
directives = moduleDirectives,
@@ -11,10 +11,7 @@ import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.platform.konan.NativePlatforms
import org.jetbrains.kotlin.test.directives.model.ComposedRegisteredDirectives
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.model.BinaryKind
import org.jetbrains.kotlin.test.model.ArtifactKinds
import org.jetbrains.kotlin.test.model.DependencyKind
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.model.*
import org.jetbrains.kotlin.test.services.TestModuleStructure
import java.io.File
@@ -52,11 +49,14 @@ class TestModuleStructureImpl(
}
companion object {
fun TargetPlatform.toArtifactKind(): BinaryKind<*> = when (this) {
in JvmPlatforms.allJvmPlatforms -> ArtifactKinds.Jvm
in JsPlatforms.allJsPlatforms -> ArtifactKinds.Js
in NativePlatforms.allNativePlatforms -> ArtifactKinds.Native
else -> BinaryKind.NoArtifact
fun TargetPlatform.toArtifactKind(frontendKind: FrontendKind<out ResultingArtifact.FrontendOutput<*>>): BinaryKind<*> {
if (frontendKind == FrontendKinds.ClassicAndFIR && this in JvmPlatforms.allJvmPlatforms) return ArtifactKinds.JvmFromK1AndK2
return when (this) {
in JvmPlatforms.allJvmPlatforms -> ArtifactKinds.Jvm
in JsPlatforms.allJsPlatforms -> ArtifactKinds.Js
in NativePlatforms.allNativePlatforms -> ArtifactKinds.Native
else -> BinaryKind.NoArtifact
}
}
}
}
@@ -10,6 +10,8 @@ import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.output.writeAll
import org.jetbrains.kotlin.codegen.ClassFileFactory
import org.jetbrains.kotlin.test.model.ArtifactKinds
import org.jetbrains.kotlin.test.model.FrontendKind
import org.jetbrains.kotlin.test.model.FrontendKinds
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.*
import java.io.File
@@ -17,14 +19,24 @@ import java.io.File
class CompiledClassesManager(val testServices: TestServices) : TestService {
private val compiledKotlinCache = mutableMapOf<TestModule, File>()
private val compiledJavaCache = mutableMapOf<TestModule, File>()
var specifiedFrontendKind: FrontendKind<*> = FrontendKind.NoFrontend
fun getCompiledKotlinDirForModule(module: TestModule, classFileFactory: ClassFileFactory? = null): File {
return compiledKotlinCache.getOrPut(module) {
val outputDir = testServices.getOrCreateTempDirectory("module_${module.name}_kotlin-classes")
@Suppress("NAME_SHADOWING")
val classFileFactory = classFileFactory
?: testServices.dependencyProvider.getArtifact(module, ArtifactKinds.Jvm).classFileFactory
val classFileFactory = classFileFactory ?: if (module.binaryKind == ArtifactKinds.JvmFromK1AndK2) {
require(specifiedFrontendKind == FrontendKinds.FIR || specifiedFrontendKind == FrontendKinds.ClassicFrontend)
val k1AndK2Artifact = testServices.dependencyProvider.getArtifact(module, ArtifactKinds.JvmFromK1AndK2)
if (specifiedFrontendKind == FrontendKinds.FIR) {
k1AndK2Artifact.fromK2.classFileFactory
} else {
k1AndK2Artifact.fromK1.classFileFactory
}
} else {
testServices.dependencyProvider.getArtifact(module, ArtifactKinds.Jvm).classFileFactory
}
val outputFileCollection = SimpleOutputFileCollection(classFileFactory.currentOutput)
val messageCollector = testServices.compilerConfigurationProvider.getCompilerConfiguration(module)
.getNotNull(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY)
@@ -44,4 +56,4 @@ class CompiledClassesManager(val testServices: TestServices) : TestService {
}
}
val TestServices.compiledClassesManager: CompiledClassesManager by TestServices.testServiceAccessor()
val TestServices.compiledClassesManager: CompiledClassesManager by TestServices.testServiceAccessor()