diff --git a/README.md b/README.md index cfca6c8..6da3fc1 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ public class LibraryArchRules implements ArchRulesService { } ``` +You will also need to create a file at `src/archRules/resources/META-INF/services/com.netflix.nebula.archrules.core.ArchRulesService` and add the fully qualified name of your rules class to that file. + When authoring rules about the usage of your own library code, it is recommended to colocate your rules library in the same project as the library code. The ArchRules plugin will publish the rules in a separate Jar, and the Runner plugin will select that jar for running rules, but these rule classes will not end up in the runtime classpath. @@ -106,6 +108,32 @@ public class LibraryArchRulesTest { } ``` +## Running Rules + +In order to run rules in a project, add the runner plugin: +```kotlin +plugins { + id("com.netflix.nebula.archrules.runner") version ("latest.release") +} +``` + +This will create a task for running rules against each source set, eg. `checkArchRulesMain` for the Main source set. +These tasks will run as dependencies of the `check` task. + +If you want to run rules on all source sets, add the rule library as a dependency to the `archRules` configuration: +```kotlin +dependencies { + archRules("your:rules:1.0.0") +} +``` + +Rules that exist in a library on each sourceSet's classpath will also be used: +```kotlin +dependencies { + implementation("some.library:which-also-has-rules:1.0.0") +} +``` + ## 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-core/build.gradle.kts b/nebula-archrules-core/build.gradle.kts index d027b20..dcb51f1 100644 --- a/nebula-archrules-core/build.gradle.kts +++ b/nebula-archrules-core/build.gradle.kts @@ -18,7 +18,7 @@ testing { } java { toolchain { - languageVersion = JavaLanguageVersion.of(11) + languageVersion = JavaLanguageVersion.of(8) } } dependencyLocking { diff --git a/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/NoClassesMatchedEvent.java b/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/NoClassesMatchedEvent.java index 4125f1d..c68c365 100644 --- a/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/NoClassesMatchedEvent.java +++ b/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/NoClassesMatchedEvent.java @@ -23,7 +23,7 @@ public ConditionEvent invert() { @Override public List getDescriptionLines() { - return List.of(NO_MATCH_MESSAGE); + return Collections.singletonList(NO_MATCH_MESSAGE); } @Override diff --git a/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/Runner.java b/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/Runner.java index 5507158..ac7ad78 100644 --- a/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/Runner.java +++ b/nebula-archrules-core/src/main/java/com/netflix/nebula/archrules/core/Runner.java @@ -12,7 +12,7 @@ public static EvaluationResult check(ArchRule rule, JavaClasses classesToCheck) return rule.evaluate(classesToCheck); } catch (AssertionError e) { // evaluate the rule again with more leniency so we can get the priority - final var result2 = rule.allowEmptyShould(true).evaluate(classesToCheck); + final EvaluationResult result2 = rule.allowEmptyShould(true).evaluate(classesToCheck); if (result2.hasViolation()) { return result2; } else { @@ -26,12 +26,13 @@ public static EvaluationResult check(ArchRule rule, JavaClasses classesToCheck) /** * Check a rule against some classes. * This can be invoked from the real Gradle plugin or unit tests for rules to ensure the same logic is observed there. - * @param rule the rule to run + * + * @param rule the rule to run * @param classesToCheck the classes to run the rule against * @return the result, which contains information about failure */ public static EvaluationResult check(ArchRule rule, Class... classesToCheck) { - final var classes = new ClassFileImporter() + final JavaClasses classes = new ClassFileImporter() .importClasses(classesToCheck); return check(rule, classes); } diff --git a/nebula-archrules-core/src/test/java/com/netflix/nebula/archrules/core/RunnerTest.java b/nebula-archrules-core/src/test/java/com/netflix/nebula/archrules/core/RunnerTest.java index a2b2c66..964dc24 100644 --- a/nebula-archrules-core/src/test/java/com/netflix/nebula/archrules/core/RunnerTest.java +++ b/nebula-archrules-core/src/test/java/com/netflix/nebula/archrules/core/RunnerTest.java @@ -1,6 +1,7 @@ package com.netflix.nebula.archrules.core; import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.EvaluationResult; import com.tngtech.archunit.lang.Priority; import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; import org.junit.jupiter.api.Test; @@ -11,19 +12,19 @@ public class RunnerTest { @Test public void test_pass() { - final var result = Runner.check(noDeprecatedRule, PassingClass.class); + final EvaluationResult result = Runner.check(noDeprecatedRule, PassingClass.class); assertThat(result.hasViolation()).isFalse(); } @Test public void test_fail() { - final var result = Runner.check(noDeprecatedRule, FailingClass.class); + final EvaluationResult result = Runner.check(noDeprecatedRule, FailingClass.class); assertThat(result.hasViolation()).isTrue(); } @Test public void test_fail_and_pass() { - final var result = Runner.check(noDeprecatedRule, PassingClass.class, FailingClass.class); + final EvaluationResult result = Runner.check(noDeprecatedRule, PassingClass.class, FailingClass.class); assertThat(result.hasViolation()).isTrue(); assertThat(result.getFailureReport().getDetails()).hasSize(1); } @@ -51,13 +52,13 @@ static class SmokeTest { @Test public void test_smoke_pass() { - final var result = Runner.check(smokeTestRule, SmokeTest.class); + final EvaluationResult result = Runner.check(smokeTestRule, SmokeTest.class); assertThat(result.hasViolation()).isFalse(); } @Test public void test_smoke_fail() { - final var result = Runner.check(smokeTestRule, SmokeTestFail.class); + final EvaluationResult result = Runner.check(smokeTestRule, SmokeTestFail.class); assertThat(result.hasViolation()).isTrue(); assertThat(result.getFailureReport().getDetails()) .contains(NoClassesMatchedEvent.NO_MATCH_MESSAGE); diff --git a/nebula-archrules-gradle-plugin/build.gradle.kts b/nebula-archrules-gradle-plugin/build.gradle.kts index 79a8858..6d05a89 100644 --- a/nebula-archrules-gradle-plugin/build.gradle.kts +++ b/nebula-archrules-gradle-plugin/build.gradle.kts @@ -7,6 +7,7 @@ repositories { mavenCentral() } dependencies { + implementation(project(":nebula-archrules-core")) testImplementation("net.javacrumbs.json-unit:json-unit-assertj:5.0.0") testImplementation("org.json:json:20250517") } @@ -30,7 +31,7 @@ gradlePlugin { } java { toolchain { - languageVersion = JavaLanguageVersion.of(11) + languageVersion = JavaLanguageVersion.of(17) } } testing { diff --git a/nebula-archrules-gradle-plugin/gradle.lockfile b/nebula-archrules-gradle-plugin/gradle.lockfile index 8f3fada..75e547c 100644 --- a/nebula-archrules-gradle-plugin/gradle.lockfile +++ b/nebula-archrules-gradle-plugin/gradle.lockfile @@ -4,6 +4,7 @@ cglib:cglib-nodep:3.2.2=integTestRuntimeClasspath,testRuntimeClasspath com.jayway.jsonpath:json-path:2.9.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath com.netflix.nebula:nebula-test:11.7.1=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath +com.tngtech.archunit:archunit:1.4.1=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath net.bytebuddy:byte-buddy:1.17.7=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath net.javacrumbs.json-unit:json-unit-assertj:5.0.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath net.javacrumbs.json-unit:json-unit-core:5.0.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath @@ -44,5 +45,5 @@ org.junit.platform:junit-platform-launcher:1.14.0=integTestCompileClasspath,inte org.objenesis:objenesis:2.4=integTestRuntimeClasspath,testRuntimeClasspath org.opentest4j:opentest4j:1.3.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath org.ow2.asm:asm:9.3=integTestRuntimeClasspath,testRuntimeClasspath -org.slf4j:slf4j-api:2.0.11=integTestRuntimeClasspath,testRuntimeClasspath -empty=annotationProcessor,integTestAnnotationProcessor,integTestKotlinScriptDefExtensions,kotlinScriptDefExtensions,runtimeClasspath,testAnnotationProcessor,testKotlinScriptDefExtensions +org.slf4j:slf4j-api:2.0.17=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +empty=annotationProcessor,integTestAnnotationProcessor,integTestKotlinScriptDefExtensions,kotlinScriptDefExtensions,testAnnotationProcessor,testKotlinScriptDefExtensions diff --git a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/ArchRuleAttribute.java b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/ArchRuleAttribute.java new file mode 100644 index 0000000..9921643 --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/ArchRuleAttribute.java @@ -0,0 +1,11 @@ +package com.netflix.nebula.archrules.gradle; + +import org.gradle.api.Named; +import org.gradle.api.attributes.Attribute; +import org.jspecify.annotations.NonNull; + +public interface ArchRuleAttribute extends Named { + Attribute<@NonNull ArchRuleAttribute> ARCH_RULES_ATTRIBUTE = + Attribute.of("com.netflix.nebula.archrules", ArchRuleAttribute.class); + String ARCH_RULES = "arch-rules"; +} diff --git a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/Rule.java b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/Rule.java new file mode 100644 index 0000000..b285d0e --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/Rule.java @@ -0,0 +1,8 @@ +package com.netflix.nebula.archrules.gradle; + +import com.tngtech.archunit.lang.Priority; + +import java.io.Serializable; + +public record Rule(String ruleClass, String ruleName, String description, Priority priority) implements Serializable { +} diff --git a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RuleResult.java b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RuleResult.java new file mode 100644 index 0000000..1f67a27 --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RuleResult.java @@ -0,0 +1,10 @@ +package com.netflix.nebula.archrules.gradle; + +import java.io.Serializable; + +public record RuleResult( + Rule rule, + String message, + RuleResultStatus status +) implements Serializable { +} diff --git a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RuleResultStatus.java b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RuleResultStatus.java new file mode 100644 index 0000000..97d6c17 --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RuleResultStatus.java @@ -0,0 +1,5 @@ +package com.netflix.nebula.archrules.gradle; + +public enum RuleResultStatus { + PASS, FAIL, NO_MATCH +} diff --git a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RuleSummary.java b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RuleSummary.java new file mode 100644 index 0000000..d5307e1 --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RuleSummary.java @@ -0,0 +1,13 @@ +package com.netflix.nebula.archrules.gradle; + +import com.tngtech.archunit.lang.Priority; + +import java.io.Serializable; + +public record RuleSummary( + String ruleClass, + String ruleName, + Priority priority, + int failures +) implements Serializable { +} diff --git a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesParams.java b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesParams.java new file mode 100644 index 0000000..2d3a697 --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesParams.java @@ -0,0 +1,14 @@ +package com.netflix.nebula.archrules.gradle; + +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.provider.Property; +import org.gradle.workers.WorkParameters; +import org.jspecify.annotations.NonNull; + +import java.io.File; + +public interface RunRulesParams extends WorkParameters { + ConfigurableFileCollection getClassesToCheck(); + + Property<@NonNull File> getDataOutputFile(); +} diff --git a/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesWorkAction.java b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesWorkAction.java new file mode 100644 index 0000000..aa5918d --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/java/com/netflix/nebula/archrules/gradle/RunRulesWorkAction.java @@ -0,0 +1,56 @@ +package com.netflix.nebula.archrules.gradle; + +import com.netflix.nebula.archrules.core.ArchRulesService; +import com.netflix.nebula.archrules.core.Runner; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import org.gradle.workers.WorkAction; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; + +import static com.netflix.nebula.archrules.core.NoClassesMatchedEvent.NO_MATCH_MESSAGE; + +public abstract class RunRulesWorkAction implements WorkAction { + + @Override + public void execute() { + ServiceLoader ruleClasses = ServiceLoader.load(ArchRulesService.class); + final var classesToCheck = new ClassFileImporter() + .importPaths(getParameters().getClassesToCheck().getFiles().stream().map(File::toPath).toList()); + final List violationList = new ArrayList<>(); + ruleClasses.forEach(ruleClass -> ruleClass.getRules().forEach((id, archRule) -> { + final var result = Runner.check(archRule, classesToCheck); + //TODO: allow priority overrides by rule name + final var rule = new Rule(ruleClass.getClass().getCanonicalName(), id, archRule.getDescription(), result.getPriority()); + if (result.hasViolation()) { + result.getFailureReport().getDetails().forEach(detail -> { + if (detail.equals(NO_MATCH_MESSAGE)) { + violationList.add(new RuleResult(rule, detail, RuleResultStatus.NO_MATCH)); + } else { + violationList.add(new RuleResult(rule, detail, RuleResultStatus.FAIL)); + } + }); + } else { + violationList.add(new RuleResult(rule, "", RuleResultStatus.PASS)); + } + })); + + try (var out = new ObjectOutputStream(new FileOutputStream(getParameters().getDataOutputFile().get()))) { + out.writeInt(violationList.size()); + violationList.forEach((v) -> { + try { + out.writeObject(v); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt index 59ff671..2469585 100644 --- a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt +++ b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesLibraryPlugin.kt @@ -1,39 +1,41 @@ package com.netflix.nebula.archrules.gradle +import com.netflix.nebula.archrules.gradle.ArchRuleAttribute.ARCH_RULES import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.attributes.Usage import org.gradle.api.plugins.JavaPluginExtension import org.gradle.api.plugins.jvm.JvmTestSuite -import org.gradle.jvm.tasks.Jar import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.invoke import org.gradle.kotlin.dsl.named -import org.gradle.kotlin.dsl.register import org.gradle.testing.base.TestingExtension class ArchrulesLibraryPlugin : Plugin { override fun apply(project: Project) { val version = ArchrulesLibraryPlugin::class.java.`package`.implementationVersion ?: "latest.release" - project.plugins.withId("java") { + project.pluginManager.withPlugin("java") { val ext = project.extensions.getByType() val archRulesSourceSet = ext.sourceSets.create("archRules") project.dependencies.add( archRulesSourceSet.implementationConfigurationName, "com.netflix.nebula:nebula-archrules-core:$version" ) - val jarTask = project.tasks.register("archRulesJar") { - archiveClassifier.set("archrules") - from(archRulesSourceSet.output) + ext.registerFeature("archRules") { + usingSourceSet(archRulesSourceSet) + capability(project.group.toString(), project.name, project.version.toString()) } - val runtimeElements = project.configurations.getByName("runtimeElements") - runtimeElements.outgoing.variants.create("archRulesElements") { + project.configurations.named("archRulesRuntimeElements") { attributes { - attribute(Usage.USAGE_ATTRIBUTE, project.objects.named("arch-rules")) + attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named(ARCH_RULES)) } - artifact(jarTask) } - project.plugins.withId("jvm-test-suite") { + project.configurations.named("archRulesApiElements") { + attributes { + attribute(ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, project.objects.named(ARCH_RULES)) + } + } + + project.pluginManager.withPlugin("jvm-test-suite") { val ext = project.extensions.getByType() ext.suites { register("archRulesTest", JvmTestSuite::class.java) { @@ -51,4 +53,4 @@ class ArchrulesLibraryPlugin : Plugin { } } } -} \ 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 158b79f..3193e79 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 @@ -1,9 +1,77 @@ package com.netflix.nebula.archrules.gradle +import com.netflix.nebula.archrules.gradle.ArchRuleAttribute.ARCH_RULES import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.attributes.Bundling +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.getByType +import org.gradle.kotlin.dsl.named +import org.gradle.kotlin.dsl.register class ArchrulesRunnerPlugin : Plugin { - override fun apply(target: Project) { + override fun apply(project: Project) { + val archRulesReportDir = project.layout.buildDirectory.dir("reports/archrules") + project.plugins.withId("java") { + project.configurations.register("archRules") { + attributes { + attribute( + ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, + project.objects.named(ARCH_RULES) + ) + attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage.JAVA_RUNTIME)) + attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category.LIBRARY)) + attribute(Bundling.BUNDLING_ATTRIBUTE, project.objects.named(Bundling.EXTERNAL)) + } + } + val checkTasks = project.extensions.getByType().sourceSets + .map { sourceSet -> + val checkTask = + project.tasks.register("checkArchRules" + sourceSet.name.capitalized()) { + description = "Checks ArchRules on ${sourceSet.name}" + val artifactView = + project.configurations.getByName(sourceSet.runtimeClasspathConfigurationName) + .incoming + .artifactView { + withVariantReselection() + attributes { + attribute( + ArchRuleAttribute.ARCH_RULES_ATTRIBUTE, + project.objects.named(ARCH_RULES) + ) + attribute( + Usage.USAGE_ATTRIBUTE, + project.objects.named(Usage.JAVA_RUNTIME) + ) + attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category.LIBRARY) + ) + attribute( + Bundling.BUNDLING_ATTRIBUTE, + project.objects.named(Bundling.EXTERNAL) + ) + } + lenient(false) + } + rulesClasspath.setFrom( + artifactView.files, + project.configurations.getByName("archRules") + ) + dataFile.set(archRulesReportDir.map { + it.file(sourceSet.name + ".data").asFile + }) + sourcesToCheck.from(sourceSet.output.classesDirs) + dependsOn(project.tasks.named(sourceSet.classesTaskName)) + } + checkTask + } + project.tasks.named("check") { + dependsOn(checkTasks) + } + } } } \ No newline at end of file diff --git a/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/CheckRulesTask.kt b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/CheckRulesTask.kt new file mode 100644 index 0000000..1d105e7 --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/main/kotlin/com/netflix/nebula/archrules/gradle/CheckRulesTask.kt @@ -0,0 +1,37 @@ +package com.netflix.nebula.archrules.gradle; + +import org.gradle.api.DefaultTask +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.provider.Property +import org.gradle.api.tasks.* +import org.gradle.kotlin.dsl.submit +import org.gradle.workers.WorkQueue +import org.gradle.workers.WorkerExecutor +import java.io.File +import javax.inject.Inject + +@CacheableTask +abstract class CheckRulesTask @Inject constructor(private val workerExecutor: WorkerExecutor) : DefaultTask() { + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val rulesClasspath: ConfigurableFileCollection + + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + abstract val sourcesToCheck: ConfigurableFileCollection + + @get:OutputFile + abstract val dataFile: Property + + @TaskAction + fun checkRules() { + val workQueue: WorkQueue = workerExecutor.classLoaderIsolation { + classpath.from(rulesClasspath) + } + workQueue.submit(RunRulesWorkAction::class) { + getClassesToCheck().from(sourcesToCheck) + getDataOutputFile().set(dataFile) + } + } +} 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 5b1f92a..8cba394 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 @@ -75,7 +75,7 @@ class ArchrulesLibraryPluginTest { assertThat(projectDir.resolve("build/libs/library-with-rules-0.0.1.jar")) .`as`("Library Jar is created") .exists() - assertThat(projectDir.resolve("build/libs/library-with-rules-0.0.1-archrules.jar")) + assertThat(projectDir.resolve("build/libs/library-with-rules-0.0.1-arch-rules.jar")) .`as`("ArchRules Jar is created") .exists() @@ -85,7 +85,7 @@ class ArchrulesLibraryPluginTest { .exists() val moduleMetadataJson = moduleMetadata.readText() - +println(moduleMetadataJson) assertThatJson(moduleMetadataJson) .inPath("$.variants[?(@.name=='runtimeElements')].files[0]") .isArray @@ -93,10 +93,10 @@ class ArchrulesLibraryPluginTest { .containsEntry("name", "library-with-rules-0.0.1.jar") assertThatJson(moduleMetadataJson) - .inPath("$.variants[?(@.name=='runtimeElementsArchRulesElements')].files[0]") + .inPath("$.variants[?(@.name=='archRulesRuntimeElements')].files[0]") .isArray .first().isObject - .containsEntry("name", "library-with-rules-0.0.1-archrules.jar") + .containsEntry("name", "library-with-rules-0.0.1-arch-rules.jar") } @Test 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 new file mode 100644 index 0000000..50c5334 --- /dev/null +++ b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/ArchrulesRunnerPluginTest.kt @@ -0,0 +1,172 @@ +package com.netflix.nebula.archrules.gradle + +import nebula.test.dsl.TestKitAssertions.assertThat +import nebula.test.dsl.main +import nebula.test.dsl.plugins +import nebula.test.dsl.repositories +import nebula.test.dsl.rootProject +import nebula.test.dsl.settings +import nebula.test.dsl.sourceSet +import nebula.test.dsl.src +import nebula.test.dsl.test +import nebula.test.dsl.testProject +import net.javacrumbs.jsonunit.assertj.assertThatJson +import org.gradle.testfixtures.ProjectBuilder +import org.gradle.testkit.runner.TaskOutcome +import org.junit.jupiter.api.Disabled +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.ObjectInputStream + +class ArchrulesRunnerPluginTest { + @TempDir + lateinit var projectDir: File + + @Test + fun `plugin registers archRules configuration`() { + val project = ProjectBuilder.builder().build() + project.plugins.apply("java") + project.plugins.apply(ArchrulesRunnerPlugin::class.java) + val configuration = project.configurations.findByName("archRules") + assertThat(configuration).isNotNull + } + + @Test + @Disabled("needs a rules library published with new library plugin") + fun `plugin checks each sourceset`() { + val runner = testProject(projectDir) { + settings { + name("consumer") + } + rootProject { + plugins { + id("java") + id("com.netflix.nebula.archrules.runner") + } + repositories { + mavenCentral() + } + dependencies( + """archRules("com.netflix.nebula:archrules-deprecation:0.0.4")""" + ) + src { + main { + exampleLibraryClass() + exampleDeprecatedUsage() + } + test { + exampleDeprecatedUsage("FailingCodeTest") + } + } + } + } + + val result = runner.run("check", "--stacktrace", "-x", "test") + + assertThat(result.task(":checkArchRulesMain")) + .`as`("archRules run for main source set") + .hasOutcome(TaskOutcome.SUCCESS) + + assertThat(result.task(":checkArchRulesTest")) + .`as`("archRules run for test source set") + .hasOutcome(TaskOutcome.SUCCESS) + + assertThat(result) + .hasNoMutableStateWarnings() + .hasNoDeprecationWarnings() + + val mainReport = projectDir.resolve("build/reports/archrules/main.data") + assertThat(mainReport) + .`as`("Main data created") + .exists() + val mainErrors = readDetails(mainReport) + assertThat(mainErrors).hasSize(1) + + val testReport = projectDir.resolve("build/reports/archrules/test.data") + assertThat(testReport) + .`as`("Test data created") + .exists() + val testErrors = readDetails(testReport) + assertThat(testErrors).hasSize(1) + } + + @Test + @Disabled("needs a rules library published with new library plugin") + fun `plugin checks each sourceset from its runtime`() { + val runner = testProject(projectDir) { + settings { + name("consumer") + } + rootProject { + plugins { + id("java") + id("com.netflix.nebula.archrules.runner") + } + repositories { + mavenCentral() + } + dependencies( + """testImplementation("com.netflix.nebula:archrules-deprecation:0.0.4")""" + ) + src { + main { + exampleLibraryClass() + exampleDeprecatedUsage() + } + test { + exampleDeprecatedUsage("FailingCodeTest") + } + } + } + } + + val result = runner.run("check", "--stacktrace", "-x", "test") + + assertThat(result.task(":checkArchRulesMain")) + .`as`("archRules run for main source set") + .hasOutcome(TaskOutcome.SUCCESS) + + assertThat(result.task(":checkArchRulesTest")) + .`as`("archRules run for test source set") + .hasOutcome(TaskOutcome.SUCCESS) + + assertThat(result) + .hasNoMutableStateWarnings() + .hasNoDeprecationWarnings() + + val mainReport = projectDir.resolve("build/reports/archrules/main.data") + assertThat(mainReport) + .`as`("rule not in main classpath, so not checked") + .exists() + val mainErrors = readDetails(mainReport) + assertThat(mainErrors).isEmpty() + + val testReport = projectDir.resolve("build/reports/archrules/test.data") + assertThat(testReport) + .`as`("Test data created") + .exists() + val testErrors = readDetails(testReport) + assertThat(testErrors).hasSize(1) + } + + fun readDetails(dataFile: File): List { + val list: MutableList = mutableListOf() + try { + ObjectInputStream(FileInputStream(dataFile)).use { objectInputStream -> + val numObjects = objectInputStream.readInt() + repeat(numObjects) { + list.add(objectInputStream.readObject() as RuleResult) + } + } + } catch (e: IOException) { + throw RuntimeException(e) + } catch (e: ClassNotFoundException) { + throw RuntimeException(e) + } + return list + } + +} \ No newline at end of file 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 150836e..a4a0f93 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 @@ -44,9 +44,19 @@ internal class IntegrationTest { dependencies( """implementation(project(":library-with-rules"))""" ) + src { + main { + exampleDeprecatedUsage() + } + } } } + val serviceFile = projectDir + .resolve("library-with-rules/src/archRules/resources/META-INF/services/com.netflix.nebula.archrules.core.ArchRulesService") + serviceFile.parentFile.mkdirs() + serviceFile.writeText("com.example.library.LibraryArchRules") + val result = runner.run("check") { withGradleVersion(gradleVersion.version) forwardOutput() @@ -54,10 +64,24 @@ internal class IntegrationTest { assertThat(result.task(":library-with-rules:check")) .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE) + + assertThat(result.task(":code-to-check:checkArchRulesMain")) + .`as`("archRules run for main source set") + .hasOutcome(TaskOutcome.SUCCESS) + + assertThat(result.task(":code-to-check:checkArchRulesTest")) + .`as`("archRules run for test source set") + .hasOutcome(TaskOutcome.SUCCESS) + assertThat(result.task(":code-to-check:check")) .hasOutcome(TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE) assertThat(result) .hasNoMutableStateWarnings() .hasNoDeprecationWarnings() + + val reportsDir = projectDir.resolve("code-to-check/build/reports/archrules") + + assertThat(reportsDir.exists()) + assertThat(reportsDir.resolve("main.data")).exists().isNotEmpty } } \ No newline at end of file diff --git a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/TestKitDslExtensions.kt b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/TestKitDslExtensions.kt index a2181a3..cb6f0dd 100644 --- a/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/TestKitDslExtensions.kt +++ b/nebula-archrules-gradle-plugin/src/test/kotlin/com/netflix/nebula/archrules/gradle/TestKitDslExtensions.kt @@ -72,6 +72,24 @@ public class LibraryArchRules implements ArchRulesService { ) } +fun SourceSetBuilder.exampleDeprecatedUsage(className: String = "FailingCode") { + java( + "com/example/consumer/$className.java", + //language=java + """ +package com.example.consumer; + +import com.example.library.LibraryClass; + +class $className { + public void aMethod() { + LibraryClass.deprecatedApi(); + } +} +""" + ) +} + fun SourceSetBuilder.exampleTestForArchRule() { java( "com/example/library/LibraryArchRulesTest.java",