[Build] Split :tests-mutes package to common and TC integration parts

This is needed because of following problem:
- :tests-mutes has `implementation` dependency on khttp library
- khttp has dependency on spek-junit-platform-engine library
- :tests-common had `testCompile` dependency on :tests-mutes which
    added spek library as as a dependency to all modules which depend
    on :tests-common, including :tests-common-new

Then, if project is configured with JPS then if user tries to run all
  tests in directory in module which uses JUnit 5 (like :tests-common-new)
  then spek library will be added to classpath and junit runner takes
  some platform extension from it which causes NoSuchMethodException
  because spek library was compiled against outdated JUnit 5 version
  and current version doesn't have some API.

So splitting :tests-mutes for two parts fixes this issue, because common
  part (:compiler:tests-mutes) no longer depends on khttp, so spek
  library doesn't spreads to all modules
This commit is contained in:
Dmitriy Novozhilov
2020-12-09 15:06:03 +03:00
parent 26d7ea6ce6
commit 8a5fc2ad29
8 changed files with 31 additions and 18 deletions
@@ -0,0 +1,27 @@
plugins {
kotlin("jvm")
id("jps-compatible")
application
}
dependencies {
api(kotlinStdlib())
implementation(project(":compiler:tests-mutes"))
implementation("khttp:khttp:1.0.0")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.11.0")
}
sourceSets {
"main" { projectDefault() }
"test" {}
}
val compileKotlin: org.jetbrains.kotlin.gradle.tasks.KotlinCompile by tasks
compileKotlin.kotlinOptions.freeCompilerArgs += "-Xskip-runtime-version-check"
val mutesPackageName = "org.jetbrains.kotlin.test.mutes"
application {
mainClassName = "$mutesPackageName.MutedTestsSyncKt"
applicationDefaultJvmArgs = rootProject.properties.filterKeys { it.startsWith(mutesPackageName) }.map { (k, v) -> "-D$k=$v" }
}
@@ -0,0 +1,77 @@
/*
* 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.mutes
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
internal val jsonObjectMapper = jacksonObjectMapper()
data class MuteTestJson(
val id: Int,
val assignment: JsonNode,
val scope: JsonNode,
val target: JsonNode,
val resolution: JsonNode
)
internal fun createMuteTestJson(testName: String, description: String, scopeId: String, isBuildType: Boolean): MuteTestJson {
val assignmentJson = """{ "text" : "$TAG $description" }"""
val scopeJson = if (isBuildType)
"""{"buildTypes":{"buildType":[{"id":"$scopeId"}]}}"""
else
"""{"project":{"id":"$scopeId"}}"""
val targetJson = """{ "tests" : { "test" : [ { "name" : "$testName" } ] } }"""
val resolutionJson = """{ "type" : "manually" }"""
return MuteTestJson(
0,
jsonObjectMapper.readTree(assignmentJson),
jsonObjectMapper.readTree(scopeJson),
jsonObjectMapper.readTree(targetJson),
jsonObjectMapper.readTree(resolutionJson)
)
}
internal fun filterMutedTestsByScope(muteTestJson: List<MuteTestJson>, scopeId: String, isBuildType: Boolean): Map<String, MuteTestJson> {
val filterCondition = { testJson: MuteTestJson ->
if (isBuildType) {
val buildTypes = testJson.scope.get("buildTypes")
val buildTypeIds = buildTypes?.get("buildType")?.toList()?.map {
it.get("id").textValue()
} ?: listOf()
buildTypeIds.contains(scopeId)
} else {
testJson.scope.get("project")?.get("id")?.textValue() == scopeId
}
}
return muteTestJson.filter(filterCondition)
.flatMap { mutedTestJson ->
val testNames = mutedTestJson.target.get("tests").get("test").toList().map { it.get("name").textValue() }
testNames.map { testName ->
testName to mutedTestJson
}
}
.toMap()
}
internal fun transformMutedTestsToJson(flakyTests: List<MutedTest>?, scopeId: String, isBuildType: Boolean): Map<String, MuteTestJson> {
val mutedMap = mutableMapOf<String, MuteTestJson>()
if (flakyTests != null) {
for (muted in flakyTests) {
val testName = formatClassnameWithInnerClasses(muted.key)
mutedMap[testName] = createMuteTestJson(testName, muted.issue ?: "", scopeId, isBuildType)
}
}
return mutedMap
}
private fun formatClassnameWithInnerClasses(classname: String): String {
val classFindRegex = "\\.(?=[A-Z])".toRegex()
val (pkg, name) = classname.split(classFindRegex, limit = 2)
return "$pkg.${name.replace(classFindRegex, "\\$")}"
}
@@ -0,0 +1,109 @@
/*
* 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.mutes
import java.io.File
fun main() {
syncMutedTestsOnTeamCityWithDatabase()
}
/**
* Synchronize muted tests on teamcity with flaky tests in database
*
* Purpose: possibility to run flaky tests on teamcity that will not affect on build status
*/
fun syncMutedTestsOnTeamCityWithDatabase() {
val remotelyMutedTests = RemotelyMutedTests()
val locallyMutedTests = LocallyMutedTests()
val bunches = Bunches.parseRulesToBunches(locallyMutedTests.tests.keys)
syncMutedTests(remotelyMutedTests.projectTests, locallyMutedTests.projectTests)
for ((originalBunchId, foundBunchId) in bunches) {
getBuildTypeIds(originalBunchId)?.let { buildTypeIds ->
for (buildTypeId in buildTypeIds.split(",")) {
syncMutedTests(remotelyMutedTests.getTestsJson(buildTypeId), locallyMutedTests.getTestsJson(foundBunchId, buildTypeId))
}
}
}
}
private fun syncMutedTests(
remotelyMutedTests: Map<String, MuteTestJson>,
locallyMutedTests: Map<String, MuteTestJson>
) {
val deleteList = remotelyMutedTests - locallyMutedTests.keys
val uploadList = locallyMutedTests - remotelyMutedTests.keys
deleteMutedTests(deleteList)
uploadMutedTests(uploadList)
}
internal fun getMandatoryProperty(propertyName: String) = (System.getProperty(propertyName)
?: throw Exception("Property $propertyName must be set"))
object Bunches {
private val bunchRules: List<String> = readAllRulesFromFile()
internal val baseBunchId = bunchRules.first()
internal fun parseRulesToBunches(platforms: Set<String>): Map<String, String> {
return bunchRules
.map { it.split('_') }
.map { rule ->
rule.first() to (rule.find { platforms.contains(it) } ?: baseBunchId)
}.toMap()
}
private fun readAllRulesFromFile(): List<String> {
val file = File("../..", ".bunch")
if (!file.exists()) {
throw BunchException("Can't build list of rules. File '${file.canonicalPath}' doesn't exist")
}
return file.readLines()
.map { it.trim() }
.filter { it.isNotEmpty() }
}
private class BunchException(msg: String? = null) : Exception(msg)
}
private const val mutesPackageName = "org.jetbrains.kotlin.test.mutes"
internal val projectId = getMandatoryProperty("$mutesPackageName.tests.project.id")
internal fun getBuildTypeIds(bunchId: String) = System.getProperty("$mutesPackageName.$bunchId")
class RemotelyMutedTests {
val tests = getMutedTestsOnTeamcityForRootProject(projectId)
val projectTests = getTestsJson(projectId, false)
internal fun getTestsJson(scopeId: String, isBuildType: Boolean = true): Map<String, MuteTestJson> {
return filterMutedTestsByScope(tests, scopeId, isBuildType)
}
}
class LocallyMutedTests {
private val muteCommonTestKey = "COMMON"
val tests = getMutedTestsFromDatabase()
val projectTests = getTestsJson(muteCommonTestKey, projectId, false)
internal fun getTestsJson(platformId: String, scopeId: String, isBuildType: Boolean = true): Map<String, MuteTestJson> {
return transformMutedTestsToJson(tests[platformId], scopeId, isBuildType)
}
private fun getMutedTestsFromDatabase(): Map<String, List<MutedTest>> {
val mutedTestsMap = mutableMapOf<String, List<MutedTest>>()
val databaseDir = "../../tests"
val commonDatabaseFile = File(databaseDir, "mute-common.csv")
mutedTestsMap[muteCommonTestKey] = flakyTests(commonDatabaseFile)
val platformDatabaseFile = File(databaseDir, "mute-platform.csv")
File(databaseDir).walkTopDown().filter { f -> f.name.startsWith(platformDatabaseFile.name) }.toList().map { f ->
val key = if (f.extension != "csv") f.extension else Bunches.baseBunchId
mutedTestsMap[key] = flakyTests(f)
}
return mutedTestsMap
}
}
@@ -0,0 +1,88 @@
package org.jetbrains.kotlin.test.mutes
import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.module.kotlin.treeToValue
import khttp.DEFAULT_TIMEOUT
import khttp.responses.Response
import khttp.structures.authorization.Authorization
internal const val TAG = "[MUTED-BY-CSVFILE]"
private val buildServerUrl = getMandatoryProperty("org.jetbrains.kotlin.test.mutes.teamcity.server.url")
private val headers = mapOf("Content-type" to "application/json", "Accept" to "application/json")
private val authUser = object : Authorization {
override val header = "Authorization" to "Bearer ${getMandatoryProperty("org.jetbrains.kotlin.test.mutes.teamcity.server.token")}"
}
internal fun getMutedTestsOnTeamcityForRootProject(rootScopeId: String): List<MuteTestJson> {
val requestHref = "/app/rest/mutes"
val requestParams = mapOf(
"locator" to "project:(id:$rootScopeId)",
"fields" to "mute(id,assignment(text),scope(project(id),buildTypes(buildType(id))),target(tests(test(name))),resolution),nextHref"
)
val jsonResponses = traverseAll(requestHref, requestParams)
val alreadyMutedTestsOnTeamCity = jsonResponses.flatMap {
it.get("mute").filter { jn -> jn.get("assignment").get("text")?.textValue().toString().startsWith(TAG) }
}
return alreadyMutedTestsOnTeamCity.mapNotNull { jsonObjectMapper.treeToValue<MuteTestJson>(it) }
}
private fun traverseAll(requestHref: String, requestParams: Map<String, String>): List<JsonNode> {
val jsonResponses = mutableListOf<JsonNode>()
fun request(url: String, params: Map<String, String>): String {
val currentResponse = khttp.get(url, headers, params, auth = authUser)
checkResponseAndLog(currentResponse)
val currentJsonResponse = jsonObjectMapper.readTree(currentResponse.text)
jsonResponses.add(currentJsonResponse)
return currentJsonResponse.get("nextHref")?.textValue() ?: ""
}
var nextHref = request("$buildServerUrl$requestHref", requestParams)
while (!nextHref.isBlank()) {
nextHref = request("$buildServerUrl$nextHref", emptyMap())
}
return jsonResponses
}
internal fun uploadMutedTests(uploadMap: Map<String, MuteTestJson>) {
for ((_, muteTestJson) in uploadMap) {
val response = khttp.post(
"$buildServerUrl/app/rest/mutes",
headers = headers,
data = jsonObjectMapper.writeValueAsString(muteTestJson),
auth = authUser
)
checkResponseAndLog(response)
}
}
internal fun deleteMutedTests(deleteMap: Map<String, MuteTestJson>) {
for ((_, muteTestJson) in deleteMap) {
val response = khttp.delete(
"$buildServerUrl/app/rest/mutes/id:${muteTestJson.id}",
headers = headers,
auth = authUser,
timeout = DEFAULT_TIMEOUT * 2
)
try {
checkResponseAndLog(response)
} catch (e: Exception) {
System.err.println(e.message)
}
}
}
private fun checkResponseAndLog(response: Response) {
val isResponseBad = response.connection.responseCode !in 200..299
if (isResponseBad) {
throw Exception(
"${response.request.method}-request to ${response.request.url} failed:\n" +
"${response.text}\n" +
"${response.request.data ?: ""}"
)
}
}