Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -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<RuleResult> 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());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.netflix.nebula.archrules.gradle

import org.gradle.api.provider.Property

abstract class ArchrulesExtension {
abstract val consoleReportEnabled: Property<Boolean>
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -27,6 +28,8 @@ class ArchrulesRunnerPlugin : Plugin<Project> {
attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling.EXTERNAL))
}
}
val archRulesExt = project.extensions.create<ArchrulesExtension>("archRules")
archRulesExt.consoleReportEnabled.convention(true)
val checkTasks = project.extensions.getByType<JavaPluginExtension>().sourceSets
.map { sourceSet ->
val checkTask =
Expand Down Expand Up @@ -80,9 +83,15 @@ class ArchrulesRunnerPlugin : Plugin<Project> {
dependsOn(checkTasks)
}

val consoleReportTask = project.tasks.register<PrintConsoleReportTask>("archRulesConsoleReport") {
getDataFiles().set(dataFiles)
dependsOn(checkTasks)
onlyIf { archRulesExt.consoleReportEnabled.get() }
}

project.tasks.named("check") {
dependsOn(checkTasks)
finalizedBy(jsonReportTask)
finalizedBy(jsonReportTask, consoleReportTask)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -24,5 +29,67 @@ class ViolationsUtil {
}
return list
}

@JvmStatic
fun printReport(violations: Map<Rule, List<RuleResult>>, 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<Rule, List<RuleResult>>, 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<RuleResult>): Map<Rule, List<RuleResult>> {
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 } }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand All @@ -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<RuleResult> {
val list: MutableList<RuleResult> = mutableListOf()
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down