diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index f1187604585..b007f2ab5f3 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -176,6 +176,18 @@ + + + + + + + + + + + + @@ -1703,6 +1715,12 @@ + + + + + + @@ -2844,6 +2862,12 @@ + + + + + + @@ -3252,6 +3276,12 @@ + + + + + + @@ -3308,6 +3338,18 @@ + + + + + + + + + + + + @@ -3328,6 +3370,12 @@ + + + + + + @@ -3342,6 +3390,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + @@ -3358,6 +3430,12 @@ + + + + + + @@ -3374,6 +3452,12 @@ + + + + + + @@ -3394,6 +3478,12 @@ + + + + + + @@ -3408,6 +3498,12 @@ + + + + + + @@ -3428,6 +3524,12 @@ + + + + + + @@ -3448,6 +3550,12 @@ + + + + + + @@ -3458,6 +3566,12 @@ + + + + + + @@ -3482,6 +3596,12 @@ + + + + + + @@ -3508,6 +3628,12 @@ + + + + + + @@ -3522,6 +3648,12 @@ + + + + + + @@ -3562,6 +3694,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3588,6 +3768,12 @@ + + + + + + @@ -3598,6 +3784,12 @@ + + + + + + @@ -3618,6 +3810,12 @@ + + + + + + @@ -3632,30 +3830,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3668,24 +3896,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -3780,6 +4050,12 @@ + + + + + + @@ -6295,6 +6571,120 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -6393,6 +6783,12 @@ + + + + + + @@ -7197,6 +7593,12 @@ + + + + + + @@ -7219,8 +7621,15 @@ + + + + + + + @@ -7258,6 +7667,7 @@ + @@ -7268,6 +7678,7 @@ + @@ -7380,6 +7791,18 @@ + + + + + + + + + + + + @@ -7412,6 +7835,12 @@ + + + + + + @@ -7504,6 +7933,12 @@ + + + + + + @@ -7612,6 +8047,7 @@ + @@ -8215,6 +8651,12 @@ + + + + + + @@ -8589,6 +9031,12 @@ + + + + + + diff --git a/gradle/versions.properties b/gradle/versions.properties index e409bc0add4..2b75896b17e 100644 --- a/gradle/versions.properties +++ b/gradle/versions.properties @@ -44,6 +44,10 @@ versions.kotlinx-coroutines-core-jvm=1.5.0 versions.kotlinx-coroutines-core=1.5.0 versions.kotlinx-metadata-jvm=0.5.0 versions.ktor-network=1.0.1 +versions.ktor-server-test-host=1.1.5 +versions.ktor-server-core=1.6.7 +versions.ktor-server-netty=1.6.7 +versions.ktor-client-mock=1.6.7 versions.native-platform=0.14 versions.protobuf=2.6.1 versions.r8=2.2.64 diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/build.gradle.kts b/libraries/tools/kotlin-gradle-plugin-integration-tests/build.gradle.kts index b22d69f8a58..fc0c5887bb5 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/build.gradle.kts +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/build.gradle.kts @@ -66,6 +66,10 @@ dependencies { testImplementation(project(":kotlin-android-extensions")) testImplementation(project(":kotlin-parcelize-compiler")) testImplementation(commonDependency("org.jetbrains.intellij.deps", "trove4j")) + testImplementation(commonDependency("io.ktor", "ktor-server-test-host")) + testImplementation(commonDependency("io.ktor", "ktor-server-core")) + testImplementation(commonDependency("io.ktor", "ktor-server-netty")) + testImplementation(commonDependency("io.ktor", "ktor-client-mock")) testImplementation(gradleApi()) testImplementation(gradleTestKit()) diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildStatisticsWithKtorIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildStatisticsWithKtorIT.kt new file mode 100644 index 00000000000..e02afde17b8 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/BuildStatisticsWithKtorIT.kt @@ -0,0 +1,276 @@ +/* + * 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.gradle + +import com.google.gson.Gson +import io.ktor.application.* +import io.ktor.http.* +import io.ktor.request.* +import io.ktor.response.* +import io.ktor.routing.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* +import io.ktor.util.collections.* +import org.gradle.util.GradleVersion +import org.jetbrains.kotlin.gradle.plugin.stat.CompileStatisticsData +import org.jetbrains.kotlin.gradle.plugin.stat.StatTag +import org.jetbrains.kotlin.gradle.report.BuildReportType +import org.jetbrains.kotlin.gradle.testbase.* +import org.jetbrains.kotlin.test.util.joinToArrayString +import org.junit.jupiter.api.AfterAll +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.DisplayName +import java.io.IOException +import java.net.HttpURLConnection +import java.net.ServerSocket +import java.net.URL +import java.util.* +import kotlin.io.path.appendText +import kotlin.test.fail + +@DisplayName("Build statistics") +@JvmGradlePluginTests +class BuildStatisticsWithKtorIT : KGPBaseTest() { + + companion object { + fun CompileStatisticsData.validateMandatoryField(kotlinVersion: String, validationData: ValidationData): List { + val validationErrors = LinkedList() + if (taskResult != validationData.taskResult) { + validationErrors.add("Unexpected taskResult: $taskResult instead of ${validationData.taskResult}") + } + if (this.kotlinVersion != kotlinVersion) { + validationErrors.add("Unexpected kotlinVersion: ${this.kotlinVersion} instead of ${kotlinVersion}") + } + if (compilerArguments.isEmpty()) { + validationErrors.add("Empty compiler arguments") + } + if (performanceMetrics.isEmpty()) { + validationErrors.add("Empty performance metrics") + } + if (buildTimesMetrics.isEmpty()) { + validationErrors.add("Empty build metrics") + } + for (tag: String in validationData.expectedTags) { + if (!tags.contains(tag)) { + validationErrors.add("Does not contains \'$tag\' tag") + } + } + + + return validationErrors + } + + fun CompileStatisticsData.validateIncrementalData(validationData: ValidationData): List { + if (validationData.nonIncrementalReasons.isNotEmpty()) return emptyList() + val validationErrors = LinkedList() + if (changes.size != validationData.changedFiles.size) { + validationErrors.add("Changed files do not equal: ${changes.joinToArrayString()} instead of ${validationData.changedFiles.joinToArrayString()} ") + } + if (!tags.contains(StatTag.INCREMENTAL.name)) { + validationErrors.add("INCREMENTAL tag was no set") + } + return validationErrors + } + + fun CompileStatisticsData.validateNonIncrementalData(validationData: ValidationData): List { + if (validationData.nonIncrementalReasons.isEmpty()) return emptyList() + val validationErrors = LinkedList() + if (!tags.contains(StatTag.NON_INCREMENTAL.name)) { + validationErrors.add("NON_INCREMENTAL tag was no set") + } + return validationErrors + } + + fun getEmptyPort(): ServerSocket { + for (port in 8080..8180) { + try { + return ServerSocket(port).also { println("Use $port port") } + } catch (_: IOException) { + continue // try next port + } + + } + throw IOException("no free port found") + } + + } + + private lateinit var server: ApplicationEngine + + private val port = getEmptyPort().localPort + + @BeforeAll + fun initEmbeddedService() { + val ktorTestDataAnnotations = this.javaClass.methods.flatMap { it.annotations.filterIsInstance() } + println("start embedded server with data: ${ktorTestDataAnnotations.joinToArrayString()}") + server = embeddedServer(Netty, port = port) + { + val ktorServerData = ktorTestDataAnnotations.associateBy({ it.projectName }) { it.validationData.toMutableList() } + val failedResults = ConcurrentList() + + suspend fun responseBadRequest(call: ApplicationCall, message: String) { + println("Validation errors: $message") + failedResults.plus(message) + call.respond(status = HttpStatusCode.BadRequest, message) + } + + routing { + post("/badRequest") { + call.respond(HttpStatusCode.BadRequest, "Some reason") + } + post("/validate") { + val body = call.receive() + println("TRACE: routing was called: $body") + + val statData = Gson().fromJson(body, CompileStatisticsData::class.java) + val projectValidation = ktorServerData[statData.projectName] + if (projectValidation == null) { + responseBadRequest(call, "Unknown validation for project ${statData.projectName}") + return@post + } + + val ktorTestData = projectValidation.firstOrNull { it.taskName == statData.taskName } + + if (ktorTestData == null) { + responseBadRequest(call, "${statData.projectName}: Unknown validation for task ${statData.taskName}") + return@post + } + + //validate response + statData.validateMandatoryField(defaultBuildOptions.kotlinVersion, ktorTestData).let { + if (it.isNotEmpty()) { + responseBadRequest( + call, + "${statData.projectName}: Fail to validate mandatory fields: ${it.joinToArrayString()}" + ) + return@post + } + } + statData.validateIncrementalData(ktorTestData).let { + if (it.isNotEmpty()) { + responseBadRequest( + call, + "${statData.projectName}: Fail to validate incremental fields: ${it.joinToArrayString()}" + ) + return@post + } + } + statData.validateNonIncrementalData(ktorTestData).let { + if (it.isNotEmpty()) { + responseBadRequest( + call, + "${statData.projectName}: Fail to validate non-incremental fields: ${it.joinToArrayString()}" + ) + return@post + } + } + call.respond(HttpStatusCode.OK) + } + get("/results") { + if (failedResults.isEmpty()) { + call.respond(HttpStatusCode.OK) + } else { + call.respond(HttpStatusCode.InternalServerError, message = failedResults.joinToArrayString()) + } + } + } + } + server.start() + } + + @AfterAll + fun shutDownEmbeddedService() { + server.stop(1000, 1000) + } + + + @DisplayName("Http build report request problems are logged only ones") + @GradleTest + fun testHttpServiceWithBadRequest(gradleVersion: GradleVersion) { + project("incrementalMultiproject", gradleVersion) { + enableStatisticReports(BuildReportType.HTTP, "http://localhost:$port/badRequest") + build("assemble") { + assertOutputContainsExactTimes("Failed to send statistic to", 1) + } + } + } + + @KtorTestData( + "validateMandatoryField", + [ + //first run + ValidationData(":lib:compileKotlin", expectedTags = ["NON_INCREMENTAL"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]), + ValidationData(":app:compileKotlin", expectedTags = ["NON_INCREMENTAL"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]) + ] + ) + @DisplayName("Validate mandatory field for http request body") + @GradleTest + fun testHttpRequest(gradleVersion: GradleVersion) { + project("incrementalMultiproject", gradleVersion) { + setProjectForTest("validateMandatoryField") + build("assemble") { + assertOutputDoesNotContain("Failed to send statistic to") + } + + } + } + + @KtorTestData( + "validateConfigurationCache", + [ + //first run + ValidationData(":lib:compileKotlin", expectedTags = ["NON_INCREMENTAL", "CONFIGURATION_CACHE"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]), + ValidationData(":app:compileKotlin", expectedTags = ["NON_INCREMENTAL", "CONFIGURATION_CACHE"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]), + //second run + ValidationData(":lib:compileKotlin", expectedTags = ["INCREMENTAL", "CONFIGURATION_CACHE"]), + ValidationData(":app:compileKotlin", expectedTags = ["INCREMENTAL", "CONFIGURATION_CACHE"]), + ] + ) + @DisplayName("Validate configuration cache tag") + @GradleTest + fun testConfigurationCache(gradleVersion: GradleVersion) { + val buildOptions = defaultBuildOptions.copy(configurationCache = true) + project("incrementalMultiproject", gradleVersion) { + setProjectForTest("validateConfigurationCache") + build("assemble", buildOptions = buildOptions) { + assertOutputDoesNotContain("Failed to send statistic to") + } + projectPath.resolve("lib/src/main/kotlin/bar/B.kt") + build("assemble", buildOptions = buildOptions) { + assertOutputDoesNotContain("Failed to send statistic to") + } + } + } + + private fun TestProject.setProjectForTest(projectName: String) { + enableStatisticReports(BuildReportType.HTTP, "http://localhost:$port/validate") + settingsGradle.appendText("rootProject.name=\'$projectName\'") + } +} + +@Repeatable +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class KtorTestData( + val projectName: String, + val validationData: Array, +) + +annotation class ValidationData( + //mandatory fields + val taskName: String, + val taskResult: String = "SUCCESS", + val expectedTags: Array = [], + + //fields for non-incremental compilation + val nonIncrementalReasons: Array = [], //if empty incremental validation expected + + //fields for incremental compilation + val changedFiles: Array = [], +) + + + diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/BuildScanStatisticsListener.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/BuildScanStatisticsListener.kt index 922b38d200e..541f80dcaa2 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/BuildScanStatisticsListener.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/BuildScanStatisticsListener.kt @@ -53,6 +53,11 @@ class BuildScanStatisticsListener( readableString(data).forEach { buildScan.value(data.taskName, it) } + data.label?.takeIf { !tags.contains(it) }?.also { + buildScan.tag(it) + tags.add(it) + } + data.tags .filter { !tags.contains(it) } .forEach { diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/CompileStatisticsData.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/CompileStatisticsData.kt index a70cd884e9b..54d65f5e21c 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/CompileStatisticsData.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/CompileStatisticsData.kt @@ -43,7 +43,9 @@ enum class StatTag { INCREMENTAL, NON_INCREMENTAL, GRADLE_DEBUG, - KOTLIN_DEBUG + KOTLIN_DEBUG, + CONFIGURATION_CACHE, + BUILD_CACHE, } //Sensitive data. This object is used directly for statistic via http diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/KotlinBuildStatListener.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/KotlinBuildStatListener.kt index c6104b5bcc3..eac13238088 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/KotlinBuildStatListener.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/statistics/KotlinBuildStatListener.kt @@ -9,8 +9,6 @@ import org.gradle.tooling.events.task.TaskFailureResult import org.gradle.tooling.events.task.TaskFinishEvent import org.gradle.tooling.events.task.TaskSkippedResult import org.gradle.tooling.events.task.TaskSuccessResult -import org.jetbrains.kotlin.cli.common.CompilerSystemProperties -import org.jetbrains.kotlin.cli.common.toBooleanLenient import org.jetbrains.kotlin.gradle.plugin.internal.state.TaskExecutionResults import org.jetbrains.kotlin.gradle.plugin.stat.CompileStatisticsData import org.jetbrains.kotlin.gradle.plugin.stat.StatTag @@ -49,7 +47,8 @@ class KotlinBuildStatListener { projectName: String, uuid: String, label: String?, - kotlinVersion: String + kotlinVersion: String, + additionalTags: List = emptyList() ): CompileStatisticsData? { val result = event.result val taskPath = event.descriptor.taskPath @@ -89,7 +88,7 @@ class KotlinBuildStatListener { projectName = projectName, taskName = taskPath, changes = changes, - tags = parseTags(taskExecutionResult).map { it.name }, + tags = parseTags(taskExecutionResult, additionalTags).map { it.name }, nonIncrementalAttributes = taskExecutionResult?.buildMetrics?.buildAttributes?.asMap()?.filter { it.value > 0 }?.keys ?: emptySet(), hostName = hostName, kotlinVersion = kotlinVersion, @@ -99,8 +98,8 @@ class KotlinBuildStatListener { ) } - private fun parseTags(taskExecutionResult: TaskExecutionResult?): List { - val tags = ArrayList() + private fun parseTags(taskExecutionResult: TaskExecutionResult?, additionalTags: List): List { + val tags = ArrayList(additionalTags) val nonIncrementalAttributes = taskExecutionResult?.buildMetrics?.buildAttributes?.asMap() ?: emptyMap() diff --git a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/report/HttpReportService.kt b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/report/HttpReportService.kt index 9eeb6686b6a..47f76300a5f 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/report/HttpReportService.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/report/HttpReportService.kt @@ -17,8 +17,10 @@ import org.gradle.tooling.events.task.TaskFinishEvent import org.jetbrains.kotlin.gradle.plugin.stat.BuildFinishData import org.jetbrains.kotlin.gradle.plugin.stat.CompileStatisticsData import org.jetbrains.kotlin.gradle.plugin.stat.GradleBuildStartParameters +import org.jetbrains.kotlin.gradle.plugin.stat.StatTag import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatListener.Companion.prepareData import org.jetbrains.kotlin.gradle.report.BuildMetricsReporterService.Companion.getStartParameters +import org.jetbrains.kotlin.gradle.utils.isConfigurationCacheAvailable import java.io.IOException import java.net.HttpURLConnection import java.net.URL @@ -31,19 +33,24 @@ abstract class HttpReportService : BuildService, OperationCompletionListener, AutoCloseable { var executorService: ExecutorService = Executors.newSingleThreadExecutor() + val uuid = UUID.randomUUID().toString() val startTime = System.nanoTime() interface Parameters : BuildServiceParameters { var label: String? - var uuid: String var projectName: String var httpSettings: HttpReportSettings var kotlinVersion: String + var additionalTags: List var startParameters: GradleBuildStartParameters } - private val log = Logging.getLogger(this.javaClass) + val log = Logging.getLogger(this.javaClass) + + init { + log.info("Http report service is registered. Unique build id: $uuid") + } // @Volatile for one thread executor it does not need private var requestPreviousFailed = false @@ -51,7 +58,8 @@ abstract class HttpReportService : BuildService, override fun onFinish(event: FinishEvent?) { if (event is TaskFinishEvent) { - val data = prepareData(event, parameters.projectName, parameters.uuid, parameters.label, parameters.kotlinVersion) + val data = + prepareData(event, parameters.projectName, uuid, parameters.label, parameters.kotlinVersion, parameters.additionalTags) data?.also { executorService.submit { report(data) } } } } @@ -64,20 +72,30 @@ abstract class HttpReportService : BuildService, companion object { fun registerIfAbsent(project: Project, kotlinVersion: String): Provider? { - val rootProject = project.gradle.rootProject + val gradle = project.gradle + val rootProject = gradle.rootProject val reportingSettings = reportingSettings(rootProject) return reportingSettings.httpReportSettings?.let { httpSettings -> - project.gradle.sharedServices.registerIfAbsent( + gradle.sharedServices.registerIfAbsent( "build_http_metric_service_${HttpReportService::class.java.classLoader.hashCode()}", HttpReportService::class.java ) { it.parameters.label = reportingSettings.buildReportLabel it.parameters.projectName = rootProject.name - it.parameters.uuid = UUID.randomUUID().toString() it.parameters.httpSettings = httpSettings it.parameters.kotlinVersion = kotlinVersion it.parameters.startParameters = getStartParameters(project) + + //init gradle tags, that present in build scan + val additionalTags = ArrayList() + if (isConfigurationCacheAvailable(gradle)) { + additionalTags.add(StatTag.CONFIGURATION_CACHE) + } + if (gradle.startParameter.isBuildCacheEnabled) { + additionalTags.add(StatTag.BUILD_CACHE) + } + it.parameters.additionalTags = additionalTags }!! }