Skip to content

Commit

Permalink
add modules()...should().beFreeOfCycles() to modules rules API
Browse files Browse the repository at this point in the history
Signed-off-by: Peter Gafert <[email protected]>
  • Loading branch information
codecholeric committed Mar 4, 2023
1 parent f12fdea commit a1ee308
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,15 @@ public class ModulesTest {
)
.ignoreDependency(alwaysTrue(), equivalentTo(AppModule.class));

/**
* This example demonstrates how to check for cyclic dependencies between modules.
*/
@ArchTest
public static ArchRule modules_should_be_free_of_cycles =
modules()
.definedByAnnotation(AppModule.class)
.should().beFreeOfCycles();

private static DescribedPredicate<ModuleDependency<AnnotationDescriptor<AppModule>>> declaredByDescriptorAnnotation() {
return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> {
AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,15 @@ public class ModulesTest {
)
.ignoreDependency(alwaysTrue(), equivalentTo(AppModule.class));

/**
* This example demonstrates how to check for cyclic dependencies between modules.
*/
@ArchTest
static ArchRule modules_should_be_free_of_cycles =
modules()
.definedByAnnotation(AppModule.class)
.should().beFreeOfCycles();

private static DescribedPredicate<ModuleDependency<AnnotationDescriptor<AppModule>>> declaredByDescriptorAnnotation() {
return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> {
AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,17 @@ public void modules_should_respect_their_declared_dependencies__use_generic_API(
.check(classes);
}

/**
* This example demonstrates how to check for cyclic dependencies between modules.
*/
@Test
public void modules_should_be_free_of_cycles() {
modules()
.definedByAnnotation(AppModule.class)
.should().beFreeOfCycles()
.check(classes);
}

private static DescribedPredicate<ModuleDependency<AnnotationDescriptor<AppModule>>> declaredByDescriptorAnnotation() {
return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> {
AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1190,6 +1190,144 @@ Stream<DynamicTest> ModulesTest() {
+ "should respect their allowed dependencies declared by descriptor annotation considering only dependencies in any package ['..example..']");
expectModulesViolations.accept(expectedFailures);

expectedFailures.ofRule("modules defined by annotation @AppModule should be free of cycles")
.by(cycle()
.from("Address")
.by(field(Address.class, "productCatalog").ofType(ProductCatalog.class))
.from("Catalog")
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toConstructor(Order.class)
.inLine(12))
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toMethod(Order.class, "addProducts", Set.class)
.inLine(16))
.from("Order")
.by(method(Order.class, "report").withParameter(Address.class)))
.by(cycle()
.from("Address")
.by(field(Address.class, "productCatalog").ofType(ProductCatalog.class))
.from("Catalog")
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toConstructor(Order.class)
.inLine(12))
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toMethod(Order.class, "addProducts", Set.class)
.inLine(16))
.from("Order")
.by(field(Order.class, "customer").ofType(Customer.class))
.by(callFromMethod(Order.class, "report")
.toMethod(Customer.class, "getAddress")
.inLine(18))
.from("Customer")
.by(field(Customer.class, "address").ofType(Address.class))
.by(method(Customer.class, "getAddress").withReturnType(Address.class)))
.by(cycle()
.from("Address")
.by(field(Address.class, "productCatalog").ofType(ProductCatalog.class))
.from("Catalog")
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toConstructor(Order.class)
.inLine(12))
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toMethod(Order.class, "addProducts", Set.class)
.inLine(16))
.from("Order")
.by(genericFieldType(Order.class, "products").dependingOn(Product.class))
.by(genericMethodParameterType(Order.class, "addProducts", Set.class).dependingOn(Product.class))
.by(callFromMethod(Order.class, "report")
.toMethod(Product.class, "report")
.inLine(20))
.from("Product")
.by(field(Product.class, "customer").ofType(Customer.class))
.from("Customer")
.by(field(Customer.class, "address").ofType(Address.class))
.by(method(Customer.class, "getAddress").withReturnType(Address.class)))
.by(cycle()
.from("Address")
.by(field(Address.class, "productCatalog").ofType(ProductCatalog.class))
.from("Catalog")
.by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class))
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toMethod(Product.class, "register")
.inLine(14))
.from("Product")
.by(field(Product.class, "customer").ofType(Customer.class))
.from("Customer")
.by(field(Customer.class, "address").ofType(Address.class))
.by(method(Customer.class, "getAddress").withReturnType(Address.class)))
.by(cycle()
.from("Address")
.by(field(Address.class, "productCatalog").ofType(ProductCatalog.class))
.from("Catalog")
.by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class))
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toMethod(Product.class, "register")
.inLine(14))
.from("Product")
.by(field(Product.class, "customer").ofType(Customer.class))
.from("Customer")
.by(method(Customer.class, "addOrder").withParameter(Order.class))
.from("Order")
.by(method(Order.class, "report").withParameter(Address.class)))
.by(cycle()
.from("Address")
.by(field(Address.class, "productCatalog").ofType(ProductCatalog.class))
.from("Catalog")
.by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class))
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toMethod(Product.class, "register")
.inLine(14))
.from("Product")
.by(method(Product.class, "getOrder").withReturnType(Order.class))
.from("Order")
.by(method(Order.class, "report").withParameter(Address.class)))
.by(cycle()
.from("Address")
.by(field(Address.class, "productCatalog").ofType(ProductCatalog.class))
.from("Catalog")
.by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class))
.by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder")
.toMethod(Product.class, "register")
.inLine(14))
.from("Product")
.by(method(Product.class, "getOrder").withReturnType(Order.class))
.from("Order")
.by(field(Order.class, "customer").ofType(Customer.class))
.by(callFromMethod(Order.class, "report")
.toMethod(Customer.class, "getAddress")
.inLine(18))
.from("Customer")
.by(field(Customer.class, "address").ofType(Address.class))
.by(method(Customer.class, "getAddress").withReturnType(Address.class)))
.by(cycle()
.from("Customer")
.by(method(Customer.class, "addOrder").withParameter(Order.class))
.from("Order")
.by(field(Order.class, "customer").ofType(Customer.class))
.by(callFromMethod(Order.class, "report")
.toMethod(Customer.class, "getAddress")
.inLine(18)))
.by(cycle()
.from("Customer")
.by(method(Customer.class, "addOrder").withParameter(Order.class))
.from("Order")
.by(genericFieldType(Order.class, "products").dependingOn(Product.class))
.by(genericMethodParameterType(Order.class, "addProducts", Set.class).dependingOn(Product.class))
.by(callFromMethod(Order.class, "report")
.toMethod(Product.class, "report")
.inLine(20))
.from("Product")
.by(field(Product.class, "customer").ofType(Customer.class)))
.by(cycle()
.from("Order")
.by(genericFieldType(Order.class, "products").dependingOn(Product.class))
.by(genericMethodParameterType(Order.class, "addProducts", Set.class).dependingOn(Product.class))
.by(callFromMethod(Order.class, "report")
.toMethod(Product.class, "report")
.inLine(20))
.from("Product")
.by(method(Product.class, "getOrder").withReturnType(Order.class)));

return expectedFailures.toDynamicTests();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,13 @@ ModulesRule respectTheirAllowedDependencies(
DescribedPredicate<? super ModuleDependency<DESCRIPTOR>> allowedDependencyPredicate,
ModuleDependencyScope dependencyScope
);

/**
* Checks that the {@link ArchModule}s under consideration don't have any cyclic dependencies within their
* {@link ArchModule#getModuleDependenciesFromSelf() module dependencies}.
*
* @return A {@link ArchRule} to be checked against a set of {@link JavaClasses}
*/
@PublicAPI(usage = ACCESS)
ModulesRule beFreeOfCycles();
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.EvaluationResult;
import com.tngtech.archunit.library.cycle_detection.rules.CycleArchCondition;
import com.tngtech.archunit.library.modules.ArchModule;
import com.tngtech.archunit.library.modules.ModuleDependency;

Expand All @@ -49,6 +50,19 @@ public ModulesRule respectTheirAllowedDependencies(DescribedPredicate<? super Mo
);
}

@Override
public ModulesRule beFreeOfCycles() {
return new ModulesRuleInternal<>(
createRule,
relevantClassDependencyPredicate -> CycleArchCondition.<ArchModule<DESCRIPTOR>>builder()
.retrieveClassesBy(Function.identity())
.retrieveDescriptionBy(ArchModule::getName)
.retrieveOutgoingDependenciesBy(ArchModule::getClassDependenciesFromSelf)
.onlyConsiderDependencies(relevantClassDependencyPredicate)
.build()
);
}

private static class RespectTheirAllowedDependenciesCondition<DESCRIPTOR extends ArchModule.Descriptor> extends ArchCondition<ArchModule<DESCRIPTOR>> {
private final DescribedPredicate<ModuleDependency<DESCRIPTOR>> allowedModuleDependencyPredicate;
private final ModuleDependencyScope dependencyScope;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ public void respect_their_allowed_dependencies_ignores_dependencies(Function<Mod
.hasNoViolationContaining(ArchRule.class.getName());
}

@Test
public void be_free_of_cycles_ignores_dependencies() {
ModulesRule rule = modulesByClassName().should().beFreeOfCycles().ignoreDependency(d -> d.getDescription().contains("cyclicDependencyOne"));

assertThatRule(rule)
.checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class))
.hasOnlyOneViolationContaining("Cycle detected")
.hasViolationContaining("cyclicDependencyTwo")
.hasNoViolationContaining("cyclicDependencyOne");

rule = rule.ignoreDependency(d -> d.getDescription().contains("cyclicDependencyTwo"));

assertThatRule(rule)
.checking(new ClassFileImporter().importClasses(ModuleOne.class, ModuleTwo.class))
.hasNoViolation();
}

@SuppressWarnings("unused")
private static class ModuleOne {
ModuleTwo dependencyToOtherModule;
Expand All @@ -81,6 +98,8 @@ private static class ModuleOne {

@SuppressWarnings("unused")
private static class ModuleTwo {
ModuleOne cyclicDependencyOne;
ModuleOne cyclicDependencyTwo;
ArchRule dependencyInOtherArchUnitPackage;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ private String toViolationMessage(Class<?> violatingClass, String violationDescr
return "Class <" + violatingClass.getName() + "> " + violationDescription + " in (" + violatingClass.getSimpleName() + ".java:0)";
}

@SuppressWarnings("OptionalGetWithoutIsPresent")
public ArchRuleCheckAssertion hasOnlyOneViolationContaining(String part) {
assertThat(getOnlyElement(evaluationResult.getFailureReport().getDetails())).contains(part);
assertThat(error.get().getMessage()).contains(part);
return this;
}

@SuppressWarnings("OptionalGetWithoutIsPresent")
public ArchRuleCheckAssertion hasOnlyOneViolationMatching(String regex) {
assertThat(getOnlyElement(evaluationResult.getFailureReport().getDetails())).matches(regex);
Expand Down

0 comments on commit a1ee308

Please sign in to comment.