Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ensureAllClassesAreContainedInLayers() to Architectures #278

Merged
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
171 changes: 166 additions & 5 deletions archunit/src/main/java/com/tngtech/archunit/library/Architectures.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,28 +119,32 @@ public static final class LayeredArchitecture implements ArchRule {
private final PredicateAggregator<Dependency> irrelevantDependenciesPredicate;
private final Optional<String> overriddenDescription;
private final boolean optionalLayers;
private final AllClassesAreContainedInArchitectureCheck allClassesAreContainedInArchitectureCheck;

private LayeredArchitecture(DependencySettings dependencySettings) {
this(new LayerDefinitions(),
new LinkedHashSet<>(),
dependencySettings,
new PredicateAggregator<Dependency>().thatORs(),
Optional.empty(),
false);
false,
new AllClassesAreContainedInArchitectureCheck.Disabled());
}

private LayeredArchitecture(LayerDefinitions layerDefinitions,
Set<LayerDependencySpecification> dependencySpecifications,
DependencySettings dependencySettings,
PredicateAggregator<Dependency> irrelevantDependenciesPredicate,
Optional<String> overriddenDescription,
boolean optionalLayers) {
boolean optionalLayers,
AllClassesAreContainedInArchitectureCheck allClassesAreContainedInArchitectureCheck) {
this.layerDefinitions = layerDefinitions;
this.dependencySpecifications = dependencySpecifications;
this.dependencySettings = dependencySettings;
this.irrelevantDependenciesPredicate = irrelevantDependenciesPredicate;
this.overriddenDescription = overriddenDescription;
this.optionalLayers = optionalLayers;
this.allClassesAreContainedInArchitectureCheck = allClassesAreContainedInArchitectureCheck;
}

/**
Expand All @@ -158,7 +162,8 @@ public LayeredArchitecture withOptionalLayers(boolean optionalLayers) {
dependencySettings,
irrelevantDependenciesPredicate,
overriddenDescription,
optionalLayers
optionalLayers,
allClassesAreContainedInArchitectureCheck
);
}

Expand Down Expand Up @@ -223,6 +228,8 @@ public String toString() {
public EvaluationResult evaluate(JavaClasses classes) {
EvaluationResult result = new EvaluationResult(this, Priority.MEDIUM);
checkEmptyLayers(classes, result);
allClassesAreContainedInArchitectureCheck.evaluate(classes, layerDefinitions).ifPresent(result::add);

for (LayerDependencySpecification specification : dependencySpecifications) {
result.add(evaluateDependenciesShouldBeSatisfied(classes, specification));
}
Expand All @@ -239,6 +246,54 @@ private void checkEmptyLayers(JavaClasses classes, EvaluationResult result) {
}
}

/**
* Ensure that all classes under test are contained within a defined layer of the architecture.
*
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(String...)
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate)
*/
@PublicAPI(usage = ACCESS)
public LayeredArchitecture ensureAllClassesAreContainedInArchitecture() {
return ensureAllClassesAreContainedInArchitectureIgnoring(alwaysFalse());
}

/**
* Like {@link #ensureAllClassesAreContainedInArchitecture()} but will ignore classes in packages matching
* the specified {@link PackageMatcher packageIdentifiers}.
*
* @param packageIdentifiers {@link PackageMatcher packageIdentifiers} specifying which classes may live outside the architecture
*
* @see #ensureAllClassesAreContainedInArchitecture()
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate)
*/
@PublicAPI(usage = ACCESS)
public LayeredArchitecture ensureAllClassesAreContainedInArchitectureIgnoring(String... packageIdentifiers) {
return ensureAllClassesAreContainedInArchitectureIgnoring(
resideInAnyPackage(packageIdentifiers).as(joinSingleQuoted(packageIdentifiers)));
}

/**
* Like {@link #ensureAllClassesAreContainedInArchitecture()} but will ignore classes in packages matching
* the specified {@link DescribedPredicate predicate}.
*
* @param predicate {@link DescribedPredicate predicate} specifying which classes may live outside the architecture
*
* @see #ensureAllClassesAreContainedInArchitecture()
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(String...)
*/
@PublicAPI(usage = ACCESS)
public LayeredArchitecture ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate<? super JavaClass> predicate) {
return new LayeredArchitecture(
layerDefinitions,
dependencySpecifications,
dependencySettings,
irrelevantDependenciesPredicate,
overriddenDescription,
optionalLayers,
new AllClassesAreContainedInArchitectureCheck.Enabled(predicate)
);
}

private EvaluationResult evaluateLayersShouldNotBeEmpty(JavaClasses classes, LayerDefinition layerDefinition) {
return classes().that(layerDefinitions.containsPredicateFor(layerDefinition.name))
.should(notBeEmptyFor(layerDefinition))
Expand Down Expand Up @@ -310,7 +365,8 @@ public LayeredArchitecture as(String newDescription) {
dependencySettings,
irrelevantDependenciesPredicate,
Optional.of(newDescription),
optionalLayers
optionalLayers,
allClassesAreContainedInArchitectureCheck
);
}

Expand Down Expand Up @@ -347,7 +403,8 @@ public LayeredArchitecture ignoreDependency(
dependencySettings,
irrelevantDependenciesPredicate.add(dependency(origin, target)),
overriddenDescription,
optionalLayers
optionalLayers,
allClassesAreContainedInArchitectureCheck
);
}

Expand All @@ -369,6 +426,41 @@ private void checkLayerNamesExist(String... layerNames) {
}
}

private abstract static class AllClassesAreContainedInArchitectureCheck {
abstract Optional<EvaluationResult> evaluate(final JavaClasses classes, final LayerDefinitions layerDefinitions);

static class Enabled extends AllClassesAreContainedInArchitectureCheck {
private final DescribedPredicate<? super JavaClass> ignorePredicate;

private Enabled(DescribedPredicate<? super JavaClass> ignorePredicate) {
this.ignorePredicate = ignorePredicate;
}

Optional<EvaluationResult> evaluate(final JavaClasses classes, final LayerDefinitions layerDefinitions) {
return Optional.of(classes().should(beContainedInLayers(layerDefinitions)).evaluate(classes));
}

private ArchCondition<JavaClass> beContainedInLayers(LayerDefinitions layerDefinitions) {
DescribedPredicate<JavaClass> classContainedInLayers = layerDefinitions.containsPredicateForAll();
return new ArchCondition<JavaClass>("be contained in architecture") {
@Override
public void check(JavaClass javaClass, ConditionEvents events) {
if (!ignorePredicate.test(javaClass) && !classContainedInLayers.test(javaClass)) {
events.add(violated(this, String.format("Class <%s> is not contained in architecture", javaClass.getName())));
}
}
};
}
}

static class Disabled extends AllClassesAreContainedInArchitectureCheck {
@Override
Optional<EvaluationResult> evaluate(JavaClasses classes, LayerDefinitions layerDefinitions) {
return Optional.empty();
}
}
}

private static ArchCondition<JavaClass> notBeEmptyFor(final LayeredArchitecture.LayerDefinition layerDefinition) {
return new LayerShouldNotBeEmptyCondition(layerDefinition);
}
Expand Down Expand Up @@ -674,6 +766,7 @@ public static final class OnionArchitecture implements ArchRule {
private Map<String, DescribedPredicate<? super JavaClass>> adapterPredicates = new LinkedHashMap<>();
private boolean optionalLayers = false;
private List<IgnoredDependency> ignoredDependencies = new ArrayList<>();
private AllClassesAreContainedInArchitectureCheck allClassesAreContainedInArchitectureCheck = new AllClassesAreContainedInArchitectureCheck.Disabled();

private OnionArchitecture() {
overriddenDescription = Optional.empty();
Expand Down Expand Up @@ -764,6 +857,46 @@ public OnionArchitecture ignoreDependency(DescribedPredicate<? super JavaClass>
return this;
}

/**
* Ensure that all classes under test are contained within a defined onion architecture component.
*
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(String...)
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate)
*/
@PublicAPI(usage = ACCESS)
public OnionArchitecture ensureAllClassesAreContainedInArchitecture() {
return ensureAllClassesAreContainedInArchitectureIgnoring(alwaysFalse());
}

/**
* Like {@link #ensureAllClassesAreContainedInArchitecture()} but will ignore classes in packages matching
* the specified {@link PackageMatcher packageIdentifiers}.
*
* @param packageIdentifiers {@link PackageMatcher packageIdentifiers} specifying which classes may live outside the architecture
*
* @see #ensureAllClassesAreContainedInArchitecture()
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate)
*/
@PublicAPI(usage = ACCESS)
public OnionArchitecture ensureAllClassesAreContainedInArchitectureIgnoring(String... packageIdentifiers) {
return ensureAllClassesAreContainedInArchitectureIgnoring(resideInAnyPackage(packageIdentifiers));
}

/**
* Like {@link #ensureAllClassesAreContainedInArchitecture()} but will ignore classes in packages matching
* the specified {@link DescribedPredicate predicate}.
*
* @param predicate {@link DescribedPredicate predicate} specifying which classes may live outside the architecture
*
* @see #ensureAllClassesAreContainedInArchitecture()
* @see #ensureAllClassesAreContainedInArchitectureIgnoring(String...)
*/
@PublicAPI(usage = ACCESS)
public OnionArchitecture ensureAllClassesAreContainedInArchitectureIgnoring(DescribedPredicate<? super JavaClass> predicate) {
allClassesAreContainedInArchitectureCheck = new AllClassesAreContainedInArchitectureCheck.Enabled(predicate);
return this;
}

private DescribedPredicate<JavaClass> byPackagePredicate(String[] packageIdentifiers) {
return resideInAnyPackage(packageIdentifiers).as(joinSingleQuoted(packageIdentifiers));
}
Expand All @@ -785,9 +918,13 @@ private LayeredArchitecture layeredArchitectureDelegate() {
.layer(adapterLayer).definedBy(adapter.getValue())
.whereLayer(adapterLayer).mayNotBeAccessedByAnyLayer();
}

for (IgnoredDependency ignoredDependency : this.ignoredDependencies) {
layeredArchitectureDelegate = ignoredDependency.ignoreFor(layeredArchitectureDelegate);
}

layeredArchitectureDelegate = allClassesAreContainedInArchitectureCheck.configure(layeredArchitectureDelegate);

return layeredArchitectureDelegate.as(getDescription());
}

Expand Down Expand Up @@ -863,5 +1000,29 @@ LayeredArchitecture ignoreFor(LayeredArchitecture layeredArchitecture) {
return layeredArchitecture.ignoreDependency(origin, target);
}
}

private abstract static class AllClassesAreContainedInArchitectureCheck {
abstract LayeredArchitecture configure(LayeredArchitecture layeredArchitecture);

static class Enabled extends AllClassesAreContainedInArchitectureCheck {
private final DescribedPredicate<? super JavaClass> ignorePredicate;

private Enabled(DescribedPredicate<? super JavaClass> ignorePredicate) {
this.ignorePredicate = ignorePredicate;
}

@Override
LayeredArchitecture configure(LayeredArchitecture layeredArchitecture) {
return layeredArchitecture.ensureAllClassesAreContainedInArchitectureIgnoring(ignorePredicate);
}
}

static class Disabled extends AllClassesAreContainedInArchitectureCheck {
@Override
LayeredArchitecture configure(LayeredArchitecture layeredArchitecture) {
return layeredArchitecture;
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.EvaluationResult;
import com.tngtech.archunit.library.Architectures.LayeredArchitecture;
import com.tngtech.archunit.library.testclasses.coveringallclasses.first.First;
import com.tngtech.archunit.library.testclasses.coveringallclasses.second.Second;
import com.tngtech.archunit.library.testclasses.coveringallclasses.third.Third;
import com.tngtech.archunit.library.testclasses.dependencysettings.DependencySettingsOutsideOfLayersAccessingLayers;
import com.tngtech.archunit.library.testclasses.dependencysettings.forbidden_backwards.DependencySettingsForbiddenByMayOnlyBeAccessed;
import com.tngtech.archunit.library.testclasses.dependencysettings.forbidden_forwards.DependencySettingsForbiddenByMayOnlyAccess;
Expand All @@ -34,6 +37,7 @@
import org.junit.runner.RunWith;

import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAnyPackage;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.simpleName;
import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME;
import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name;
import static com.tngtech.archunit.library.Architectures.layeredArchitecture;
Expand Down Expand Up @@ -379,6 +383,46 @@ public void layered_architecture_supports_dependency_setting_considering_only_de
assertPatternMatches(result.getFailureReport().getDetails(), dependencySettingsViolationsInLayers());
}

@Test
public void layered_architecture_ensure_all_classes_are_contained_in_architecture() {
JavaClasses classes = new ClassFileImporter().importClasses(First.class, Second.class);

LayeredArchitecture architectureNotCoveringAllClasses = layeredArchitecture().consideringAllDependencies()
.layer("One").definedBy("..first..")
.ensureAllClassesAreContainedInArchitecture();

assertThatRule(architectureNotCoveringAllClasses).checking(classes)
.hasOnlyOneViolation("Class <" + Second.class.getName() + "> is not contained in architecture");

LayeredArchitecture architectureCoveringAllClasses = architectureNotCoveringAllClasses
.layer("Two").definedBy("..second..");
assertThatRule(architectureCoveringAllClasses).checking(classes).hasNoViolation();
}

@Test
public void layered_architecture_ensure_all_classes_are_contained_in_architecture_ignoring_packages() {
JavaClasses classes = new ClassFileImporter().importClasses(First.class, Second.class, Third.class);

LayeredArchitecture architecture = layeredArchitecture().consideringAllDependencies()
.layer("One").definedBy("..first..")
.ensureAllClassesAreContainedInArchitectureIgnoring("..second..");

assertThatRule(architecture).checking(classes)
.hasOnlyOneViolation("Class <" + Third.class.getName() + "> is not contained in architecture");
}

@Test
public void layered_architecture_ensure_all_classes_are_contained_in_architecture_ignoring_predicate() {
JavaClasses classes = new ClassFileImporter().importClasses(First.class, Second.class, Third.class);

LayeredArchitecture architecture = layeredArchitecture().consideringAllDependencies()
.layer("One").definedBy("..first..")
.ensureAllClassesAreContainedInArchitectureIgnoring(simpleName("Second"));

assertThatRule(architecture).checking(classes)
.hasOnlyOneViolation("Class <" + Third.class.getName() + "> is not contained in architecture");
}

private LayeredArchitecture defineLayeredArchitectureForDependencySettings(LayeredArchitecture layeredArchitecture) {
return layeredArchitecture
.layer("Origin").definedBy("..library.testclasses.dependencysettings.origin..")
Expand Down
Loading