diff --git a/subprojects/common/throwable-utils/src/main/kotlin/com/avito/utils/Exception.kt b/subprojects/common/throwable-utils/src/main/kotlin/com/avito/utils/Exception.kt index cbe12df390..0c38b40d78 100644 --- a/subprojects/common/throwable-utils/src/main/kotlin/com/avito/utils/Exception.kt +++ b/subprojects/common/throwable-utils/src/main/kotlin/com/avito/utils/Exception.kt @@ -13,3 +13,14 @@ fun Throwable.getStackTraceString(): String { return stringWriter.buffer.toString() } + +fun Throwable.getCausesRecursively(): List { + val causes = mutableListOf() + var current = this + while (current.cause != null) { + val cause = current.cause!! + causes.add(cause) + current = cause + } + return causes +} diff --git a/subprojects/gradle/build-verdict/build.gradle.kts b/subprojects/gradle/build-verdict/build.gradle.kts index ad7944a340..a9e964e333 100644 --- a/subprojects/gradle/build-verdict/build.gradle.kts +++ b/subprojects/gradle/build-verdict/build.gradle.kts @@ -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")) diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/BuildVerdictPlugin.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/BuildVerdictPlugin.kt index 99f554f512..ce81394b68 100644 --- a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/BuildVerdictPlugin.kt +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/BuildVerdictPlugin.kt @@ -29,12 +29,21 @@ class BuildVerdictPlugin : Plugin { if (project.pluginIsEnabled) { val extension = project.extensions.create("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 + ) + ) } } } diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BaseBuildListener.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BaseBuildListener.kt new file mode 100644 index 0000000000..683638be3c --- /dev/null +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BaseBuildListener.kt @@ -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) {} +} diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildConfigurationFailureListener.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildConfigurationFailureListener.kt new file mode 100644 index 0000000000..e04f51de63 --- /dev/null +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildConfigurationFailureListener.kt @@ -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) + ) + ) + } + } +} diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildFailureListener.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildExecutionFailureListener.kt similarity index 59% rename from subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildFailureListener.kt rename to subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildExecutionFailureListener.kt index 59af6de2c2..51a7d90d38 100644 --- a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildFailureListener.kt +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildExecutionFailureListener.kt @@ -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, private val writer: BuildVerdictWriter -) : Action { +) : BaseBuildListener() { - override fun execute(result: BuildResult) { + override fun buildFinished(result: BuildResult) { result.failure?.apply { onFailure(this) } @@ -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) } ) } ) diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildVerdict.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildVerdict.kt index 457ebb9b5a..9960c42ae8 100644 --- a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildVerdict.kt +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildVerdict.kt @@ -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 -) + abstract val error: Error + + data class Configuration(override val error: Error) : BuildVerdict() + + data class Execution( + override val error: Error, + val failedTasks: List + ) : BuildVerdict() +} internal data class FailedTask( val name: String, val projectPath: String, val errorOutput: String, - val originalError: Error + val error: Error ) diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildVerdictPluginServices.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildVerdictPluginServices.kt index 206829d9b1..5b7bc7b728 100644 --- a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildVerdictPluginServices.kt +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/BuildVerdictPluginServices.kt @@ -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() private val logs = ConcurrentHashMap() + private val gson by lazy { + GsonBuilder() + .disableHtmlEscaping() + .setPrettyPrinting() + .create() + } fun gradleLogEventListener(): OutputEventListener { return GradleLogEventListener( @@ -36,28 +43,42 @@ internal class BuildVerdictPluginServices { ) } + fun gradleConfigurationListener( + outputDir: Provider, + logger: CILogger + ): BuildListener { + return BuildConfigurationFailureListener( + createWriter( + outputDir = outputDir, + logger = logger + ) + ) + } + fun gradleBuildFinishedListener( graph: TaskExecutionGraph, - outputDir: File, + outputDir: Provider, + logger: CILogger + ): BuildListener = BuildExecutionFailureListener( + graph = graph, + logs = logs, + writer = createWriter(outputDir, logger) + ) + + private fun createWriter( + outputDir: Provider, logger: CILogger - ): Action { - 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 ) ) ) diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/Error.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/Error.kt new file mode 100644 index 0000000000..633ddc13c2 --- /dev/null +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/Error.kt @@ -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 + ) : Error() + + data class Single( + override val message: String, + val stackTrace: String, + val causes: List + ) : 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)") } + ) + } +} diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/BuildVerdictConfigurationPlainText.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/BuildVerdictConfigurationPlainText.kt new file mode 100644 index 0000000000..1fbf9f3a88 --- /dev/null +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/BuildVerdictConfigurationPlainText.kt @@ -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() + } + } + } +} diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/BuildVerdictExecutionPlainText.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/BuildVerdictExecutionPlainText.kt new file mode 100644 index 0000000000..2424ab5482 --- /dev/null +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/BuildVerdictExecutionPlainText.kt @@ -0,0 +1,55 @@ +package com.avito.android.build_verdict.internal.writer + +import com.avito.android.build_verdict.internal.BuildVerdict +import com.avito.android.build_verdict.internal.Error +import com.avito.android.build_verdict.internal.FailedTask + +internal fun BuildVerdict.Execution.plainText(): String { + require(failedTasks.isNotEmpty()) { + "Must have at least one failed task" + } + return when (failedTasks.size) { + 1 -> failedTasks[0].plainText() + else -> buildString { + appendln("FAILURE: Build completed with ${failedTasks.size} failed tasks.") + appendln() + failedTasks.forEachIndexed { index, task -> + appendln("${index + 1}: ") + appendln(task.plainText().trimIndent()) + } + } + }.trimIndent() +} + +private fun FailedTask.plainText() = buildString { + appendln(error.plainText().trimIndent()) + appendln() + appendln("* Error logs:") + appendln(errorOutput.trimIndent()) +} + +private fun Error.plainText() = buildString { + appendln("* What went wrong:") + when (this@plainText) { + is Error.Single -> appendln(plainText().trimIndent()) + is Error.Multi -> { + appendln(message.trimIndent()) + errors.forEachIndexed { index, error -> + append("${1 + index}: ") + appendln(error.plainText()) + if (index < errors.size - 1) { + appendln("==============================================================================") + } + } + } + } +} + +private fun Error.Single.plainText() = buildString { + appendln(message.trimIndent()) + causes.forEachIndexed { index, cause -> + append("\t".repeat(index + 1)) + append("> ") + appendln(cause.message.trimIndent()) + } +} diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/PlainTextBuildVerdictWriter.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/PlainTextBuildVerdictWriter.kt index 6e26c9636c..4c51267764 100644 --- a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/PlainTextBuildVerdictWriter.kt +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/PlainTextBuildVerdictWriter.kt @@ -2,35 +2,24 @@ package com.avito.android.build_verdict.internal.writer import com.avito.android.build_verdict.internal.BuildVerdict import com.avito.utils.logging.CILogger +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider import java.io.File internal class PlainTextBuildVerdictWriter( - private val buildVerdictDir: File, + private val buildVerdictDir: Provider, private val logger: CILogger ) : BuildVerdictWriter { override fun write(buildVerdict: BuildVerdict) { - val dir = buildVerdictDir.apply { mkdirs() } + val dir = buildVerdictDir.get().asFile.apply { mkdirs() } val file = File(dir, buildVerdictFileName) file.createNewFile() file.writeText( - """ -Your build FAILED: "${buildVerdict.rootError.message}" -------------------------------------------------------- - -Failed tasks: -${ - StringBuilder().apply { - val lineSeparator = System.lineSeparator() - buildVerdict.failedTasks.forEachIndexed { index, failedTask -> - append("${index + 1}: Task ${failedTask.projectPath}:${failedTask.name}$lineSeparator") - append("* What went wrong:$lineSeparator") - append("${failedTask.errorOutput.trimIndent()}$lineSeparator") - append("________________________________________") - append(lineSeparator) - } - } - }""".trimMargin() + when (buildVerdict) { + is BuildVerdict.Execution -> buildVerdict.plainText() + is BuildVerdict.Configuration -> buildVerdict.plainText() + } ) logger.warn("Pretty build verdict at $file") } diff --git a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/RawBuildVerdictWriter.kt b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/RawBuildVerdictWriter.kt index f0bc407b3d..6b7e1c646f 100644 --- a/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/RawBuildVerdictWriter.kt +++ b/subprojects/gradle/build-verdict/src/main/kotlin/com/avito/android/build_verdict/internal/writer/RawBuildVerdictWriter.kt @@ -3,18 +3,20 @@ package com.avito.android.build_verdict.internal.writer import com.avito.android.build_verdict.internal.BuildVerdict import com.avito.utils.logging.CILogger import com.google.gson.Gson +import org.gradle.api.file.Directory +import org.gradle.api.provider.Provider import java.io.File internal class RawBuildVerdictWriter( private val gson: Gson, - private val buildVerdictDir: File, + private val buildVerdictDir: Provider, private val logger: CILogger ) : BuildVerdictWriter { override fun write(buildVerdict: BuildVerdict) { val verdict = gson.toJson( buildVerdict ) - val dir = buildVerdictDir.apply { mkdirs() } + val dir = buildVerdictDir.get().asFile.apply { mkdirs() } val file = File(dir, buildVerdictFileName) file.createNewFile() file.writeText( diff --git a/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BaseBuildVerdictTest.kt b/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BaseBuildVerdictTest.kt new file mode 100644 index 0000000000..5b1ca6fb50 --- /dev/null +++ b/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BaseBuildVerdictTest.kt @@ -0,0 +1,75 @@ +package com.avito.android.build_verdict + +import com.avito.android.build_verdict.internal.Error +import com.avito.android.build_verdict.internal.Error.Multi +import com.avito.android.build_verdict.internal.Error.Single +import com.avito.test.gradle.TestProjectGenerator +import com.avito.test.gradle.module.AndroidAppModule +import com.avito.test.gradle.module.Module +import com.google.common.truth.Truth.assertWithMessage +import com.google.gson.GsonBuilder +import com.google.gson.JsonDeserializationContext +import com.google.gson.JsonDeserializer +import com.google.gson.JsonElement +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.lang.reflect.Type +import com.avito.android.build_verdict.internal.writer.PlainTextBuildVerdictWriter.Companion.buildVerdictFileName as plainTextVerdict +import com.avito.android.build_verdict.internal.writer.RawBuildVerdictWriter.Companion.buildVerdictFileName as rawVerdict + +abstract class BaseBuildVerdictTest { + + @field:TempDir + lateinit var temp: File + + protected val jsonBuildVerdict by lazy { + File(temp, "outputs/build-verdict/$rawVerdict") + } + protected val plainTextBuildVerdict by lazy { + File(temp, "outputs/build-verdict/$plainTextVerdict") + } + + protected val gson = GsonBuilder() + .registerTypeAdapter( + Error::class.java, + object : JsonDeserializer { + override fun deserialize( + json: JsonElement, + type: Type, + context: JsonDeserializationContext + ): Error { + return when { + json.asJsonObject.has("errors") -> context.deserialize(json, Multi::class.java) + else -> context.deserialize(json, Single::class.java) + } + } + } + ).create() + + protected fun generateProject( + module: Module = AndroidAppModule( + name = appName + ) + ) { + TestProjectGenerator( + plugins = listOf("com.avito.android.build-verdict"), + modules = listOf(module) + ).generateIn(temp) + } + + protected fun assertBuildVerdictFileExist( + exist: Boolean + ) { + assertWithMessage("$jsonBuildVerdict is ${if (exist) "" else "not"} present") + .that(jsonBuildVerdict.exists()) + .isEqualTo(exist) + + assertWithMessage("$plainTextBuildVerdict is ${if (exist) "" else "not"} present") + .that(plainTextBuildVerdict.exists()) + .isEqualTo(exist) + } + + protected companion object { + const val appName = "app" + } +} diff --git a/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BuildVerdictPluginConfigurationPhaseTest.kt b/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BuildVerdictPluginConfigurationPhaseTest.kt new file mode 100644 index 0000000000..64299bfe36 --- /dev/null +++ b/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BuildVerdictPluginConfigurationPhaseTest.kt @@ -0,0 +1,129 @@ +package com.avito.android.build_verdict + +import com.avito.android.build_verdict.internal.BuildVerdict +import com.avito.android.build_verdict.internal.Error.Multi +import com.avito.android.build_verdict.internal.Error.Single +import com.avito.test.gradle.dependencies.GradleDependency.Safe.Companion.project +import com.avito.test.gradle.gradlew +import com.avito.test.gradle.module.AndroidAppModule +import com.avito.test.gradle.module.KotlinModule +import com.google.common.truth.Truth.assertThat +import org.junit.jupiter.api.Test + +class BuildVerdictPluginConfigurationPhaseTest : BaseBuildVerdictTest() { + + @Test + fun `configuration success`() { + generateProject() + + val result = gradlew( + temp, + "help", + dryRun = true, + expectFailure = false + ) + + result.assertThat().buildSuccessful() + assertBuildVerdictFileExist(false) + } + + @Test + fun `configuration fails - build verdict contains gradle fail error`() { + generateProject( + AndroidAppModule( + name = appName, + buildGradleExtra = """illegal gradle text""" + ) + ) + + val result = gradlew( + temp, + "help", + dryRun = true, + expectFailure = true + ) + result.assertThat().buildFailed() + assertBuildVerdictFileExist(true) + assertThat(plainTextBuildVerdict.readText()).isEqualTo(configurationIllegalMethodFails(temp)) + val actualBuildVerdict = gson.fromJson(jsonBuildVerdict.readText(), BuildVerdict.Configuration::class.java) + + assertThat(actualBuildVerdict.error.message).isEqualTo("Build completed with 2 failures.") + + val errors = (actualBuildVerdict.error as Multi).errors + + assertThat(errors).hasSize(2) + + @Suppress("MaxLineLength") + errors[0].assertSingleError( + expectedMessageLines = listOf( + "$temp/app/build.gradle' line: 9", + "A problem occurred evaluating project ':app'." + ), + expectedCauseMessages = listOf( + "A problem occurred evaluating project ':app'.", + "Could not find method illegal()" + ) + ) + + errors[1].assertSingleError( + expectedMessageLines = listOf( + "A problem occurred configuring project ':app'." + ), + expectedCauseMessages = listOf( + "A problem occurred configuring project ':app'.", + "compileSdkVersion is not specified" + ) + ) + } + + @Test + fun `configuration fails - build verdict contains project dependecy error`() { + generateProject( + KotlinModule( + name = appName, + dependencies = setOf( + project(":not-existed") + ) + ) + ) + + val result = gradlew( + temp, + "help", + dryRun = true, + expectFailure = true + ) + result.assertThat().buildFailed() + assertBuildVerdictFileExist(true) + assertThat(plainTextBuildVerdict.readText()).isEqualTo(configurationProjectNotFoundFails(temp)) + + val actualBuildVerdict = gson.fromJson(jsonBuildVerdict.readText(), BuildVerdict.Configuration::class.java) + val error = actualBuildVerdict.error as Single + + error.assertSingleError( + expectedMessageLines = listOf( + "$temp/app/build.gradle' line: 9", + "A problem occurred evaluating project ':app'." + ), + expectedCauseMessages = listOf( + "A problem occurred evaluating project ':app'.", + "Project with path ':not-existed' could not be found" + ) + ) + } + + private fun Single.assertSingleError( + expectedMessageLines: List, + expectedCauseMessages: List + ) { + val messageLines = message.lines() + assertThat(messageLines).hasSize(expectedMessageLines.size) + messageLines.forEachIndexed { index, line -> + assertThat(line).contains(expectedMessageLines[index]) + } + assertThat(causes).hasSize(expectedCauseMessages.size) + causes.forEachIndexed { index, cause -> + assertThat(cause.message).contains(expectedCauseMessages[index]) + } + } +} diff --git a/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BuildVerdictPluginTest.kt b/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BuildVerdictPluginExecutionPhaseTest.kt similarity index 76% rename from subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BuildVerdictPluginTest.kt rename to subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BuildVerdictPluginExecutionPhaseTest.kt index 9084b84922..cdfbc0a878 100644 --- a/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BuildVerdictPluginTest.kt +++ b/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/BuildVerdictPluginExecutionPhaseTest.kt @@ -1,30 +1,20 @@ package com.avito.android.build_verdict import com.avito.android.build_verdict.internal.BuildVerdict -import com.avito.test.gradle.TestProjectGenerator import com.avito.test.gradle.dir import com.avito.test.gradle.gradlew import com.avito.test.gradle.kotlinClass import com.avito.test.gradle.module.AndroidAppModule +import com.avito.test.gradle.module.KotlinModule import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage -import com.google.gson.GsonBuilder import org.junit.jupiter.api.Test -import org.junit.jupiter.api.io.TempDir import java.io.File -import com.avito.android.build_verdict.internal.writer.RawBuildVerdictWriter.Companion.buildVerdictFileName as rawBuildVerdictFileName -class BuildVerdictPluginTest { - - @field:TempDir - lateinit var temp: File - - private val buildVerdict by lazy { File(temp, "outputs/build-verdict/$rawBuildVerdictFileName") } - private val gson = GsonBuilder().create() +class BuildVerdictPluginExecutionPhaseTest : BaseBuildVerdictTest() { @Test fun `kotlin compilation fails - build-verdict contains kotlin compile info`() { - generateProject {} + generateProject() File(temp, "app/src/main/kotlin").kotlinClass("Uncompiled") { "incorrect syntax" } val result = gradlew( @@ -37,6 +27,7 @@ class BuildVerdictPluginTest { assertBuildVerdict( failedTask = "compileDebugKotlin", + plainTextVerdict = compileFails(temp), expectedErrorLines = listOf( "$temp/app/src/main/kotlin/Uncompiled.kt: (1, 1): Expecting a top level declaration", "$temp/app/src/main/kotlin/Uncompiled.kt: (1, 11): Expecting a top level declaration" @@ -46,7 +37,12 @@ class BuildVerdictPluginTest { @Test fun `kapt stubs generating fails - build-verdict contains kapt info`() { - generateProject(enableKapt = true) {} + generateProject( + AndroidAppModule( + name = appName, + enableKapt = true + ) + ) File(temp, "app/src/main/kotlin").kotlinClass("Uncompiled") { "incorrect syntax" } val result = gradlew( @@ -59,6 +55,7 @@ class BuildVerdictPluginTest { assertBuildVerdict( failedTask = "kaptGenerateStubsDebugKotlin", + plainTextVerdict = kaptStubGeneratingFails(temp), expectedErrorLines = listOf( "$temp/app/src/main/kotlin/Uncompiled.kt: (1, 1): Expecting a top level declaration", "$temp/app/src/main/kotlin/Uncompiled.kt: (1, 11): Expecting a top level declaration" @@ -68,10 +65,14 @@ class BuildVerdictPluginTest { @Test fun `kapt fails - build-verdict contains kapt info`() { - generateProject(enableKapt = true) { module -> - dir("src/main/kotlin/") { - kotlinClass("DaggerComponent", module.packageName) { - """ + generateProject( + AndroidAppModule( + name = appName, + enableKapt = true, + mutator = { module -> + dir("src/main/kotlin/") { + kotlinClass("DaggerComponent", module.packageName) { + """ import dagger.Component class CoffeeMaker @@ -80,9 +81,11 @@ class BuildVerdictPluginTest { fun maker(): CoffeeMaker } """.trimIndent() + } + } } - } - } + ) + ) val result = gradlew( temp, @@ -95,6 +98,7 @@ class BuildVerdictPluginTest { @Suppress("MaxLineLength") assertBuildVerdict( failedTask = "kaptDebugKotlin", + plainTextVerdict = kaptFails(temp), expectedErrorLines = listOf( "$temp/app/build/tmp/kapt3/stubs/debug/DaggerComponent.java:6: error: [Dagger/MissingBinding] CoffeeMaker cannot be provided without an @Inject constructor or an @Provides-annotated method.", "public abstract interface DaggerComponent {", @@ -107,10 +111,13 @@ class BuildVerdictPluginTest { @Test fun `unit test fails - build-verdict contains test info`() { - generateProject { module -> - dir("src/test/kotlin/") { - kotlinClass("AppTest", module.packageName) { - """ + generateProject( + AndroidAppModule( + name = appName, + mutator = { module -> + dir("src/test/kotlin/") { + kotlinClass("AppTest", module.packageName) { + """ import org.junit.Test import junit.framework.Assert @@ -127,9 +134,11 @@ class BuildVerdictPluginTest { } } """.trimIndent() + } + } } - } - } + ) + ) val result = gradlew( temp, @@ -141,6 +150,7 @@ class BuildVerdictPluginTest { assertBuildVerdict( failedTask = "testDebugUnitTest", + plainTextVerdict = unitTestsFails(temp), expectedErrorLines = listOf( "FAILED tests:", "\tAppTest.test assert true", @@ -169,7 +179,9 @@ class BuildVerdictPluginTest { fun `buildVerdictTask fails - build-verdict contains task's verdict info`() { //language=Groovy generateProject( - buildGradleExtra = """ + module = KotlinModule( + name = appName, + buildGradleExtra = """ import com.avito.android.build_verdict.BuildVerdictTask class CustomTask extends DefaultTask implements BuildVerdictTask { @@ -190,6 +202,7 @@ class BuildVerdictPluginTest { tasks.register("customTask", CustomTask) """.trimIndent() + ) ) val result = gradlew( @@ -204,38 +217,23 @@ class BuildVerdictPluginTest { assertBuildVerdict( failedTask = "customTask", + plainTextVerdict = customTaskFails, expectedErrorLines = listOf( "Custom verdict" ) ) } - private fun generateProject( - enableKapt: Boolean = false, - buildGradleExtra: String = "", - mutator: File.(AndroidAppModule) -> Unit = {} - ) { - TestProjectGenerator( - plugins = listOf("com.avito.android.build-verdict"), - modules = listOf( - AndroidAppModule( - name = appName, - enableKapt = enableKapt, - buildGradleExtra = buildGradleExtra, - mutator = mutator - ) - ) - ).generateIn(temp) - } - private fun assertBuildVerdict( failedApp: String = appName, failedTask: String, + plainTextVerdict: String, expectedErrorLines: List ) { assertBuildVerdictFileExist(true) + assertThat(plainTextBuildVerdict.readText()).isEqualTo(plainTextVerdict) - val actualBuildVerdict = gson.fromJson(buildVerdict.readText(), BuildVerdict::class.java) + val actualBuildVerdict = gson.fromJson(jsonBuildVerdict.readText(), BuildVerdict.Execution::class.java) assertThat(actualBuildVerdict.failedTasks) .hasSize(1) @@ -258,16 +256,4 @@ class BuildVerdictPluginTest { .contains(expectedErrorLines[index]) } } - - private fun assertBuildVerdictFileExist( - exist: Boolean - ) { - assertWithMessage("outputs/build-verdict/$rawBuildVerdictFileName exist is $exist") - .that(buildVerdict.exists()) - .isEqualTo(exist) - } - - companion object { - const val appName = "app" - } } diff --git a/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/PlainTextVerdicts.kt b/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/PlainTextVerdicts.kt new file mode 100644 index 0000000000..6a545ae4d0 --- /dev/null +++ b/subprojects/gradle/build-verdict/src/test/kotlin/com/avito/android/build_verdict/PlainTextVerdicts.kt @@ -0,0 +1,86 @@ +@file:Suppress("MaxLineLength") +package com.avito.android.build_verdict + +import java.io.File + +fun kaptStubGeneratingFails(dir: File) = """ +* What went wrong: +Execution failed for task ':app:kaptGenerateStubsDebugKotlin'. + > Compilation error. See log for more details + +* Error logs: +e: ${dir.canonicalPath}/app/src/main/kotlin/Uncompiled.kt: (1, 1): Expecting a top level declaration +e: ${dir.canonicalPath}/app/src/main/kotlin/Uncompiled.kt: (1, 11): Expecting a top level declaration +""".trimIndent() + +fun unitTestsFails(dir: File) = """ +* What went wrong: +Execution failed for task ':app:testDebugUnitTest'. + > There were failing tests. See the report at: file://${dir.canonicalPath}/app/build/reports/tests/testDebugUnitTest/index.html + +* Error logs: +FAILED tests: + AppTest.test assert true + AppTest.test runtime exception +""".trimIndent() + +fun kaptFails(dir: File) = """ +* What went wrong: +Execution failed for task ':app:kaptDebugKotlin'. + > A failure occurred while executing org.jetbrains.kotlin.gradle.internal.KaptExecution + > class java.lang.reflect.InvocationTargetException (no error message) + > Error while annotation processing + +* Error logs: +${dir.canonicalPath}/app/build/tmp/kapt3/stubs/debug/DaggerComponent.java:6: error: [Dagger/MissingBinding] CoffeeMaker cannot be provided without an @Inject constructor or an @Provides-annotated method. +public abstract interface DaggerComponent { + ^ + CoffeeMaker is requested at + DaggerComponent.maker() +""".trimIndent() + +val customTaskFails = """ +* What went wrong: +Execution failed for task ':app:customTask'. + > Surprise + +* Error logs: +Custom verdict +""".trimIndent() + +fun compileFails(dir: File) = """ +* What went wrong: +Execution failed for task ':app:compileDebugKotlin'. + > Compilation error. See log for more details + +* Error logs: +e: ${dir.canonicalPath}/app/src/main/kotlin/Uncompiled.kt: (1, 1): Expecting a top level declaration +e: ${dir.canonicalPath}/app/src/main/kotlin/Uncompiled.kt: (1, 11): Expecting a top level declaration +""".trimIndent() + +fun configurationIllegalMethodFails(dir: File) = """ +FAILURE: Build completed with 2 failures. + +1: Task failed with an exception. +----------- +Build file '${dir.canonicalPath}/app/build.gradle' line: 9 +A problem occurred evaluating project ':app'. + > A problem occurred evaluating project ':app'. + > Could not find method illegal() for arguments [build 'test-project'] on project ':app' of type org.gradle.api.Project. + +2: Task failed with an exception. +----------- +A problem occurred configuring project ':app'. + > A problem occurred configuring project ':app'. + > compileSdkVersion is not specified. Please add it to build.gradle +""".trimIndent() + +fun configurationProjectNotFoundFails(dir: File) = """ +FAILURE: Build failed with an exception. + +* What went wrong: +Build file '${dir.canonicalPath}/app/build.gradle' line: 9 +A problem occurred evaluating project ':app'. + > A problem occurred evaluating project ':app'. + > Project with path ':not-existed' could not be found in project ':app'. +""".trimIndent()