[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:
@@ -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" }
|
||||
}
|
||||
+77
@@ -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, "\\$")}"
|
||||
}
|
||||
+109
@@ -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
|
||||
}
|
||||
}
|
||||
+88
@@ -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 ?: ""}"
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user