Add tests for statistic report after build finish

This commit is contained in:
nataliya.valtman
2022-07-05 20:02:40 +02:00
committed by Space
parent 2520ef9232
commit 3b84284743
4 changed files with 124 additions and 64 deletions
@@ -6,6 +6,7 @@
package org.jetbrains.kotlin.gradle package org.jetbrains.kotlin.gradle
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.JsonParser
import io.ktor.application.* import io.ktor.application.*
import io.ktor.http.* import io.ktor.http.*
import io.ktor.request.* import io.ktor.request.*
@@ -15,28 +16,25 @@ import io.ktor.server.engine.*
import io.ktor.server.netty.* import io.ktor.server.netty.*
import io.ktor.util.collections.* import io.ktor.util.collections.*
import org.gradle.util.GradleVersion import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.plugin.stat.CompileStatisticsData import org.jetbrains.kotlin.gradle.plugin.stat.*
import org.jetbrains.kotlin.gradle.plugin.stat.StatTag
import org.jetbrains.kotlin.gradle.report.BuildReportType import org.jetbrains.kotlin.gradle.report.BuildReportType
import org.jetbrains.kotlin.gradle.testbase.* import org.jetbrains.kotlin.gradle.testbase.*
import org.jetbrains.kotlin.gradle.utils.`is`
import org.jetbrains.kotlin.test.util.joinToArrayString import org.jetbrains.kotlin.test.util.joinToArrayString
import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.DisplayName
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection
import java.net.ServerSocket import java.net.ServerSocket
import java.net.URL
import java.util.* import java.util.*
import kotlin.io.path.appendText import kotlin.io.path.appendText
import kotlin.test.fail
@DisplayName("Build statistics") @DisplayName("Build statistics")
@JvmGradlePluginTests @JvmGradlePluginTests
class BuildStatisticsWithKtorIT : KGPBaseTest() { class BuildStatisticsWithKtorIT : KGPBaseTest() {
companion object { companion object {
fun CompileStatisticsData.validateMandatoryField(kotlinVersion: String, validationData: ValidationData): List<String> { fun CompileStatisticsData.validateMandatoryField(kotlinVersion: String, validationData: TaskExecutionValidationData): List<String> {
val validationErrors = LinkedList<String>() val validationErrors = LinkedList<String>()
if (taskResult != validationData.taskResult) { if (taskResult != validationData.taskResult) {
validationErrors.add("Unexpected taskResult: $taskResult instead of ${validationData.taskResult}") validationErrors.add("Unexpected taskResult: $taskResult instead of ${validationData.taskResult}")
@@ -58,12 +56,10 @@ class BuildStatisticsWithKtorIT : KGPBaseTest() {
validationErrors.add("Does not contains \'$tag\' tag") validationErrors.add("Does not contains \'$tag\' tag")
} }
} }
return validationErrors return validationErrors
} }
fun CompileStatisticsData.validateIncrementalData(validationData: ValidationData): List<String> { fun CompileStatisticsData.validateIncrementalData(validationData: TaskExecutionValidationData): List<String> {
if (validationData.nonIncrementalReasons.isNotEmpty()) return emptyList() if (validationData.nonIncrementalReasons.isNotEmpty()) return emptyList()
val validationErrors = LinkedList<String>() val validationErrors = LinkedList<String>()
if (changes.size != validationData.changedFiles.size) { if (changes.size != validationData.changedFiles.size) {
@@ -75,7 +71,7 @@ class BuildStatisticsWithKtorIT : KGPBaseTest() {
return validationErrors return validationErrors
} }
fun CompileStatisticsData.validateNonIncrementalData(validationData: ValidationData): List<String> { fun CompileStatisticsData.validateNonIncrementalData(validationData: TaskExecutionValidationData): List<String> {
if (validationData.nonIncrementalReasons.isEmpty()) return emptyList() if (validationData.nonIncrementalReasons.isEmpty()) return emptyList()
val validationErrors = LinkedList<String>() val validationErrors = LinkedList<String>()
if (!tags.contains(StatTag.NON_INCREMENTAL.name)) { if (!tags.contains(StatTag.NON_INCREMENTAL.name)) {
@@ -84,6 +80,15 @@ class BuildStatisticsWithKtorIT : KGPBaseTest() {
return validationErrors return validationErrors
} }
fun BuildFinishStatisticsData.validate(validationData: BuildExecutionValidationData): List<String> {
if (validationData.tasks.isEmpty()) return emptyList()
val validationErrors = LinkedList<String>()
if (!startParameters.tasks.containsAll(validationData.tasks.toList())) {
validationErrors.add("Different set of executed tasks. Expected ${validationData.tasks.asList()} actual ${startParameters.tasks}")
}
return validationErrors
}
fun getEmptyPort(): ServerSocket { fun getEmptyPort(): ServerSocket {
for (port in 8080..8180) { for (port in 8080..8180) {
try { try {
@@ -108,7 +113,7 @@ class BuildStatisticsWithKtorIT : KGPBaseTest() {
println("start embedded server with data: ${ktorTestDataAnnotations.joinToArrayString()}") println("start embedded server with data: ${ktorTestDataAnnotations.joinToArrayString()}")
server = embeddedServer(Netty, port = port) server = embeddedServer(Netty, port = port)
{ {
val ktorServerData = ktorTestDataAnnotations.associateBy({ it.projectName }) { it.validationData.toMutableList() } val ktorServerData = ktorTestDataAnnotations.associateBy { it.projectName }
val failedResults = ConcurrentList<String>() val failedResults = ConcurrentList<String>()
suspend fun responseBadRequest(call: ApplicationCall, message: String) { suspend fun responseBadRequest(call: ApplicationCall, message: String) {
@@ -117,6 +122,62 @@ class BuildStatisticsWithKtorIT : KGPBaseTest() {
call.respond(status = HttpStatusCode.BadRequest, message) call.respond(status = HttpStatusCode.BadRequest, message)
} }
suspend fun validateBuildExecutionData(
statisticsData: BuildFinishStatisticsData,
projectValidation: KtorTestData,
call: ApplicationCall
) {
statisticsData.validate(projectValidation.buildValidationData).let {
if (it.isNotEmpty()) {
responseBadRequest(
call,
"${projectValidation.projectName}: Fail to validate statistics after build executed: ${it.joinToArrayString()}"
)
return
}
call.respond(HttpStatusCode.OK)
}
}
suspend fun validateTaskExecutionData(statData: CompileStatisticsData, projectValidation: KtorTestData, call: ApplicationCall) {
val ktorTestData = projectValidation.validationData.firstOrNull { it.taskName == statData.taskName }
if (ktorTestData == null) {
responseBadRequest(call, "${projectValidation.projectName}: Unknown validation for task ${statData.taskName}")
return
}
//validate response
statData.validateMandatoryField(defaultBuildOptions.kotlinVersion, ktorTestData).let {
if (it.isNotEmpty()) {
responseBadRequest(
call,
"${projectValidation.projectName}: Fail to validate mandatory fields: ${it.joinToArrayString()}"
)
return
}
}
statData.validateIncrementalData(ktorTestData).let {
if (it.isNotEmpty()) {
responseBadRequest(
call,
"${projectValidation.projectName}: Fail to validate incremental fields: ${it.joinToArrayString()}"
)
return
}
}
statData.validateNonIncrementalData(ktorTestData).let {
if (it.isNotEmpty()) {
responseBadRequest(
call,
"${projectValidation.projectName}: Fail to validate non-incremental fields: ${it.joinToArrayString()}"
)
return
}
}
call.respond(HttpStatusCode.OK)
}
routing { routing {
post("/badRequest") { post("/badRequest") {
call.respond(HttpStatusCode.BadRequest, "Some reason") call.respond(HttpStatusCode.BadRequest, "Some reason")
@@ -125,49 +186,29 @@ class BuildStatisticsWithKtorIT : KGPBaseTest() {
val body = call.receive<String>() val body = call.receive<String>()
println("TRACE: routing was called: $body") println("TRACE: routing was called: $body")
val statData = Gson().fromJson(body, CompileStatisticsData::class.java) val jsonObject = JsonParser.parseString(body).asJsonObject
val projectValidation = ktorServerData[statData.projectName] val projectName = jsonObject["projectName"].asString
val projectValidation = ktorServerData[projectName]
if (projectValidation == null) { if (projectValidation == null) {
responseBadRequest(call, "Unknown validation for project ${statData.projectName}") responseBadRequest(call, "Unknown validation for project $projectName")
return@post return@post
} }
val ktorTestData = projectValidation.firstOrNull { it.taskName == statData.taskName } val type = jsonObject["type"].asString
if (ktorTestData == null) { when (BuildDataType.valueOf(type)) {
responseBadRequest(call, "${statData.projectName}: Unknown validation for task ${statData.taskName}") BuildDataType.TASK_DATA -> validateTaskExecutionData(
return@post Gson().fromJson(body, CompileStatisticsData::class.java),
projectValidation,
call
)
BuildDataType.BUILD_DATA -> validateBuildExecutionData(
Gson().fromJson(body, BuildFinishStatisticsData::class.java),
projectValidation,
call
)
} }
//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") { get("/results") {
if (failedResults.isEmpty()) { if (failedResults.isEmpty()) {
@@ -202,9 +243,10 @@ class BuildStatisticsWithKtorIT : KGPBaseTest() {
"validateMandatoryField", "validateMandatoryField",
[ [
//first run //first run
ValidationData(":lib:compileKotlin", expectedTags = ["NON_INCREMENTAL"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]), TaskExecutionValidationData(":lib:compileKotlin", expectedTags = ["NON_INCREMENTAL"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]),
ValidationData(":app:compileKotlin", expectedTags = ["NON_INCREMENTAL"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]) TaskExecutionValidationData(":app:compileKotlin", expectedTags = ["NON_INCREMENTAL"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"])
] ],
BuildExecutionValidationData(["assemble"])
) )
@DisplayName("Validate mandatory field for http request body") @DisplayName("Validate mandatory field for http request body")
@GradleTest @GradleTest
@@ -222,12 +264,13 @@ class BuildStatisticsWithKtorIT : KGPBaseTest() {
"validateConfigurationCache", "validateConfigurationCache",
[ [
//first run //first run
ValidationData(":lib:compileKotlin", expectedTags = ["NON_INCREMENTAL", "CONFIGURATION_CACHE"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]), TaskExecutionValidationData(":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"]), TaskExecutionValidationData(":app:compileKotlin", expectedTags = ["NON_INCREMENTAL", "CONFIGURATION_CACHE"], nonIncrementalReasons = ["UNKNOWN_CHANGES_IN_GRADLE_INPUTS"]),
//second run //second run
ValidationData(":lib:compileKotlin", expectedTags = ["INCREMENTAL", "CONFIGURATION_CACHE"]), TaskExecutionValidationData(":lib:compileKotlin", expectedTags = ["INCREMENTAL", "CONFIGURATION_CACHE"]),
ValidationData(":app:compileKotlin", expectedTags = ["INCREMENTAL", "CONFIGURATION_CACHE"]), TaskExecutionValidationData(":app:compileKotlin", expectedTags = ["INCREMENTAL", "CONFIGURATION_CACHE"]),
] ],
BuildExecutionValidationData(["assemble"])
) )
@DisplayName("Validate configuration cache tag") @DisplayName("Validate configuration cache tag")
@GradleTest @GradleTest
@@ -256,10 +299,11 @@ class BuildStatisticsWithKtorIT : KGPBaseTest() {
@Retention(AnnotationRetention.RUNTIME) @Retention(AnnotationRetention.RUNTIME)
annotation class KtorTestData( annotation class KtorTestData(
val projectName: String, val projectName: String,
val validationData: Array<ValidationData>, val validationData: Array<TaskExecutionValidationData>,
val buildValidationData: BuildExecutionValidationData = BuildExecutionValidationData()
) )
annotation class ValidationData( annotation class TaskExecutionValidationData(
//mandatory fields //mandatory fields
val taskName: String, val taskName: String,
val taskResult: String = "SUCCESS", val taskResult: String = "SUCCESS",
@@ -272,5 +316,12 @@ annotation class ValidationData(
val changedFiles: Array<String> = [], val changedFiles: Array<String> = [],
) )
annotation class BuildExecutionValidationData(
val tasks: Array<String> = [],
val excludedTasks: Array<String> = [],
val projectProperties: Array<String> = [],
val systemProperties: Array<String> = [],
)
@@ -30,7 +30,8 @@ data class CompileStatisticsData(
val nonIncrementalAttributes: Set<BuildAttribute>, val nonIncrementalAttributes: Set<BuildAttribute>,
//TODO think about it,time in milliseconds //TODO think about it,time in milliseconds
val buildTimesMetrics: Map<BuildTime, Long>, val buildTimesMetrics: Map<BuildTime, Long>,
val performanceMetrics: Map<BuildPerformanceMetric, Long> val performanceMetrics: Map<BuildPerformanceMetric, Long>,
val type: String = BuildDataType.TASK_DATA.name
) { ) {
companion object { companion object {
private val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").also { it.timeZone = TimeZone.getTimeZone("UTC")} private val formatter = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss").also { it.timeZone = TimeZone.getTimeZone("UTC")}
@@ -48,6 +49,11 @@ enum class StatTag {
BUILD_CACHE, BUILD_CACHE,
} }
enum class BuildDataType {
TASK_DATA,
BUILD_DATA
}
//Sensitive data. This object is used directly for statistic via http //Sensitive data. This object is used directly for statistic via http
data class GradleBuildStartParameters( data class GradleBuildStartParameters(
val tasks: List<String>, val tasks: List<String>,
@@ -58,11 +64,13 @@ data class GradleBuildStartParameters(
) : java.io.Serializable ) : java.io.Serializable
//Sensitive data. This object is used directly for statistic via http //Sensitive data. This object is used directly for statistic via http
data class BuildFinishData( data class BuildFinishStatisticsData(
val projectName: String,
val startParameters: GradleBuildStartParameters, val startParameters: GradleBuildStartParameters,
val buildUuid: String = "Unset", val buildUuid: String = "Unset",
val label: String?, val label: String?,
val totalTime: Long, val totalTime: Long,
val type: String = BuildDataType.BUILD_DATA.name
) )
@@ -115,7 +115,7 @@ abstract class BuildMetricsReporterService : BuildService<BuildMetricsReporterSe
fun getStartParameters(project: Project) = project.gradle.startParameter.let { fun getStartParameters(project: Project) = project.gradle.startParameter.let {
GradleBuildStartParameters( GradleBuildStartParameters(
tasks = it.taskRequests.map { it.args.toString() }, tasks = it.taskRequests.flatMap { it.args },
excludedTasks = it.excludedTaskNames, excludedTasks = it.excludedTaskNames,
currentDir = it.currentDir.path, currentDir = it.currentDir.path,
projectProperties = it.projectProperties.map {(key, value) -> "$key: $value"}, projectProperties = it.projectProperties.map {(key, value) -> "$key: $value"},
@@ -15,9 +15,8 @@ import org.gradle.api.services.BuildServiceParameters
import org.gradle.tooling.events.FinishEvent import org.gradle.tooling.events.FinishEvent
import org.gradle.tooling.events.OperationCompletionListener import org.gradle.tooling.events.OperationCompletionListener
import org.gradle.tooling.events.task.TaskFinishEvent import org.gradle.tooling.events.task.TaskFinishEvent
import org.jetbrains.kotlin.gradle.plugin.stat.BuildFinishData import org.jetbrains.kotlin.gradle.plugin.stat.BuildFinishStatisticsData
import org.jetbrains.kotlin.gradle.plugin.BuildEventsListenerRegistryHolder import org.jetbrains.kotlin.gradle.plugin.BuildEventsListenerRegistryHolder
import org.jetbrains.kotlin.gradle.plugin.stat.CompileStatisticsData
import org.jetbrains.kotlin.gradle.plugin.stat.GradleBuildStartParameters import org.jetbrains.kotlin.gradle.plugin.stat.GradleBuildStartParameters
import org.jetbrains.kotlin.gradle.plugin.stat.StatTag import org.jetbrains.kotlin.gradle.plugin.stat.StatTag
import org.jetbrains.kotlin.gradle.plugin.statistics.BuildScanStatisticsListener import org.jetbrains.kotlin.gradle.plugin.statistics.BuildScanStatisticsListener
@@ -159,6 +158,7 @@ abstract class HttpReportService : BuildService<HttpReportService.Parameters>,
checkResponseAndLog(connection) checkResponseAndLog(connection)
connection.inputStream.use { it.reader().use { reader -> reader.readText() } } connection.inputStream.use { it.reader().use { reader -> reader.readText() } }
} catch (e: Exception) { } catch (e: Exception) {
log.debug("Unexpected exception happened ${e.message}: ${e.stackTrace}")
checkResponseAndLog(connection) checkResponseAndLog(connection)
} finally { } finally {
connection.disconnect() connection.disconnect()
@@ -168,7 +168,8 @@ abstract class HttpReportService : BuildService<HttpReportService.Parameters>,
} }
private fun reportBuildFinish() { private fun reportBuildFinish() {
val buildFinishData = BuildFinishData( val buildFinishData = BuildFinishStatisticsData(
projectName = parameters.projectName,
startParameters = parameters.startParameters, startParameters = parameters.startParameters,
buildUuid = buildUuid, buildUuid = buildUuid,
label = parameters.label, label = parameters.label,