com.jetbrains.pluginverifier.output.teamcity.TeamCityResultPrinter.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of verifier-cli Show documentation
Show all versions of verifier-cli Show documentation
Command-line interface for JetBrains Plugin Verifier with set of high-level tasks for plugin and IDE validation
/*
* Copyright 2000-2020 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/
package com.jetbrains.pluginverifier.output.teamcity
import com.jetbrains.plugin.structure.base.utils.pluralize
import com.jetbrains.plugin.structure.base.utils.rethrowIfInterrupted
import com.jetbrains.plugin.structure.ide.VersionComparatorUtil
import com.jetbrains.plugin.structure.intellij.version.IdeVersion
import com.jetbrains.pluginverifier.PluginVerificationResult
import com.jetbrains.pluginverifier.PluginVerificationTarget
import com.jetbrains.pluginverifier.dependencies.MissingDependency
import com.jetbrains.pluginverifier.repository.Browseable
import com.jetbrains.pluginverifier.repository.PluginInfo
import com.jetbrains.pluginverifier.repository.PluginRepository
import com.jetbrains.pluginverifier.repository.repositories.marketplace.MarketplaceRepository
import com.jetbrains.pluginverifier.repository.repositories.marketplace.UpdateInfo
import com.jetbrains.pluginverifier.results.problems.ClassNotFoundProblem
import com.jetbrains.pluginverifier.results.problems.CompatibilityProblem
import com.jetbrains.pluginverifier.tasks.InvalidPluginFile
import com.jetbrains.pluginverifier.tasks.checkIde.MissingCompatibleVersionProblem
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import java.util.Locale
class TeamCityResultPrinter(
private val tcLog: TeamCityLog,
private val groupBy: GroupBy,
private val repository: PluginRepository
) {
companion object {
private val LOG: Logger = LoggerFactory.getLogger(TeamCityResultPrinter::class.java)
/**
* Converts string like "com.some.package.name.MyClassNameProblem" to "my class name"
*/
fun convertProblemClassNameToSentence(clazz: Class): String {
val name = clazz.name.substringAfterLast(".")
var words = name.split("(?=[A-Z])".toRegex()).dropWhile { it.isEmpty() }
if (words.isEmpty()) {
return name.lowercase(Locale.getDefault())
}
if (words.last() == "Problem") {
words = words.dropLast(1)
}
return words.joinToString(" ") { it.lowercase(Locale.getDefault()) }
}
fun printInvalidPluginFiles(tcLog: TeamCityLog, invalidPluginFiles: List) {
if (invalidPluginFiles.isNotEmpty()) {
val testName = "(invalid plugins)"
tcLog.testStarted(testName).use {
val message = buildString {
for ((pluginFile, pluginErrors) in invalidPluginFiles) {
append(pluginFile)
for (pluginError in pluginErrors) {
append(" $pluginError")
}
}
}
tcLog.testFailed(testName, message, "")
}
}
}
}
fun printNoCompatibleVersionsProblems(missingVersionsProblems: List): TeamCityHistory {
val failedTests = arrayListOf()
when (groupBy) {
GroupBy.BY_PLUGIN -> {
missingVersionsProblems.forEach { missingProblem ->
val testSuiteName = missingProblem.pluginId
tcLog.testSuiteStarted(testSuiteName).use {
val testName = "(no compatible version)"
tcLog.testStarted(testName).use {
failedTests += TeamCityTest(testSuiteName, testName)
tcLog.testFailed(testName, "#$missingProblem\n", "")
}
}
}
}
GroupBy.BY_PROBLEM_TYPE -> {
val testSuiteName = "(no compatible version)"
tcLog.testSuiteStarted(testSuiteName).use {
missingVersionsProblems.forEach { problem ->
tcLog.testSuiteStarted(problem.pluginId).use {
val testName = problem.pluginId
tcLog.testStarted(testName).use {
failedTests += TeamCityTest(testSuiteName, testName)
tcLog.testFailed(testName, "#$problem\n", "")
}
}
}
}
}
}
return TeamCityHistory(failedTests)
}
fun printResults(results: List): TeamCityHistory =
when (groupBy) {
GroupBy.BY_PROBLEM_TYPE -> groupByProblemType(results)
GroupBy.BY_PLUGIN -> groupByPlugin(results)
}
private fun PluginVerificationResult.getProblems(): Set =
if (this is PluginVerificationResult.Verified) {
compatibilityProblems
} else {
emptySet()
}
private fun collectMissingDependenciesForRequiringPlugins(results: List): Map> {
val missingToRequiring = mutableMapOf>()
results.filterIsInstance().forEach {
it.directMissingMandatoryDependencies.forEach { missingDependency ->
missingToRequiring.getOrPut(missingDependency) { hashSetOf() } += it.plugin
}
}
return missingToRequiring
}
//pluginOne
//....(1.0)
//........#invoking unknown method
//............someClass
//........#accessing to unknown class
//............another class
//....(1.2)
//........#invoking unknown method
//............someClass
//........missing non-optional dependency dep#1
//........missing non-optional dependency pluginOne:1.2 -> otherPlugin:3.3 -> dep#2 (it doesn't have a compatible build with IDE #IU-162.1121.10)
//........missing optional dependency dep#3
//pluginTwo
//...and so on...
private fun groupByPlugin(results: List): TeamCityHistory {
val failedTests = arrayListOf()
val verificationTargets = results.map { it.verificationTarget }.distinct()
val targetToLastPluginVersions = requestLastVersionsOfCheckedPlugins(verificationTargets)
results.groupBy { it.plugin.pluginId }.forEach { (pluginId, pluginResults) ->
failedTests += printResultsForSpecificPluginId(pluginId, pluginResults, targetToLastPluginVersions)
}
return TeamCityHistory(failedTests)
}
/**
* Generates the test group with name equal to the [pluginId]
* and for each verified version of the plugin
* creates a separate test. Thus the layout is as follows:
*
* ```
* plugin.id.one <- test group equal to [pluginId]
* ....(1.0) <- test name equal to the plugin's version
* ........results of the plugin.id.one:1.0
* ....(2.0)
* ........results of the plugin.id.one:2.0
* plugin.id.two
* ....(1.5)
* ........results of the plugin id.two:1.5
* ....and so on...
* ```
*/
private fun printResultsForSpecificPluginId(
pluginId: String,
pluginResults: List,
targetToLastPluginVersions: Map>
): List {
val failedTests = arrayListOf()
tcLog.testSuiteStarted(pluginId).use {
pluginResults.groupBy { it.plugin.version }.forEach { versionToResults ->
versionToResults.value.forEach { result ->
val testName = getPluginVersionAsTestName(result.plugin, result.verificationTarget, targetToLastPluginVersions)
tcLog.testStarted(testName).use {
when (result) {
is PluginVerificationResult.Verified -> {
val message = getMessageCompatibilityProblemsAndMissingDependencies(result.plugin, result.compatibilityProblems, result.directMissingMandatoryDependencies)
if (message != null) {
failedTests += TeamCityTest(pluginId, testName)
tcLog.testFailed(testName, message, "")
}
}
is PluginVerificationResult.InvalidPlugin -> {
val message = "Plugin is invalid: ${result.pluginStructureErrors.joinToString()}"
failedTests += TeamCityTest(pluginId, testName)
tcLog.testFailed(testName, message, "")
Unit
}
else -> Unit
}
}
}
}
}
return failedTests
}
private fun getMessageCompatibilityProblemsAndMissingDependencies(
plugin: PluginInfo,
problems: Set,
missingDependencies: List
): String? {
val mandatoryMissingDependencies = missingDependencies.filterNot { it.dependency.isOptional }
if (problems.isNotEmpty() || mandatoryMissingDependencies.isNotEmpty()) {
return buildString {
appendLine(getPluginOverviewLink(plugin))
if (problems.isNotEmpty()) {
appendLine("$plugin has ${problems.size} compatibility " + "problem".pluralize(problems.size))
}
if (missingDependencies.isNotEmpty()) {
if (problems.isNotEmpty()) {
appendLine("Some problems might have been caused by missing dependencies: ")
}
for (missingDependency in missingDependencies) {
appendLine("Missing dependency ${missingDependency.dependency}: ${missingDependency.missingReason}")
}
}
val notFoundClassesProblems = problems.filterIsInstance()
val problemsContent = if (missingDependencies.isNotEmpty() && notFoundClassesProblems.size > 20) {
getTooManyUnknownClassesProblems(notFoundClassesProblems, problems)
} else {
getProblemsContent(problems)
}
appendLine()
appendLine(problemsContent)
}
}
return null
}
private fun getPluginOverviewLink(plugin: PluginInfo): String {
val url = (plugin as? Browseable)?.browserUrl ?: return ""
return "Plugin URL: $url"
}
private fun getProblemsContent(problems: Iterable): String = buildString {
for ((shortDescription, problemsWithShortDescription) in problems.groupBy { it.shortDescription }) {
appendLine("#$shortDescription")
for (compatibilityProblem in problemsWithShortDescription) {
appendLine(" ${compatibilityProblem.fullDescription}")
}
}
}
private fun getTooManyUnknownClassesProblems(
notFoundClassesProblems: List,
problems: Set
): String {
val otherProblems = getProblemsContent(problems.filterNot { it in notFoundClassesProblems })
return buildString {
appendLine("There are too many missing classes (${notFoundClassesProblems.size});")
appendLine("it's probably because of missing plugins or modules")
appendLine("some not-found classes: [${notFoundClassesProblems.take(20).map { it.unresolved }.joinToString()}...];")
if (otherProblems.isNotEmpty()) {
appendLine("Other problems: ")
appendLine(otherProblems)
}
}
}
/**
* For each IDE returns the last versions f the plugin available in the [repository]
* and compatible with this IDE.
*/
private fun requestLastVersionsOfCheckedPlugins(verificationTargets: List): Map> =
verificationTargets.associateWith { target ->
try {
when (target) {
is PluginVerificationTarget.IDE -> {
requestLastVersionsOfEachCompatiblePlugins(target.ideVersion)
}
is PluginVerificationTarget.Plugin -> emptyList()
}
} catch (e: Exception) {
e.rethrowIfInterrupted()
LOG.info("Unable to determine the last compatible updates of IDE $target", e)
@Suppress("RemoveExplicitTypeArguments")
emptyList()
}
}
private fun requestLastVersionsOfEachCompatiblePlugins(ideVersion: IdeVersion): List {
val plugins = runCatching { repository.getLastCompatiblePlugins(ideVersion) }.getOrDefault(emptyList())
return plugins.groupBy { it.pluginId }.mapValues { (_, sameIdPlugins) ->
if (repository is MarketplaceRepository) {
sameIdPlugins.maxByOrNull { (it as UpdateInfo).updateId }
} else {
sameIdPlugins.maxWithOrNull(compareBy(VersionComparatorUtil.COMPARATOR) { it.version })
}
}.values.filterNotNull()
}
/**
* Generates a TC test name in which the verification report will be printed.
*
* The test name is the version of the plugin
* plus, possibly, the suffix 'newest' indicating that this
* is the last available version of the plugin.
*
* The test name is wrapped into parenthesis like so `()`
* to make TC display the version as a whole. Not doing this
* would lead to TC arbitrarily splitting the version.
*
* Examples are:
* 1) `(173.3727.144.8)`
* 2) `(173.3727.244.997 - newest)`
*/
private fun getPluginVersionAsTestName(
pluginInfo: PluginInfo,
verificationTarget: PluginVerificationTarget,
ideLastPluginVersions: Map>
): String {
val lastVersions = ideLastPluginVersions.getOrDefault(verificationTarget, emptyList())
return if (pluginInfo in lastVersions) {
"(${pluginInfo.version} - newest)"
} else {
"(${pluginInfo.version})"
}
}
//accessing to unknown class SomeClass
//....(pluginOne:1.2.0)
//....(pluginTwo:2.0.0)
//invoking unknown method method
//....(pluginThree:1.0.0)
//missing dependencies
//....(missing#1)
//.........Required for plugin1, plugin2, plugin3
private fun groupByProblemType(results: List): TeamCityHistory {
val failedTests = arrayListOf()
val problem2Plugin: MutableMap> = hashMapOf()
for (result in results) {
for (problem in result.getProblems()) {
problem2Plugin.getOrPut(problem) { hashSetOf() } += result.plugin
}
}
val allProblems = problem2Plugin.keys
for ((problemClass, problemsOfClass) in allProblems.groupBy { it.javaClass }) {
val prefix = convertProblemClassNameToSentence(problemClass)
val testSuiteName = "($prefix)"
tcLog.testSuiteStarted(testSuiteName).use {
for (problem in problemsOfClass) {
for (plugin in (problem2Plugin[problem] ?: emptySet())) {
tcLog.testSuiteStarted(problem.shortDescription).use {
val testName = "($plugin)"
tcLog.testStarted(testName).use {
failedTests += TeamCityTest(testSuiteName, testName)
tcLog.testFailed(testName, getPluginOverviewLink(plugin) + "\nPlugin: $plugin", problem.fullDescription)
}
}
}
}
}
}
val missingToRequired = collectMissingDependenciesForRequiringPlugins(results)
if (missingToRequired.isNotEmpty()) {
val testSuiteName = "(missing dependencies)"
tcLog.testSuiteStarted(testSuiteName).use {
missingToRequired.entries.forEach { (key, values) ->
val testName = "($key)"
tcLog.testStarted(testName).use {
failedTests += TeamCityTest(testSuiteName, testName)
tcLog.testFailed(testName, "Required for ${values.joinToString()}", "")
}
}
}
}
return TeamCityHistory(failedTests)
}
enum class GroupBy(private val arg: String) {
BY_PROBLEM_TYPE("problem_type"),
BY_PLUGIN("plugin");
companion object {
@JvmStatic
fun parse(groupValue: String?): GroupBy =
values().find { it.arg == groupValue } ?: BY_PLUGIN
}
}
}