Skip to content

Commit

Permalink
Merge pull request TNG#237 from mikomatic/issue-177
Browse files Browse the repository at this point in the history
Check that layered architecture does not contain empty layers - Issue #177
  • Loading branch information
codecholeric committed Sep 15, 2019
2 parents 9d06eca + b02fdc4 commit 4cad7cc
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 14 deletions.
Expand Up @@ -36,7 +36,9 @@
import com.tngtech.archunit.core.domain.Dependency;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.EvaluationResult;
import com.tngtech.archunit.lang.Priority;
import com.tngtech.archunit.lang.syntax.PredicateAggregator;
Expand All @@ -50,6 +52,7 @@
import static com.tngtech.archunit.core.domain.Dependency.Predicates.dependencyOrigin;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
import static com.tngtech.archunit.lang.SimpleConditionEvent.violated;
import static com.tngtech.archunit.lang.conditions.ArchConditions.onlyHaveDependentsWhere;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
import static java.lang.System.lineSeparator;
Expand Down Expand Up @@ -152,20 +155,31 @@ public String getDescription() {
@PublicAPI(usage = ACCESS)
public EvaluationResult evaluate(JavaClasses classes) {
EvaluationResult result = new EvaluationResult(this, Priority.MEDIUM);
for (LayerDefinition layerDefinition : layerDefinitions.values()) {
result.add(evaluateLayersShouldNotBeEmpty(classes, layerDefinition));
}
for (LayerDependencySpecification specification : dependencySpecifications) {
SortedSet<String> packagesOfOwnLayer = packagesOf(specification.layerName);
SortedSet<String> packagesOfAllowedAccessors = packagesOf(specification.allowedAccessors);
packagesOfAllowedAccessors.addAll(packagesOfOwnLayer);

EvaluationResult partial = classes().that().resideInAnyPackage(toArray(packagesOfOwnLayer))
.should(onlyHaveDependentsWhere(originPackageMatchesIfDependencyIsRelevant(packagesOfAllowedAccessors)))
.evaluate(classes);

result.add(partial);
result.add(evaluateDependenciesShouldBeSatisfied(classes, specification));
}
return result;
}

private EvaluationResult evaluateLayersShouldNotBeEmpty(JavaClasses classes, LayerDefinition layerDefinition) {
return classes().that().resideInAnyPackage(toArray(layerDefinition.packageIdentifiers))
.should(notBeEmptyFor(layerDefinition))
.evaluate(classes);
}

private EvaluationResult evaluateDependenciesShouldBeSatisfied(JavaClasses classes, LayerDependencySpecification specification) {
SortedSet<String> packagesOfOwnLayer = packagesOf(specification.layerName);
SortedSet<String> packagesOfAllowedAccessors = packagesOf(specification.allowedAccessors);
packagesOfAllowedAccessors.addAll(packagesOfOwnLayer);

return classes().that().resideInAnyPackage(toArray(packagesOfOwnLayer))
.should(onlyHaveDependentsWhere(originPackageMatchesIfDependencyIsRelevant(packagesOfAllowedAccessors)))
.evaluate(classes);
}

private DescribedPredicate<Dependency> originPackageMatchesIfDependencyIsRelevant(SortedSet<String> packagesOfAllowedAccessors) {
DescribedPredicate<Dependency> originPackageMatches =
dependencyOrigin(JavaClass.Functions.GET_PACKAGE_NAME.is(PackageMatchers.of(toArray(packagesOfAllowedAccessors))));
Expand All @@ -175,6 +189,32 @@ private DescribedPredicate<Dependency> originPackageMatchesIfDependencyIsRelevan
originPackageMatches;
}

private static ArchCondition<JavaClass> notBeEmptyFor(final LayeredArchitecture.LayerDefinition layerDefinition) {
return new LayerShouldNotBeEmptyCondition(layerDefinition);
}

private static class LayerShouldNotBeEmptyCondition extends ArchCondition<JavaClass> {
private final LayeredArchitecture.LayerDefinition layerDefinition;
private boolean empty = true;

LayerShouldNotBeEmptyCondition(final LayeredArchitecture.LayerDefinition layerDefinition) {
super("not be empty");
this.layerDefinition = layerDefinition;
}

@Override
public void check(JavaClass item, ConditionEvents events) {
empty = false;
}

@Override
public void finish(ConditionEvents events) {
if (empty) {
events.add(violated(layerDefinition, String.format("Layer '%s' is empty", layerDefinition.name)));
}
}
}

@Override
@PublicAPI(usage = ACCESS)
public void check(JavaClasses classes) {
Expand Down Expand Up @@ -316,10 +356,10 @@ private OnionArchitecture() {
}

private OnionArchitecture(String[] domainModelPackageIdentifiers,
String[] domainServicePackageIdentifiers,
String[] applicationPackageIdentifiers,
Map<String, String[]> adapterPackageIdentifiers,
Optional<String> overriddenDescription) {
String[] domainServicePackageIdentifiers,
String[] applicationPackageIdentifiers,
Map<String, String[]> adapterPackageIdentifiers,
Optional<String> overriddenDescription) {
this.domainModelPackageIdentifiers = domainModelPackageIdentifiers;
this.domainServicePackageIdentifiers = domainServicePackageIdentifiers;
this.applicationPackageIdentifiers = applicationPackageIdentifiers;
Expand Down
Expand Up @@ -113,6 +113,21 @@ public void layered_architecture_defining_constraint_on_non_existing_origin_is_r
.whereLayer("Some").mayOnlyBeAccessedByLayers("NotThere");
}

@Test
public void layered_architecture_defining_empty_layers_is_rejected() {
LayeredArchitecture architecture = layeredArchitecture()
.layer("Some").definedBy(absolute("should.not.be.found.."))
.layer("Other").definedBy(absolute("also.not.found"))
.layer("Okay").definedBy("..testclasses..");

JavaClasses classes = new ClassFileImporter().importPackages(getClass().getPackage().getName() + ".testclasses");

EvaluationResult result = architecture.evaluate(classes);
assertThat(result.hasViolation()).isTrue();
assertPatternMatches(result.getFailureReport().getDetails(),
ImmutableSet.of(expectedEmptyLayer("Some"), expectedEmptyLayer("Other")));
}

@Test
public void layered_architecture_gathers_all_layer_violations() {
LayeredArchitecture architecture = layeredArchitecture()
Expand Down Expand Up @@ -283,7 +298,7 @@ private void assertPatternMatches(List<String> input, Set<String> expectedRegexe
Set<String> toMatch = new HashSet<>(expectedRegexes);
for (String line : input) {
if (!matchIteratorAndRemove(toMatch, line)) {
Assert.fail("Line " + line + " didn't match any pattern in " + expectedRegexes);
Assert.fail("Line '" + line + "' didn't match any pattern in " + expectedRegexes);
}
}
assertThat(toMatch).as("Unmatched Patterns").isEmpty();
Expand All @@ -303,6 +318,10 @@ private static String expectedAccessViolationPattern(Class<?> from, String fromM
return String.format(".*%s.%s().*%s.%s().*", quote(from.getName()), fromMethod, quote(to.getName()), toMethod);
}

private static String expectedEmptyLayer(String layerName) {
return String.format("Layer '%s' is empty", layerName);
}

private static String fieldTypePattern(Class<?> owner, String fieldName, Class<?> fieldType) {
return String.format("Field .*%s\\.%s.* has type .*<%s>.*", owner.getSimpleName(), fieldName, fieldType.getName());
}
Expand Down

0 comments on commit 4cad7cc

Please sign in to comment.