Skip to content

Commit

Permalink
MBS-9952 Add gradle configuration errors to the build verdict (#674)
Browse files Browse the repository at this point in the history
  • Loading branch information
sboishtyan authored Dec 11, 2020
1 parent 5159e3d commit 246b6f3
Show file tree
Hide file tree
Showing 17 changed files with 603 additions and 132 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,14 @@ fun Throwable.getStackTraceString(): String {

return stringWriter.buffer.toString()
}

fun Throwable.getCausesRecursively(): List<Throwable> {
val causes = mutableListOf<Throwable>()
var current = this
while (current.cause != null) {
val cause = current.cause!!
causes.add(cause)
current = cause
}
return causes
}
4 changes: 0 additions & 4 deletions subprojects/gradle/build-verdict/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ plugins {

dependencies {
implementation(gradleApi())
implementation(Dependencies.Gradle.kotlinPlugin)
implementation(Dependencies.Gradle.androidPlugin) {
because("Ad-hoc TODO add documentation MBS-9695")
}
implementation(project(":common:throwable-utils"))
implementation(project(":gradle:kotlin-dsl-support"))
implementation(project(":gradle:ci-logger"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,21 @@ class BuildVerdictPlugin : Plugin<ProjectInternal> {

if (project.pluginIsEnabled) {
val extension = project.extensions.create<BuildVerdictPluginExtension>("buildVerdict")
val outputDir = extension.outputDir
val services = BuildVerdictPluginServices()
project.gradle.addListener(services.gradleTaskExecutionListener())
project.gradle.addLogEventListener(services.gradleLogEventListener())
val configurationListener = services.gradleConfigurationListener(outputDir, project.ciLogger)
project.gradle.addBuildListener(configurationListener)
project.gradle.taskGraph.whenReady { graph ->
val outputDir = extension.outputDir.get().asFile
project.gradle.buildFinished(services.gradleBuildFinishedListener(graph, outputDir, project.ciLogger))
project.gradle.removeListener(configurationListener)
project.gradle.addBuildListener(
services.gradleBuildFinishedListener(
graph,
outputDir,
project.ciLogger
)
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.avito.android.build_verdict.internal

import org.gradle.BuildListener
import org.gradle.BuildResult
import org.gradle.api.initialization.Settings
import org.gradle.api.invocation.Gradle

abstract class BaseBuildListener : BuildListener {
override fun buildStarted(gradle: Gradle) {}

override fun settingsEvaluated(settings: Settings) {}

override fun projectsLoaded(gradle: Gradle) {}

override fun projectsEvaluated(gradle: Gradle) {}

override fun buildFinished(result: BuildResult) {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.avito.android.build_verdict.internal

import com.avito.android.build_verdict.internal.writer.BuildVerdictWriter
import org.gradle.BuildResult

internal class BuildConfigurationFailureListener(
private val writer: BuildVerdictWriter
) : BaseBuildListener() {

override fun buildFinished(result: BuildResult) {
result.failure?.let { failure ->
writer.write(
BuildVerdict.Configuration(
error = Error.from(failure)
)
)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
package com.avito.android.build_verdict.internal

import com.avito.android.build_verdict.internal.writer.BuildVerdictWriter
import com.avito.utils.getStackTraceString
import org.gradle.BuildResult
import org.gradle.api.Action
import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.util.Path

internal class BuildFailureListener(
internal class BuildExecutionFailureListener(
private val graph: TaskExecutionGraph,
private val logs: Map<Path, LogsTextBuilder>,
private val writer: BuildVerdictWriter
) : Action<BuildResult> {
) : BaseBuildListener() {

override fun execute(result: BuildResult) {
override fun buildFinished(result: BuildResult) {
result.failure?.apply {
onFailure(this)
}
Expand All @@ -25,22 +23,14 @@ internal class BuildFailureListener(
.filter { it.state.failure != null }

writer.write(
BuildVerdict(
rootError = Error(
message = failure.localizedMessage,
stackTrace = failure.getStackTraceString()
),
BuildVerdict.Execution(
error = Error.from(failure),
failedTasks = failedTasks.map { task ->
FailedTask(
name = task.name,
projectPath = task.project.path,
errorOutput = logs[Path.path(task.path)]?.build() ?: "No error logs",
originalError = task.state.failure!!.let { error ->
Error(
message = error.localizedMessage,
stackTrace = error.getStackTraceString()
)
}
error = task.state.failure!!.let { error -> Error.from(error) }
)
}
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package com.avito.android.build_verdict.internal

internal data class Error(
val message: String,
val stackTrace: String
)
internal sealed class BuildVerdict {

internal data class BuildVerdict(
val rootError: Error,
val failedTasks: List<FailedTask>
)
abstract val error: Error

data class Configuration(override val error: Error) : BuildVerdict()

data class Execution(
override val error: Error,
val failedTasks: List<FailedTask>
) : BuildVerdict()
}

internal data class FailedTask(
val name: String,
val projectPath: String,
val errorOutput: String,
val originalError: Error
val error: Error
)
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,26 @@ import com.avito.android.build_verdict.internal.writer.PlainTextBuildVerdictWrit
import com.avito.android.build_verdict.internal.writer.RawBuildVerdictWriter
import com.avito.utils.logging.CILogger
import com.google.gson.GsonBuilder
import org.gradle.BuildResult
import org.gradle.api.Action
import org.gradle.BuildListener
import org.gradle.api.execution.TaskExecutionGraph
import org.gradle.api.execution.TaskExecutionListener
import org.gradle.api.file.Directory
import org.gradle.api.provider.Provider
import org.gradle.internal.logging.events.OutputEventListener
import org.gradle.internal.operations.OperationIdentifier
import org.gradle.util.Path
import java.io.File
import java.util.concurrent.ConcurrentHashMap

internal class BuildVerdictPluginServices {

private val listeners = ConcurrentHashMap<OperationIdentifier, LogMessageListener>()
private val logs = ConcurrentHashMap<Path, LogsTextBuilder>()
private val gson by lazy {
GsonBuilder()
.disableHtmlEscaping()
.setPrettyPrinting()
.create()
}

fun gradleLogEventListener(): OutputEventListener {
return GradleLogEventListener(
Expand All @@ -36,28 +43,42 @@ internal class BuildVerdictPluginServices {
)
}

fun gradleConfigurationListener(
outputDir: Provider<Directory>,
logger: CILogger
): BuildListener {
return BuildConfigurationFailureListener(
createWriter(
outputDir = outputDir,
logger = logger
)
)
}

fun gradleBuildFinishedListener(
graph: TaskExecutionGraph,
outputDir: File,
outputDir: Provider<Directory>,
logger: CILogger
): BuildListener = BuildExecutionFailureListener(
graph = graph,
logs = logs,
writer = createWriter(outputDir, logger)
)

private fun createWriter(
outputDir: Provider<Directory>,
logger: CILogger
): Action<BuildResult> {
return BuildFailureListener(
graph = graph,
logs = logs,
writer = CompositeBuildVerdictWriter(
writers = listOf(
RawBuildVerdictWriter(
buildVerdictDir = outputDir,
logger = logger,
gson = GsonBuilder()
.disableHtmlEscaping()
.setPrettyPrinting()
.create()
),
PlainTextBuildVerdictWriter(
buildVerdictDir = outputDir,
logger = logger
)
): CompositeBuildVerdictWriter {
return CompositeBuildVerdictWriter(
writers = listOf(
RawBuildVerdictWriter(
buildVerdictDir = outputDir,
logger = logger,
gson = gson
),
PlainTextBuildVerdictWriter(
buildVerdictDir = outputDir,
logger = logger
)
)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.avito.android.build_verdict.internal

import com.avito.utils.getCausesRecursively
import com.avito.utils.getStackTraceString
import org.gradle.internal.exceptions.MultiCauseException

internal sealed class Error {

abstract val message: String

data class Multi(
override val message: String,
val errors: List<Single>
) : Error()

data class Single(
override val message: String,
val stackTrace: String,
val causes: List<Cause>
) : Error()

data class Cause(val message: String)

companion object {
fun from(throwable: Throwable) = when {
throwable is MultiCauseException && throwable.causes.size > 1 -> Multi(
message = throwable.localizedMessage,
errors = throwable.causes.map { it.toSingle() }
)
else -> throwable.toSingle()
}

private fun Throwable.toSingle() = Single(
message = localizedMessage,
stackTrace = getStackTraceString(),
causes = getCausesRecursively().map { Cause(it.localizedMessage ?: "${it::class.java} (no error message)") }
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.avito.android.build_verdict.internal.writer

import com.avito.android.build_verdict.internal.BuildVerdict
import com.avito.android.build_verdict.internal.Error

internal fun BuildVerdict.Configuration.plainText(): String {
return error.plainText().trimIndent()
}

private fun Error.plainText() = when (this) {
is Error.Single -> buildString {
appendln("FAILURE: Build failed with an exception.")
appendln()
appendln("* What went wrong:")
append(plainText())
}
is Error.Multi -> plainText()
}

private fun Error.Single.plainText(): String {
return buildString {
appendln(message)
causes.forEachIndexed { index, cause ->
append("\t".repeat(index + 1))
append("> ")
appendln(cause.message.trimIndent())
}
}
}

private fun Error.Multi.plainText(): String {
return buildString {
appendln("FAILURE: $message")
appendln()
errors.forEachIndexed { index, error ->
appendln("${index + 1}: Task failed with an exception.")
appendln("-----------")
appendln(error.plainText().trimIndent())
if (index < errors.size - 1) {
appendln()
}
}
}
}
Loading

0 comments on commit 246b6f3

Please sign in to comment.