blob: d4ad8725a928dddc649aee8733274482cd0954a9 [file] [log] [blame]
/*
* Copyright 2010-2021 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.
*/
// usages in build scripts are not tracked properly
@file:Suppress("unused")
import com.sun.management.OperatingSystemMXBean
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.attributes.Usage
import org.gradle.api.file.FileCollection
import org.gradle.api.file.FileSystemOperations
import org.gradle.api.internal.tasks.testing.filter.DefaultTestFilter
import org.gradle.api.tasks.TaskProvider
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.*
import org.gradle.kotlin.dsl.support.serviceOf
import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
import java.io.File
import java.lang.Character.isLowerCase
import java.lang.Character.isUpperCase
import java.lang.management.ManagementFactory
import java.nio.file.Files
import java.nio.file.Path
val kotlinGradlePluginAndItsRequired = arrayOf(
":kotlin-assignment",
":kotlin-allopen",
":kotlin-noarg",
":kotlin-sam-with-receiver",
":kotlin-lombok",
":kotlin-serialization",
":kotlin-android-extensions",
":kotlin-android-extensions-runtime",
":kotlin-parcelize-compiler",
":kotlin-build-common",
":kotlin-compiler-embeddable",
":native:kotlin-native-utils",
":kotlin-util-klib",
":kotlin-util-io",
":kotlin-compiler-runner",
":kotlin-daemon-embeddable",
":kotlin-daemon-client",
":kotlin-project-model",
":kotlin-gradle-plugins-bom",
":kotlin-gradle-plugin-api",
":kotlin-gradle-plugin-annotations",
":kotlin-gradle-plugin-idea",
":kotlin-gradle-plugin-idea-proto",
":kotlin-gradle-plugin",
":kotlin-gradle-plugin-model",
":kotlin-tooling-metadata",
":kotlin-tooling-core",
":kotlin-reflect",
":kotlin-test",
":kotlin-gradle-subplugin-example",
":kotlin-stdlib-common",
":kotlin-stdlib",
":kotlin-stdlib-jdk7",
":kotlin-stdlib-jdk8",
":kotlin-stdlib-js",
":kotlin-stdlib-wasm",
":examples:annotation-processor-example",
":kotlin-assignment-compiler-plugin.embeddable",
":kotlin-allopen-compiler-plugin.embeddable",
":kotlin-noarg-compiler-plugin.embeddable",
":kotlin-sam-with-receiver-compiler-plugin.embeddable",
":kotlin-lombok-compiler-plugin.embeddable",
":kotlinx-serialization-compiler-plugin.embeddable",
":kotlin-annotation-processing-embeddable",
":kotlin-script-runtime",
":kotlin-scripting-common",
":kotlin-scripting-jvm",
":kotlin-scripting-compiler-embeddable",
":kotlin-scripting-compiler-impl-embeddable",
":kotlin-test-js-runner",
":native:kotlin-klib-commonizer-embeddable",
":native:kotlin-klib-commonizer-api",
":compiler:build-tools:kotlin-build-tools-api",
":compiler:build-tools:kotlin-build-tools-impl",
)
fun Task.dependsOnKotlinGradlePluginInstall() {
kotlinGradlePluginAndItsRequired.forEach {
dependsOn("${it}:install")
}
}
fun Task.dependsOnKotlinGradlePluginPublish() {
kotlinGradlePluginAndItsRequired.forEach {
project.rootProject.tasks.findByPath("${it}:publish")?.let { task ->
dependsOn(task)
}
}
}
// Mixing JUnit4 and Junit5 in one module proved to be problematic, consider using separate modules instead
enum class JUnitMode {
JUnit4, JUnit5
}
/**
* @param parallel is redundant if @param jUnit5Enabled is true, because
* JUnit5 supports parallel test execution by itself, without gradle help
*/
fun Project.projectTest(
taskName: String = "test",
parallel: Boolean = false,
shortenTempRootName: Boolean = false,
jUnitMode: JUnitMode = JUnitMode.JUnit4,
maxHeapSizeMb: Int? = null,
minHeapSizeMb: Int? = null,
reservedCodeCacheSizeMb: Int = 256,
defineJDKEnvVariables: List<JdkMajorVersion> = emptyList(),
body: Test.() -> Unit = {}
): TaskProvider<Test> {
val shouldInstrument = project.providers.gradleProperty("kotlin.test.instrumentation.disable")
.orNull?.toBoolean() != true
if (shouldInstrument) {
evaluationDependsOn(":test-instrumenter")
}
return getOrCreateTask<Test>(taskName) {
dependsOn(":createIdeaHomeForTests")
doFirst {
if (jUnitMode == JUnitMode.JUnit5) return@doFirst
val commandLineIncludePatterns = commandLineIncludePatterns.toMutableSet()
val patterns = filter.includePatterns + commandLineIncludePatterns
if (patterns.isEmpty() || patterns.any { '*' in it }) return@doFirst
patterns.forEach { pattern ->
var isClassPattern = false
val maybeMethodName = pattern.substringAfterLast('.')
val maybeClassFqName = if (maybeMethodName.isFirstChar(::isLowerCase)) {
pattern.substringBeforeLast('.')
} else {
isClassPattern = true
pattern
}
if (!maybeClassFqName.substringAfterLast('.').isFirstChar(::isUpperCase)) {
return@forEach
}
val classFileNameWithoutExtension = maybeClassFqName.replace('.', '/')
val classFileName = "$classFileNameWithoutExtension.class"
if (isClassPattern) {
val innerClassPattern = "$pattern$*"
if (pattern in commandLineIncludePatterns) {
commandLineIncludePatterns.add(innerClassPattern)
(filter as? DefaultTestFilter)?.setCommandLineIncludePatterns(commandLineIncludePatterns)
} else {
filter.includePatterns.add(innerClassPattern)
}
}
include { treeElement ->
val path = treeElement.path
if (treeElement.isDirectory) {
classFileNameWithoutExtension.startsWith(path)
} else {
if (path == classFileName) return@include true
if (!path.endsWith(".class")) return@include false
path.startsWith("$classFileNameWithoutExtension$")
}
}
}
}
if (shouldInstrument) {
val instrumentationArgsProperty = project.providers.gradleProperty("kotlin.test.instrumentation.args")
val testInstrumenterOutputs = project.tasks.findByPath(":test-instrumenter:jar")!!.outputs.files
doFirst {
val agent = testInstrumenterOutputs.singleFile
val args = instrumentationArgsProperty.orNull?.let { "=$it" }.orEmpty()
jvmArgs("-javaagent:$agent$args")
}
dependsOn(":test-instrumenter:jar")
}
jvmArgs(
"-ea",
"-XX:+HeapDumpOnOutOfMemoryError",
"-XX:+UseCodeCacheFlushing",
"-XX:ReservedCodeCacheSize=${reservedCodeCacheSizeMb}m",
"-Djna.nosys=true"
)
val junit5ParallelTestWorkers =
project.kotlinBuildProperties.junit5NumberOfThreadsForParallelExecution ?: Runtime.getRuntime().availableProcessors()
val memoryPerTestProcessMb = maxHeapSizeMb ?: if (jUnitMode == JUnitMode.JUnit5)
totalMaxMemoryForTestsMb.coerceIn(defaultMaxMemoryPerTestWorkerMb, defaultMaxMemoryPerTestWorkerMb * junit5ParallelTestWorkers)
else
defaultMaxMemoryPerTestWorkerMb
maxHeapSize = "${memoryPerTestProcessMb}m"
if (minHeapSizeMb != null) {
minHeapSize = "${minHeapSizeMb}m"
}
systemProperty("idea.is.unit.test", "true")
systemProperty("idea.home.path", project.ideaHomePathForTests().canonicalPath)
systemProperty("idea.use.native.fs.for.win", false)
systemProperty("java.awt.headless", "true")
environment("NO_FS_ROOTS_ACCESS_CHECK", "true")
environment("PROJECT_CLASSES_DIRS", project.testSourceSet.output.classesDirs.asPath)
environment("PROJECT_BUILD_DIR", project.buildDir)
systemProperty("jps.kotlin.home", project.rootProject.extra["distKotlinHomeDir"]!!)
systemProperty("org.jetbrains.kotlin.skip.muted.tests", if (project.rootProject.hasProperty("skipMutedTests")) "true" else "false")
systemProperty("cacheRedirectorEnabled", project.rootProject.findProperty("cacheRedirectorEnabled")?.toString() ?: "false")
project.kotlinBuildProperties.junit5NumberOfThreadsForParallelExecution?.let { n ->
systemProperty("junit.jupiter.execution.parallel.config.strategy", "fixed")
systemProperty("junit.jupiter.execution.parallel.config.fixed.parallelism", n)
}
systemProperty("idea.ignore.disabled.plugins", "true")
var subProjectTempRoot: Path? = null
val projectName = project.name
val teamcity = project.rootProject.findProperty("teamcity") as? Map<*, *>
doFirst {
val systemTempRoot =
// TC by default doesn't switch `teamcity.build.tempDir` to 'java.io.tmpdir' so it could cause to wasted disk space
// Should be fixed soon on Teamcity side
(teamcity?.get("teamcity.build.tempDir") as? String)
?: System.getProperty("java.io.tmpdir")
systemTempRoot.let {
val prefix = (projectName + "Project_" + taskName + "_").takeUnless { shortenTempRootName }
subProjectTempRoot = Files.createTempDirectory(File(systemTempRoot).toPath(), prefix)
systemProperty("java.io.tmpdir", subProjectTempRoot.toString())
}
}
val fs = project.serviceOf<FileSystemOperations>()
doLast {
subProjectTempRoot?.let {
try {
fs.delete {
delete(it)
}
} catch (e: Exception) {
logger.warn("Can't delete test temp root folder $it", e.printStackTrace())
}
}
}
if (parallel && jUnitMode != JUnitMode.JUnit5) {
val forks = (totalMaxMemoryForTestsMb / memoryPerTestProcessMb).coerceAtMost(16)
maxParallelForks =
project.providers.gradleProperty("kotlin.test.maxParallelForks").orNull?.toInt()
?: forks.coerceIn(1, Runtime.getRuntime().availableProcessors())
}
defineJDKEnvVariables.forEach { version ->
val jdkHome = project.getToolchainJdkHomeFor(version).orNull ?: error("Can't find toolchain for $version")
environment(version.envName, jdkHome)
}
}.apply { configure(body) }
}
val defaultMaxMemoryPerTestWorkerMb = 1600
val reservedMemoryMb = 9000 // system processes, gradle daemon, kotlin daemon, etc ...
val totalMaxMemoryForTestsMb: Int
get() {
val mxbean = ManagementFactory.getOperatingSystemMXBean() as OperatingSystemMXBean
return (mxbean.totalPhysicalMemorySize / 1048576 - reservedMemoryMb).toInt()
}
val Test.commandLineIncludePatterns: Set<String>
get() = (filter as? DefaultTestFilter)?.commandLineIncludePatterns.orEmpty()
private inline fun String.isFirstChar(f: (Char) -> Boolean) = isNotEmpty() && f(first())
inline fun <reified T : Task> Project.getOrCreateTask(taskName: String, noinline body: T.() -> Unit): TaskProvider<T> =
if (tasks.names.contains(taskName)) tasks.named(taskName, T::class.java).apply { configure(body) }
else tasks.register(taskName, T::class.java, body)
object TaskUtils {
fun useAndroidSdk(task: Task) {
task.useAndroidConfiguration(systemPropertyName = "android.sdk", configName = "androidSdk")
}
fun useAndroidJar(task: Task) {
task.useAndroidConfiguration(systemPropertyName = "android.jar", configName = "androidJar")
}
fun useAndroidEmulator(task: Task) {
task.useAndroidConfiguration(systemPropertyName = "android.sdk", configName = "androidEmulator")
}
}
private fun Task.useAndroidConfiguration(systemPropertyName: String, configName: String) {
val configuration = with(project) {
configurations.getOrCreate(configName)
.also {
if (it.allDependencies.matching { dep ->
dep is ProjectDependency &&
dep.targetConfiguration == configName &&
dep.dependencyProject.path == ":dependencies:android-sdk"
}.count() == 0) {
dependencies.add(
configName,
dependencies.project(":dependencies:android-sdk", configuration = configName)
)
}
}
}
dependsOn(configuration)
if (this is Test) {
val androidFilePath = configuration.singleFile.canonicalPath
doFirst {
systemProperty(systemPropertyName, androidFilePath)
}
}
}
fun Task.useAndroidSdk() {
TaskUtils.useAndroidSdk(this)
}
fun Task.useAndroidJar() {
TaskUtils.useAndroidJar(this)
}
fun Task.acceptAndroidSdkLicenses() {
val separator = System.lineSeparator()
with(project) {
val androidSdk = configurations["androidSdk"].singleFile
val sdkLicensesDir = androidSdk.resolve("licenses").also {
if (!it.exists()) it.mkdirs()
}
val sdkLicenses = listOf(
"8933bad161af4178b1185d1a37fbf41ea5269c55",
"d56f5187479451eabf01fb78af6dfcb131a6481e",
"24333f8a63b6825ea9c5514f83c2829b004d1fee",
)
val sdkPreviewLicense = "84831b9409646a918e30573bab4c9c91346d8abd"
val sdkLicenseFile = sdkLicensesDir.resolve("android-sdk-license")
if (!sdkLicenseFile.exists()) {
sdkLicenseFile.createNewFile()
sdkLicenseFile.writeText(
sdkLicenses.joinToString(separator = separator)
)
} else {
sdkLicenses
.subtract(
sdkLicenseFile.readText().lines()
)
.forEach {
sdkLicenseFile.appendText("$it$separator")
}
}
val sdkPreviewLicenseFile = sdkLicensesDir.resolve("android-sdk-preview-license")
if (!sdkPreviewLicenseFile.exists()) {
sdkPreviewLicenseFile.writeText(sdkPreviewLicense)
} else {
if (sdkPreviewLicense != sdkPreviewLicenseFile.readText().trim()) {
sdkPreviewLicenseFile.writeText(sdkPreviewLicense)
}
}
}
}
fun Project.confugureFirPluginAnnotationsDependency(testTask: TaskProvider<Test>) {
val firPluginJvmAnnotations: Configuration by configurations.creating
val firPluginJsAnnotations: Configuration by configurations.creating {
attributes {
attribute(Usage.USAGE_ATTRIBUTE, objects.named(org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages.KOTLIN_RUNTIME))
attribute(org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.attribute, org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType.js)
}
}
dependencies {
firPluginJvmAnnotations(project(":plugins:fir-plugin-prototype:plugin-annotations")) { isTransitive = false }
firPluginJsAnnotations(project(":plugins:fir-plugin-prototype:plugin-annotations")) { isTransitive = false }
}
testTask.configure {
dependsOn(firPluginJvmAnnotations, firPluginJsAnnotations)
val localFirPluginJvmAnnotations: FileCollection = firPluginJvmAnnotations
val localFirPluginJsAnnotations: FileCollection = firPluginJsAnnotations
doFirst {
systemProperty("firPluginAnnotations.jvm.path", localFirPluginJvmAnnotations.singleFile.canonicalPath)
systemProperty("firPluginAnnotations.js.path", localFirPluginJsAnnotations.singleFile.canonicalPath)
}
}
}
fun Project.optInToExperimentalCompilerApi() {
@Suppress("DEPRECATION")
tasks.withType<org.jetbrains.kotlin.gradle.dsl.KotlinCompile<*>>().configureEach {
kotlinOptions {
freeCompilerArgs += "-opt-in=org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi"
}
}
}