blob: e6b342662d9694625c1feaf6c499965682590fd1 [file] [log] [blame]
/*
* Copyright 2010-2020 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.services.configuration
import com.intellij.ide.highlighter.JavaFileType
import com.intellij.openapi.util.SystemInfo
import com.intellij.psi.PsiJavaModule.MODULE_INFO_FILE
import org.jetbrains.kotlin.backend.common.phaser.PhaseConfig
import org.jetbrains.kotlin.backend.jvm.jvmPhases
import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys
import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments
import org.jetbrains.kotlin.cli.jvm.addModularRootIfNotNull
import org.jetbrains.kotlin.cli.jvm.config.*
import org.jetbrains.kotlin.cli.jvm.configureStandardLibs
import org.jetbrains.kotlin.config.CompilerConfiguration
import org.jetbrains.kotlin.config.CompilerConfigurationKey
import org.jetbrains.kotlin.config.JVMConfigurationKeys
import org.jetbrains.kotlin.config.JvmTarget
import org.jetbrains.kotlin.platform.jvm.JvmPlatforms
import org.jetbrains.kotlin.test.ConfigurationKind
import org.jetbrains.kotlin.test.MockLibraryUtil
import org.jetbrains.kotlin.test.MockLibraryUtil.compileJavaFilesLibraryToJar
import org.jetbrains.kotlin.test.TestJavacVersion
import org.jetbrains.kotlin.test.TestJdkKind
import org.jetbrains.kotlin.test.backend.handlers.PhasedIrDumpHandler
import org.jetbrains.kotlin.test.directives.CodegenTestDirectives
import org.jetbrains.kotlin.test.directives.ConfigurationDirectives
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.ALL_JAVA_AS_BINARY
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.ASSERTIONS_MODE
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.COMPILE_JAVA_USING
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.INCLUDE_JAVA_AS_BINARY
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.JVM_TARGET
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.LAMBDAS
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.SAM_CONVERSIONS
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.SERIALIZE_IR
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.STRING_CONCAT
import org.jetbrains.kotlin.test.directives.JvmEnvironmentConfigurationDirectives.USE_OLD_INLINE_CLASSES_MANGLING_SCHEME
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives.DISABLE_CALL_ASSERTIONS
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives.DISABLE_PARAM_ASSERTIONS
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives.EMIT_JVM_TYPE_ANNOTATIONS
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives.ENABLE_JVM_PREVIEW
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives.NO_OPTIMIZED_CALLABLE_REFERENCES
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives.NO_UNIFIED_NULL_CHECKS
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives.PARAMETERS_METADATA
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives.JDK_RELEASE
import org.jetbrains.kotlin.test.directives.LanguageSettingsDirectives.USE_TYPE_TABLE
import org.jetbrains.kotlin.test.directives.model.DirectivesContainer
import org.jetbrains.kotlin.test.directives.model.RegisteredDirectives
import org.jetbrains.kotlin.test.model.DependencyDescription
import org.jetbrains.kotlin.test.model.DependencyKind
import org.jetbrains.kotlin.test.model.TestFile
import org.jetbrains.kotlin.test.model.TestModule
import org.jetbrains.kotlin.test.services.*
import org.jetbrains.kotlin.test.services.jvm.CompiledClassesManager
import org.jetbrains.kotlin.test.services.jvm.compiledClassesManager
import org.jetbrains.kotlin.test.util.KtTestUtil
import org.jetbrains.kotlin.test.util.joinToArrayString
import org.jetbrains.kotlin.utils.PathUtil
import java.io.File
import kotlin.io.path.ExperimentalPathApi
class JvmEnvironmentConfigurator(testServices: TestServices) : EnvironmentConfigurator(testServices) {
companion object {
val TEST_CONFIGURATION_KIND_KEY = CompilerConfigurationKey.create<ConfigurationKind>("ConfigurationKind")
private val DEFAULT_JVM_TARGET_FROM_PROPERTY: String? = System.getProperty("kotlin.test.default.jvm.target")
private const val JAVA_BINARIES_JAR_NAME = "java-binaries"
fun extractConfigurationKind(registeredDirectives: RegisteredDirectives): ConfigurationKind {
val withStdlib = ConfigurationDirectives.WITH_STDLIB in registeredDirectives
val withReflect = JvmEnvironmentConfigurationDirectives.WITH_REFLECT in registeredDirectives
val noRuntime = JvmEnvironmentConfigurationDirectives.NO_RUNTIME in registeredDirectives
if (noRuntime && withStdlib) {
error("NO_RUNTIME and WITH_STDLIB can not be used together")
}
return when {
withStdlib && !withReflect -> ConfigurationKind.NO_KOTLIN_REFLECT
withStdlib || withReflect -> ConfigurationKind.ALL
noRuntime -> ConfigurationKind.JDK_NO_RUNTIME
else -> ConfigurationKind.JDK_ONLY
}
}
fun extractJdkKind(registeredDirectives: RegisteredDirectives): TestJdkKind {
val fullJdkEnabled = JvmEnvironmentConfigurationDirectives.FULL_JDK in registeredDirectives
val jdkKinds = registeredDirectives[JvmEnvironmentConfigurationDirectives.JDK_KIND]
if (fullJdkEnabled) {
if (jdkKinds.isNotEmpty()) {
error("FULL_JDK and JDK_KIND can not be used together")
}
return TestJdkKind.FULL_JDK
}
return when (jdkKinds.size) {
0 -> TestJdkKind.MOCK_JDK
1 -> jdkKinds.single()
else -> error("Too many jdk kinds passed: ${jdkKinds.joinToArrayString()}")
}
}
fun getLibraryFilesExceptRealRuntime(
testServices: TestServices,
configurationKind: ConfigurationKind,
directives: RegisteredDirectives
): List<File> {
val provider = testServices.standardLibrariesPathProvider
val files = mutableListOf<File>()
if (configurationKind.withRuntime) {
files.add(provider.kotlinTestJarForTests())
} else if (configurationKind.withMockRuntime) {
files.add(provider.minimalRuntimeJarForTests())
files.add(provider.scriptRuntimeJarForTests())
}
if (configurationKind.withReflection) {
files.add(provider.reflectJarForTests())
}
files.add(KtTestUtil.getAnnotationsJar())
if (JvmEnvironmentConfigurationDirectives.STDLIB_JDK8 in directives) {
files.add(provider.runtimeJarForTestsWithJdk8())
}
files.add(KtTestUtil.getAnnotationsJar())
return files
}
fun getJdkHome(jdkKindTestJdkKind: TestJdkKind): File? = when (jdkKindTestJdkKind) {
TestJdkKind.MOCK_JDK -> null
TestJdkKind.MODIFIED_MOCK_JDK -> null
TestJdkKind.FULL_JDK_6 -> File(System.getenv("JDK_16") ?: error("Environment variable JDK_16 is not set"))
TestJdkKind.FULL_JDK_11 -> KtTestUtil.getJdk11Home()
TestJdkKind.FULL_JDK_17 -> KtTestUtil.getJdk17Home()
TestJdkKind.FULL_JDK -> if (SystemInfo.IS_AT_LEAST_JAVA9) File(System.getProperty("java.home")) else null
TestJdkKind.ANDROID_API -> null
}
fun getJdkClasspathRoot(jdkKind: TestJdkKind): File? = when (jdkKind) {
TestJdkKind.MOCK_JDK -> KtTestUtil.findMockJdkRtJar()
TestJdkKind.MODIFIED_MOCK_JDK -> KtTestUtil.findMockJdkRtModified()
TestJdkKind.ANDROID_API -> KtTestUtil.findAndroidApiJar()
TestJdkKind.FULL_JDK_6 -> null
TestJdkKind.FULL_JDK_11 -> null
TestJdkKind.FULL_JDK_17 -> null
TestJdkKind.FULL_JDK -> null
}
}
override val directiveContainers: List<DirectivesContainer>
get() = listOf(JvmEnvironmentConfigurationDirectives)
override val additionalServices: List<ServiceRegistrationData>
get() = listOf(service(::CompiledClassesManager))
override fun DirectiveToConfigurationKeyExtractor.provideConfigurationKeys() {
register(STRING_CONCAT, JVMConfigurationKeys.STRING_CONCAT)
register(ASSERTIONS_MODE, JVMConfigurationKeys.ASSERTIONS_MODE)
register(SAM_CONVERSIONS, JVMConfigurationKeys.SAM_CONVERSIONS)
register(LAMBDAS, JVMConfigurationKeys.LAMBDAS)
register(USE_OLD_INLINE_CLASSES_MANGLING_SCHEME, JVMConfigurationKeys.USE_OLD_INLINE_CLASSES_MANGLING_SCHEME)
register(ENABLE_JVM_PREVIEW, JVMConfigurationKeys.ENABLE_JVM_PREVIEW)
register(EMIT_JVM_TYPE_ANNOTATIONS, JVMConfigurationKeys.EMIT_JVM_TYPE_ANNOTATIONS)
register(NO_OPTIMIZED_CALLABLE_REFERENCES, JVMConfigurationKeys.NO_OPTIMIZED_CALLABLE_REFERENCES)
register(DISABLE_PARAM_ASSERTIONS, JVMConfigurationKeys.DISABLE_PARAM_ASSERTIONS)
register(DISABLE_CALL_ASSERTIONS, JVMConfigurationKeys.DISABLE_CALL_ASSERTIONS)
register(NO_UNIFIED_NULL_CHECKS, JVMConfigurationKeys.NO_UNIFIED_NULL_CHECKS)
register(PARAMETERS_METADATA, JVMConfigurationKeys.PARAMETERS_METADATA)
register(JVM_TARGET, JVMConfigurationKeys.JVM_TARGET)
register(SERIALIZE_IR, JVMConfigurationKeys.SERIALIZE_IR)
register(JDK_RELEASE, JVMConfigurationKeys.JDK_RELEASE)
register(USE_TYPE_TABLE, JVMConfigurationKeys.USE_TYPE_TABLE)
}
@OptIn(ExperimentalPathApi::class, ExperimentalStdlibApi::class)
override fun configureCompilerConfiguration(configuration: CompilerConfiguration, module: TestModule) {
if (module.targetPlatform !in JvmPlatforms.allJvmPlatforms) return
configureDefaultJvmTarget(configuration)
val registeredDirectives = module.directives
val jdkKind = extractJdkKind(registeredDirectives)
getJdkHome(jdkKind)?.let { configuration.put(JVMConfigurationKeys.JDK_HOME, it) }
getJdkClasspathRoot(jdkKind)?.let { configuration.addJvmClasspathRoot(it) }
when (jdkKind) {
TestJdkKind.MOCK_JDK, TestJdkKind.MODIFIED_MOCK_JDK, TestJdkKind.ANDROID_API -> {
configuration.put(JVMConfigurationKeys.NO_JDK, true)
}
TestJdkKind.FULL_JDK_6 -> {}
TestJdkKind.FULL_JDK_11 -> {}
TestJdkKind.FULL_JDK_17 -> {}
TestJdkKind.FULL_JDK -> {}
}
val configurationKind = extractConfigurationKind(registeredDirectives).also {
configuration.put(TEST_CONFIGURATION_KIND_KEY, it)
}
val javaVersionToCompile = registeredDirectives[COMPILE_JAVA_USING].singleOrNull()
val javaBinaryFiles = if (ALL_JAVA_AS_BINARY !in registeredDirectives) {
module.javaFiles.filter { INCLUDE_JAVA_AS_BINARY in it.directives }
} else module.javaFiles
val useJava11ToCompileIncludedJavaFiles = javaVersionToCompile == TestJavacVersion.JAVAC_11
if (configurationKind.withRuntime) {
configuration.configureStandardLibs(PathUtil.kotlinPathsForDistDirectory, K2JVMCompilerArguments().also { it.noReflect = true })
}
configuration.addJvmClasspathRoots(getLibraryFilesExceptRealRuntime(testServices, configurationKind, module.directives))
val isIr = module.targetBackend?.isIR == true
configuration.put(JVMConfigurationKeys.IR, isIr)
val javaSourceFiles = module.javaFiles.filter { INCLUDE_JAVA_AS_BINARY !in it.directives }
if (javaSourceFiles.isNotEmpty() &&
JvmEnvironmentConfigurationDirectives.SKIP_JAVA_SOURCES !in module.directives &&
ALL_JAVA_AS_BINARY !in registeredDirectives
) {
// NB: [getRealFileForSourceFile] is misleading, since it actually creates a real file from the given test file as well.
val realSourceFileMap = javaSourceFiles.associateWith { testServices.sourceFileProvider.getRealFileForSourceFile(it) }
// TODO: temporary hack to provide java 9 modules in the source mode properly (see comment on ClasspathRootsResolved::addModularRoots)
addJavaCompiledModulesFromDependentKotlinModules(configuration, configurationKind, module, bySources = true)
val (moduleInfoFiles, sourceFiles) = javaSourceFiles.partition { it.name == MODULE_INFO_FILE }
if (moduleInfoFiles.isNotEmpty()) {
addJavaSourceRootsByJavaModules(configuration, moduleInfoFiles)
} else {
sourceFiles.forEach l@{ testFile ->
val file = realSourceFileMap[testFile] ?: return@l
if (JvmEnvironmentConfigurationDirectives.USE_JAVAC !in module.directives &&
!file.isDirectory &&
file.extension == JavaFileType.DEFAULT_EXTENSION
) {
configuration.addJavaSourceRoot(file)
}
}
configuration.addJavaSourceRoot(testServices.sourceFileProvider.javaSourceDirectory)
}
}
if (javaBinaryFiles.isNotEmpty()) {
javaBinaryFiles.forEach { testServices.sourceFileProvider.getRealFileForBinaryFile(it) }
addJavaCompiledModulesFromDependentKotlinModules(configuration, configurationKind, module, bySources = false)
val moduleInfoFiles = javaBinaryFiles.filter { it.name == MODULE_INFO_FILE }
// TODO: Use module graph to build proper modulepath for each module according cross-module dependencies
if (moduleInfoFiles.isNotEmpty()) {
addJavaBinaryRootsByJavaModules(configuration, configurationKind, moduleInfoFiles)
} else {
configuration.addJvmClasspathRoot(
compileJavaFilesLibraryToJar(
testServices.sourceFileProvider.javaBinaryDirectory.path,
JAVA_BINARIES_JAR_NAME,
extraClasspath = configuration.jvmClasspathRoots.map { it.absolutePath },
assertions = JUnit5Assertions,
useJava11 = useJava11ToCompileIncludedJavaFiles
)
)
}
}
configuration.registerModuleDependencies(module)
if (JvmEnvironmentConfigurationDirectives.USE_PSI_CLASS_FILES_READING in module.directives) {
configuration.put(JVMConfigurationKeys.USE_PSI_CLASS_FILES_READING, true)
}
if (LanguageSettingsDirectives.ALLOW_KOTLIN_PACKAGE in module.directives) {
configuration.put(CLIConfigurationKeys.ALLOW_KOTLIN_PACKAGE, true)
}
if (CodegenTestDirectives.DUMP_IR_FOR_GIVEN_PHASES in module.directives) {
configuration.putCustomPhaseConfigWithEnabledDump(module)
}
configuration.put(JVMConfigurationKeys.VALIDATE_IR, true)
configuration.put(JVMConfigurationKeys.VALIDATE_BYTECODE, true)
configuration.configureJdkClasspathRoots()
}
private fun addJavaSourceRootsByJavaModules(configuration: CompilerConfiguration, moduleInfoFiles: List<TestFile>) {
val javaSourceDirectory = testServices.sourceFileProvider.javaSourceDirectory
for (moduleInfoFile in moduleInfoFiles) {
val moduleName = moduleInfoFile.relativePath.substringBefore('/')
val moduleDir = File("${javaSourceDirectory.path}/$moduleName").also { it.mkdir() }
configuration.addJavaSourceRoot(moduleDir)
}
}
private fun addJavaBinaryRootsByJavaModules(
configuration: CompilerConfiguration,
configurationKind: ConfigurationKind,
moduleInfoFiles: List<TestFile>
) {
val javaBinaryDirectory = testServices.sourceFileProvider.javaBinaryDirectory
for (moduleInfoFile in moduleInfoFiles) {
val moduleName = moduleInfoFile.relativePath.substringBefore('/')
addJavaCompiledModule(configuration, configurationKind, moduleName, bySources = true, targetDir = javaBinaryDirectory)
}
}
private fun addJavaCompiledModulesFromDependentKotlinModules(
configuration: CompilerConfiguration,
configurationKind: ConfigurationKind,
module: TestModule,
bySources: Boolean
) {
val moduleDependencies = module.allDependencies.map { testServices.dependencyProvider.getTestModule(it.moduleName) }
val filterJavaModuleInfoFiles = { testFile: TestFile ->
val binaryFilesFilter = INCLUDE_JAVA_AS_BINARY in testFile.directives || ALL_JAVA_AS_BINARY in module.directives
val includeOrExcludeBinaryFilesFilter = (bySources && !binaryFilesFilter) || (!bySources && binaryFilesFilter)
includeOrExcludeBinaryFilesFilter && testFile.name == MODULE_INFO_FILE
}
val moduleInfoFilesFromDependencies = moduleDependencies.mapNotNull { it.javaFiles.singleOrNull(filterJavaModuleInfoFiles) }
for (dependentModuleInfoFile in moduleInfoFilesFromDependencies) {
val moduleName = dependentModuleInfoFile.relativePath.substringBefore('/')
addJavaCompiledModule(configuration, configurationKind, moduleName, bySources)
}
}
private fun addJavaCompiledModule(
configuration: CompilerConfiguration,
configurationKind: ConfigurationKind,
moduleName: String,
bySources: Boolean,
targetDir: File = testServices.sourceFileProvider.run { if (bySources) javaSourceDirectory else javaBinaryDirectory }
) {
val moduleDir = File("${targetDir.path}/$moduleName")
val javaBinaries = if (bySources) {
compileJavaFilesToModularJar(configuration, configurationKind, moduleDir)
} else {
File("${moduleDir.path}/$JAVA_BINARIES_JAR_NAME.jar")
}
configuration.addModularRootIfNotNull(isModularJava = true, moduleName, javaBinaries)
}
@OptIn(ExperimentalStdlibApi::class)
private fun compileJavaFilesToModularJar(
configuration: CompilerConfiguration,
configurationKind: ConfigurationKind,
sourcesDir: File
): File {
val modulePath = buildList {
addAll(configuration.jvmModularRoots.map { it.absolutePath })
if (configurationKind.withRuntime) {
add(testServices.standardLibrariesPathProvider.runtimeJarForTests().path)
}
}
return MockLibraryUtil.compileLibraryToJar(
sourcesDir.path,
sourcesDir,
JAVA_BINARIES_JAR_NAME,
extraClasspath = configuration.jvmClasspathRoots.map { it.absolutePath },
extraModulepath = modulePath,
assertions = JUnit5Assertions,
useJava11 = true
)
}
private fun configureDefaultJvmTarget(configuration: CompilerConfiguration) {
if (DEFAULT_JVM_TARGET_FROM_PROPERTY == null) return
val customDefaultTarget = JvmTarget.fromString(DEFAULT_JVM_TARGET_FROM_PROPERTY)
?: error("Can't construct JvmTarget for $DEFAULT_JVM_TARGET_FROM_PROPERTY")
val originalTarget = configuration[JVMConfigurationKeys.JVM_TARGET]
if (originalTarget == null || customDefaultTarget.majorVersion > originalTarget.majorVersion) {
// It's not safe to substitute target in general
// cause it can affect generated bytecode and original behaviour should be tested somehow.
// Original behaviour testing is perfomed by
//
// codegenTest(target = 6, jvm = "Last", jdk = mostRecentJdk)
// codegenTest(target = 8, jvm = "Last", jdk = mostRecentJdk)
//
// in compiler/tests-different-jdk/build.gradle.kts
configuration.put(JVMConfigurationKeys.JVM_TARGET, customDefaultTarget)
}
}
private fun CompilerConfiguration.putCustomPhaseConfigWithEnabledDump(module: TestModule) {
val dumpDirectory = testServices.getOrCreateTempDirectory(PhasedIrDumpHandler.DUMPED_IR_FOLDER_NAME)
val phases = module.directives[CodegenTestDirectives.DUMP_IR_FOR_GIVEN_PHASES].toSet()
if (phases.isNotEmpty()) {
val phaseConfig = PhaseConfig(
jvmPhases,
toDumpStateBefore = phases,
toDumpStateAfter = phases,
dumpToDirectory = dumpDirectory.absolutePath
)
put(CLIConfigurationKeys.PHASE_CONFIG, phaseConfig)
}
}
private fun CompilerConfiguration.registerModuleDependencies(module: TestModule) {
addJvmClasspathRoots(module.allDependencies.filter { it.kind == DependencyKind.Binary }.toFileList())
val binaryFriends = module.friendDependencies.filter { it.kind == DependencyKind.Binary }
if (binaryFriends.isNotEmpty()) {
put(JVMConfigurationKeys.FRIEND_PATHS, binaryFriends.toFileList().map { it.absolutePath })
}
}
private fun List<DependencyDescription>.toFileList(): List<File> = this.flatMap { dependency ->
val friendModule = testServices.dependencyProvider.getTestModule(dependency.moduleName)
listOfNotNull(
testServices.compiledClassesManager.getCompiledKotlinDirForModule(friendModule),
testServices.compiledClassesManager.getCompiledJavaDirForModule(friendModule)
)
}
}