Skip to content

Commit

Permalink
Implement composite ArchRules.
Browse files Browse the repository at this point in the history
Multiple ArchRules can be checked at once via CompositeArchRule.of(rule1).and(rule2). Also added sample tests for both plain, junit4 and junit5

Issue: TNG#78
Signed-off-by: Moritz Bogs <moritz.bogs@tngtech.com>
  • Loading branch information
Moritz Bogs authored and codecholeric committed Dec 16, 2018
1 parent 13ae498 commit f7eaf7f
Show file tree
Hide file tree
Showing 10 changed files with 232 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.junit.ArchUnitRunner;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.CompositeArchRule;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;

Expand All @@ -20,16 +21,22 @@
public class CodingRulesTest {

@ArchTest
private final ArchRule NO_ACCESS_TO_STANDARD_STREAMS = NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;
private final ArchRule no_access_to_standard_streams = NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;

@ArchTest
private void no_access_to_standard_streams_as_method(JavaClasses classes) {
noClasses().should(ACCESS_STANDARD_STREAMS).check(classes);
}

@ArchTest
private final ArchRule NO_GENERIC_EXCEPTIONS = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;
private final ArchRule no_generic_exceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;

@ArchTest
private final ArchRule NO_JAVA_UTIL_LOGGING = NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING;
private final ArchRule no_java_util_logging = NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING;

@ArchTest
public static final ArchRule no_classes_should_access_standard_streams_or_throw_generic_exceptions =
CompositeArchRule.of(NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS)
.and(NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS);

}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.tngtech.archunit.junit.ArchTag;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.CompositeArchRule;

import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
import static com.tngtech.archunit.library.GeneralCodingRules.ACCESS_STANDARD_STREAMS;
Expand All @@ -17,16 +18,22 @@
public class CodingRulesTest {

@ArchTest
private final ArchRule NO_ACCESS_TO_STANDARD_STREAMS = NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;
private final ArchRule no_access_to_standard_streams = NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS;

@ArchTest
private void no_access_to_standard_streams_as_method(JavaClasses classes) {
noClasses().should(ACCESS_STANDARD_STREAMS).check(classes);
}

@ArchTest
private final ArchRule NO_GENERIC_EXCEPTIONS = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;
private final ArchRule no_generic_exceptions = NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS;

@ArchTest
private final ArchRule NO_JAVA_UTIL_LOGGING = NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING;
private final ArchRule no_java_util_logging = NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING;

@ArchTest
static final ArchRule no_classes_should_access_standard_streams_or_throw_generic_exceptions =
CompositeArchRule.of(NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS)
.and(NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS);

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.example.ClassViolatingCodingRules;
import com.tngtech.archunit.lang.CompositeArchRule;
import org.junit.Test;
import org.junit.experimental.categories.Category;

Expand Down Expand Up @@ -36,4 +37,11 @@ public void classes_should_not_throw_generic_exceptions() {
public void classes_should_not_use_java_util_logging() {
NO_CLASSES_SHOULD_USE_JAVA_UTIL_LOGGING.check(classes);
}

@Test
public void no_classes_should_access_standard_streams_or_throw_generic_exceptions() {
CompositeArchRule.of(NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS)
.and(NO_CLASSES_SHOULD_THROW_GENERIC_EXCEPTIONS).check(classes);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -160,13 +160,33 @@ static void disableExtension() {

@TestFactory
Stream<DynamicTest> CodingRulesTest() {
return ExpectedTestFailures
ExpectedTestFailures expectFailures = ExpectedTestFailures
.forTests(
com.tngtech.archunit.exampletest.CodingRulesTest.class,
com.tngtech.archunit.exampletest.junit4.CodingRulesTest.class,
com.tngtech.archunit.exampletest.junit5.CodingRulesTest.class)
com.tngtech.archunit.exampletest.junit5.CodingRulesTest.class);

expectFailures.ofRule("no classes should access standard streams");
expectAccessToStandardStreams(expectFailures);
expectFailures.times(2);

expectFailures.ofRule("no classes should throw generic exceptions");
expectThrownGenericExceptions(expectFailures);

expectFailures.ofRule("no classes should use java.util.logging")
.by(callFromStaticInitializer(ClassViolatingCodingRules.class)
.setting().field(ClassViolatingCodingRules.class, "log")
.inLine(9));

expectFailures.ofRule("no classes should access standard streams and no classes should throw generic exceptions");
expectAccessToStandardStreams(expectFailures);
expectThrownGenericExceptions(expectFailures);

return expectFailures.toDynamicTests();
}

.ofRule("no classes should access standard streams")
private static void expectAccessToStandardStreams(ExpectedTestFailures expectFailures) {
expectFailures
.by(callFromMethod(ClassViolatingCodingRules.class, "printToStandardStream")
.getting().field(System.class, "out")
.inLine(12))
Expand All @@ -178,10 +198,11 @@ Stream<DynamicTest> CodingRulesTest() {
.inLine(14))
.by(callFromMethod(ServiceViolatingLayerRules.class, "illegalAccessToController")
.getting().field(System.class, "out")
.inLine(16))
.times(2)
.inLine(16));
}

.ofRule("no classes should throw generic exceptions")
private static void expectThrownGenericExceptions(ExpectedTestFailures expectFailures) {
expectFailures
.by(callFromMethod(ClassViolatingCodingRules.class, "throwGenericExceptions")
.toConstructor(Throwable.class)
.inLine(22))
Expand All @@ -193,14 +214,7 @@ Stream<DynamicTest> CodingRulesTest() {
.inLine(26))
.by(callFromMethod(ClassViolatingCodingRules.class, "throwGenericExceptions")
.toConstructor(Exception.class, String.class)
.inLine(26))

.ofRule("no classes should use java.util.logging")
.by(callFromStaticInitializer(ClassViolatingCodingRules.class)
.setting().field(ClassViolatingCodingRules.class, "log")
.inLine(9))

.toDynamicTests();
.inLine(26));
}

@TestFactory
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ private static class ExpectedViolationToAssign {

static ExpectedViolationToAssign byRuleText(String ruleText) {
return new ExpectedViolationToAssign(
failure -> failure.error.getMessage().contains(ruleText),
failure -> failure.error.getMessage().contains(String.format("Rule '%s'", ruleText)),
ExpectedViolation.ofRule(ruleText),
HandlingAssertion.ofRule());
}
Expand Down
11 changes: 10 additions & 1 deletion archunit/src/main/java/com/tngtech/archunit/lang/ArchRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,11 @@ public static <T> ArchRule create(final ClassesTransformer<T> classesTransformer
}

public static ArchRule withBecause(ArchRule rule, String reason) {
return rule.as(rule.getDescription() + ", because " + reason);
return rule.as(createBecauseDescription(rule, reason));
}

static String createBecauseDescription(ArchRule rule, String reason) {
return rule.getDescription() + ", because " + reason;
}

private static class SimpleArchRule<T> implements ArchRule {
Expand Down Expand Up @@ -215,6 +219,11 @@ public String getDescription() {
overriddenDescription.get() :
ConfiguredMessageFormat.get().formatRuleText(classesTransformer, condition);
}

@Override
public String toString() {
return getDescription();
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2018 TNG Technology Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.lang;

import java.util.List;

import com.google.common.collect.ImmutableList;
import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.core.domain.JavaClasses;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
import static com.tngtech.archunit.lang.ArchRule.Factory.createBecauseDescription;
import static java.util.Collections.singletonList;

public final class CompositeArchRule implements ArchRule {
private final List<ArchRule> rules;
private final String description;

private CompositeArchRule(List<ArchRule> rules, String description) {
this.rules = checkNotNull(rules);
this.description = checkNotNull(description);
}

@PublicAPI(usage = ACCESS)
public static CompositeArchRule of(ArchRule rule) {
return new CompositeArchRule(singletonList(rule), rule.getDescription());
}

@PublicAPI(usage = ACCESS)
public CompositeArchRule and(ArchRule rule) {
List<ArchRule> newRules = ImmutableList.<ArchRule>builder().addAll(rules).add(rule).build();
String newDescription = description + " and " + rule.getDescription();
return new CompositeArchRule(newRules, newDescription);
}

@Override
public void check(JavaClasses classes) {
Assertions.check(this, classes);
}

@Override
public CompositeArchRule because(String reason) {
return new CompositeArchRule(rules, createBecauseDescription(this, reason));
}

@Override
public EvaluationResult evaluate(JavaClasses classes) {
EvaluationResult result = new EvaluationResult(this, Priority.MEDIUM);
for (ArchRule rule : rules) {
result.add(rule.evaluate(classes));
}
return result;
}

@Override
public CompositeArchRule as(String newDescription) {
return new CompositeArchRule(rules, newDescription);
}

@Override
public String getDescription() {
return description;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

import static com.tngtech.archunit.core.domain.TestUtils.importClassWithContext;
import static com.tngtech.archunit.testutil.Assertions.assertThat;
import static org.mockito.Matchers.argThat;
import static org.mockito.Mockito.argThat;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.tngtech.archunit.lang;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.java.junit.dataprovider.DataProvider;
import com.tngtech.java.junit.dataprovider.DataProviderRunner;
import com.tngtech.java.junit.dataprovider.UseDataProvider;
import org.junit.Test;
import org.junit.runner.RunWith;

import static com.tngtech.archunit.core.domain.TestUtils.importClasses;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static com.tngtech.java.junit.dataprovider.DataProviders.$;
import static com.tngtech.java.junit.dataprovider.DataProviders.$$;
import static org.assertj.core.api.Assertions.assertThat;

@RunWith(DataProviderRunner.class)
public class CompositeArchRuleTest {
private static final boolean SATISFIED = true;
private static final boolean UNSATISFIED = false;

@DataProvider
public static Object[][] rules_to_AND() {
return $$(
$(archRuleThatSucceeds(), archRuleThatSucceeds(), SATISFIED),
$(archRuleThatSucceeds(), archRuleThatFails(), UNSATISFIED),
$(archRuleThatFails(), archRuleThatSucceeds(), UNSATISFIED),
$(archRuleThatFails(), archRuleThatFails(), UNSATISFIED)
);
}

@Test
@UseDataProvider("rules_to_AND")
public void rules_are_ANDed(ArchRule first, ArchRule second, boolean expectedSatisfied) {
EvaluationResult result = CompositeArchRule.of(first).and(second).evaluate(importClasses(getClass()));

assertThat(result.hasViolation()).as("result has violation").isEqualTo(!expectedSatisfied);
}

@Test
public void description_is_modified_correctly() {
ArchRule input = classes().should().bePublic();
CompositeArchRule start = CompositeArchRule.of(input);

assertThat(start.getDescription()).isEqualTo(input.getDescription());

CompositeArchRule modified = start.and(classes().should().bePrivate().as("inner changed"));

assertThat(modified.getDescription()).isEqualTo(start.getDescription() + " and inner changed");

modified = modified.as("overridden")
.because("reason")
.and(classes().should().bePublic().as("changed"));

assertThat(modified.getDescription()).isEqualTo("overridden, because reason and changed");
}

private static ArchRule archRuleThatSucceeds() {
return createArchRuleWithSatisfied(true);
}

private static ArchRule archRuleThatFails() {
return createArchRuleWithSatisfied(false);
}

private static ArchRule createArchRuleWithSatisfied(final boolean satisfied) {
return ArchRule.Factory.create(new AbstractClassesTransformer<JavaClass>("irrelevant") {
@Override
public Iterable<JavaClass> doTransform(JavaClasses collection) {
return collection;
}
}, new ArchCondition<JavaClass>("irrelevant") {
@Override
public void check(JavaClass item, ConditionEvents events) {
events.add(new SimpleConditionEvent(item, satisfied, "irrelevant"));
}
}, Priority.MEDIUM).as(String.format("%s rule", satisfied ? "satisfied" : "failing"));
}
}
Loading

0 comments on commit f7eaf7f

Please sign in to comment.