diff --git a/README.md b/README.md index 6da3fc1..c718a44 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,13 @@ dependencies { } ``` +The plugin can generate JSON and console reports. Both are enabled by default. The console report can be disabled: +```kotlin +archRules { + consoleReportEnabled = false +} +``` + ## How it works The Archrules Library plugin produces a separate Jar for the `archRules` sourceset, which is exposed as an alternate variant of the library. diff --git a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/PrintConsoleReportTask.java b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/PrintConsoleReportTask.java new file mode 100644 index 0000000..a0a5d94 --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/PrintConsoleReportTask.java @@ -0,0 +1,47 @@ +package com.netflix.nebula.archrules.gradle; + +import com.tngtech.archunit.lang.Priority; +import org.gradle.api.DefaultTask; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.tasks.*; +import org.gradle.internal.logging.text.StyledTextOutput; +import org.gradle.internal.logging.text.StyledTextOutputFactory; +import org.jspecify.annotations.NonNull; + +import java.io.File; +import java.util.List; + +/** + * Prints summary and detail information about {@link RuleResult}s to the console + */ +@UntrackedTask(because = "Provides console feedback to the user") +abstract public class PrintConsoleReportTask extends DefaultTask { + + /** + * The data files to read in. These files should container binary data representing {@link RuleResult}s + * @return all data files to process + */ + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + abstract public ListProperty<@NonNull File> getDataFiles(); + + @TaskAction + public void printReport() { + final var consoleOutput = getServices().get(StyledTextOutputFactory.class).create("archrules"); + consoleOutput.style(StyledTextOutput.Style.Header).println("ArchRule summary:").println(); + List list = getDataFiles().get().stream() + .flatMap(it -> ViolationsUtil.readDetails(it).stream()) + .toList(); + final var byRule = ViolationsUtil.consolidatedFailures(list); + ViolationsUtil.printSummary(byRule, consoleOutput); + consoleOutput.println(); + if (list.stream().anyMatch(it -> it.status() != RuleResultStatus.FAIL && it.rule().priority() == Priority.LOW) && !getLogger().isInfoEnabled()) { + consoleOutput.style(StyledTextOutput.Style.Header) + .text("Note: ") + .style(StyledTextOutput.Style.Normal) + .println("In order to see details of LOW priority rules, run build with --info") + .println(); + } + ViolationsUtil.printReport(byRule, consoleOutput, getLogger().isInfoEnabled()); + } +} diff --git a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesExtension.kt b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesExtension.kt new file mode 100644 index 0000000..4482445 --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesExtension.kt @@ -0,0 +1,7 @@ +package com.netflix.nebula.archrules.gradle + +import org.gradle.api.provider.Property + +abstract class ArchrulesExtension { + abstract val consoleReportEnabled: Property +} \ No newline at end of file diff --git a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPlugin.kt b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPlugin.kt index 0ddef2a..9dcbd52 100644 --- a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPlugin.kt +++ b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPlugin.kt @@ -8,6 +8,7 @@ import org.gradle.api.attributes.Category import org.gradle.api.attributes.Usage import org.gradle.api.plugins.JavaPluginExtension import org.gradle.internal.extensions.stdlib.capitalized +import org.gradle.kotlin.dsl.create import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.named import org.gradle.kotlin.dsl.register @@ -27,6 +28,8 @@ class ArchrulesRunnerPlugin : Plugin { attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling.EXTERNAL)) } } + val archRulesExt = project.extensions.create("archRules") + archRulesExt.consoleReportEnabled.convention(true) val checkTasks = project.extensions.getByType().sourceSets .map { sourceSet -> val checkTask = @@ -80,9 +83,15 @@ class ArchrulesRunnerPlugin : Plugin { dependsOn(checkTasks) } + val consoleReportTask = project.tasks.register("archRulesConsoleReport") { + getDataFiles().set(dataFiles) + dependsOn(checkTasks) + onlyIf { archRulesExt.consoleReportEnabled.get() } + } + project.tasks.named("check") { dependsOn(checkTasks) - finalizedBy(jsonReportTask) + finalizedBy(jsonReportTask, consoleReportTask) } } } diff --git a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ViolationsUtil.kt b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ViolationsUtil.kt index 37e002f..7b077ec 100644 --- a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ViolationsUtil.kt +++ b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ViolationsUtil.kt @@ -1,10 +1,15 @@ package com.netflix.nebula.archrules.gradle +import com.tngtech.archunit.lang.Priority +import org.gradle.internal.logging.text.StyledTextOutput import java.io.File import java.io.FileInputStream import java.io.IOException import java.io.ObjectInputStream +/** + * Helpers for dealing with [RuleResult] + */ class ViolationsUtil { companion object { @JvmStatic @@ -24,5 +29,67 @@ class ViolationsUtil { } return list } + + @JvmStatic + fun printReport(violations: Map>, output: StyledTextOutput, infoLogging: Boolean) { + violations + .mapValues { it.value.filter { it.rule().priority() != Priority.LOW || infoLogging } } + .filter { it.value.isNotEmpty() } + .forEach { (rule, ruleViolations) -> + val style = when (rule.priority()) { + Priority.LOW -> StyledTextOutput.Style.Normal + Priority.MEDIUM -> StyledTextOutput.Style.Info + Priority.HIGH -> StyledTextOutput.Style.Failure + } + output + .style(StyledTextOutput.Style.Header).text("Rule: ${rule.ruleName} Priority: ") + .style(style) + .println(rule.priority().asString()) + .style(style) + .println(rule.description()) + ruleViolations.forEach { + output.style(style).println(" " + it.message()) + } + output.println() + } + } + + @JvmStatic + fun printSummary(results: Map>, output: StyledTextOutput) { + results.forEach { (rule, results) -> + val failures = results.filter { it.status() != RuleResultStatus.PASS } + if (failures.isEmpty()) { + output.style(StyledTextOutput.Style.Success) + .text(rule.ruleName().padEnd(30)) + .text(" ") + .text(rule.priority().asString().padEnd(10)) + .println(" (No failures)") + } else { + val style = when (rule.priority()) { + Priority.LOW -> StyledTextOutput.Style.Normal + Priority.MEDIUM -> StyledTextOutput.Style.Info + Priority.HIGH -> StyledTextOutput.Style.Failure + } + output.style(style) + .text(rule.ruleName().padEnd(30)) + .text(" ") + .text(rule.priority().asString().padEnd(10)) + .println(" (" + failures.size + " failures)") + } + } + } + + /** + * Rules which fail due to no match should only count as a failure if they fail for every source set in which that rule was run + */ + @JvmStatic + fun consolidatedFailures(violations: List): Map> { + val byType = violations.groupBy { it.rule() }.mapValues { it.value.toSet() } + return byType + .mapValues { (_, fullSet) -> + fullSet.filter { !(it.status() == RuleResultStatus.NO_MATCH && fullSet.size != 1) } + } + .mapValues { it.value.filter { it.status() != RuleResultStatus.PASS } } + } } } \ No newline at end of file diff --git a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPluginTest.kt b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPluginTest.kt index ef35fc2..4c52ba0 100644 --- a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPluginTest.kt +++ b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPluginTest.kt @@ -139,7 +139,7 @@ println(moduleMetadataJson) assertThat(result.task(":archRulesTest")) .`as`("archRules test task runs") - .hasOutcome(TaskOutcome.SUCCESS) + .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE) assertThat(result) .hasNoMutableStateWarnings() .hasNoDeprecationWarnings() diff --git a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPluginTest.kt b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPluginTest.kt index 4b3d894..607f7e7 100644 --- a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPluginTest.kt +++ b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPluginTest.kt @@ -78,14 +78,18 @@ class ArchrulesRunnerPluginTest { assertThat(result.task(":checkArchRulesMain")) .`as`("archRules run for main source set") - .hasOutcome(TaskOutcome.SUCCESS) + .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE) assertThat(result.task(":checkArchRulesTest")) .`as`("archRules run for test source set") - .hasOutcome(TaskOutcome.SUCCESS) + .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE) assertThat(result.task(":archRulesJsonReport")) .`as`("archRules json report runs by default") + .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE) + + assertThat(result.task(":archRulesConsoleReport")) + .`as`("archRules console report runs by default") .hasOutcome(TaskOutcome.SUCCESS) assertThat(result) @@ -110,6 +114,10 @@ class ArchrulesRunnerPluginTest { assertThat(jsonReport) .`as`("json report created") .exists() + + assertThat(result.output) + .contains("ArchRule summary:") + .contains("deprecated LOW (1 failures)") } @Test @@ -148,11 +156,11 @@ class ArchrulesRunnerPluginTest { assertThat(result.task(":checkArchRulesMain")) .`as`("archRules run for main source set") - .hasOutcome(TaskOutcome.SUCCESS) + .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE) assertThat(result.task(":checkArchRulesTest")) .`as`("archRules run for test source set") - .hasOutcome(TaskOutcome.SUCCESS) + .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE) assertThat(result) .hasNoMutableStateWarnings() @@ -173,6 +181,63 @@ class ArchrulesRunnerPluginTest { assertThat(testErrors).hasSize(1) } + @ParameterizedTest + @EnumSource(SupportedGradleVersion::class) + fun `console report can be disabled`(gradleVersion: SupportedGradleVersion) { + val runner = testProject(projectDir) { + properties { + gradleCache(true) + } + settings { + name("consumer") + } + rootProject { + plugins { + id("java") + id("com.netflix.nebula.archrules.runner") + } + repositories { + mavenCentral() + } + dependencies( + """archRules("com.netflix.nebula:archrules-deprecation:0.1.+")""" + ) + rawBuildScript(""" +archRules { + consoleReportEnabled = false +} +""" + ) + src { + main { + exampleLibraryClass() + exampleDeprecatedUsage() + } + test { + exampleDeprecatedUsage("FailingCodeTest") + } + } + } + } + + val result = runner.run("check", "--stacktrace", "-x", "test"){ + withGradleVersion(gradleVersion.version) + forwardOutput() + } + + assertThat(result.task(":archRulesConsoleReport")) + .`as`("archRules console report runs by default") + .hasOutcome(TaskOutcome.SKIPPED) + + assertThat(result) + .hasNoMutableStateWarnings() + .hasNoDeprecationWarnings() + + assertThat(result.output) + .doesNotContain("ArchRule summary:") + .doesNotContain("deprecated LOW (1 failures)") + } + fun readDetails(dataFile: File): List { val list: MutableList = mutableListOf() try { diff --git a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/IntegrationTest.kt b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/IntegrationTest.kt index 3bb7936..2524c47 100644 --- a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/IntegrationTest.kt +++ b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/IntegrationTest.kt @@ -70,11 +70,11 @@ internal class IntegrationTest { assertThat(result.task(":code-to-check:checkArchRulesMain")) .`as`("archRules run for main source set") - .hasOutcome(TaskOutcome.SUCCESS) + .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE) assertThat(result.task(":code-to-check:checkArchRulesTest")) .`as`("archRules run for test source set") - .hasOutcome(TaskOutcome.SUCCESS) + .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.FROM_CACHE) assertThat(result.task(":code-to-check:check")) .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE)