package com.tngtech.archunit.lang.conditions; import java.util.Collection; import java.util.List; import com.google.common.collect.ImmutableList; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaCall; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.lang.ConditionEvent; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; import org.assertj.core.api.Condition; import org.junit.Test; import org.junit.runner.RunWith; import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.target; import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.targetOwner; import static com.tngtech.archunit.core.domain.JavaCall.Predicates.target; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; import static com.tngtech.archunit.core.domain.JavaClass.namesOf; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name; import static com.tngtech.archunit.core.domain.properties.HasParameterTypes.Predicates.parameterTypes; import static com.tngtech.archunit.lang.conditions.ArchPredicates.has; import static com.tngtech.archunit.lang.conditions.ArchPredicates.is; import static com.tngtech.archunit.lang.conditions.testobjects.TestObjects.CALLER_CLASS; import static com.tngtech.archunit.lang.conditions.testobjects.TestObjects.TARGET_CLASS; 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 CodeUnitCallConditionTest { @DataProvider public static Object[][] calls_to_analyze() { return $$( $(methodCallToTargetFrom(CALLER_CLASS)), $(constructorCallToTargetFrom(CALLER_CLASS))); } @Test @UseDataProvider("calls_to_analyze") public void condition_is_satisfied_by_matching_call(MethodCallToAnalyze callToAnalyze) { CodeUnitCallCondition targetCodeUnitCallCondition = callConditionBuilderMatching(callToAnalyze).build(); ConditionEvents events = new ConditionEvents(); targetCodeUnitCallCondition.check(callToAnalyze.call, events); boolean satisfied = !events.containViolation(); assertThat(satisfied).as("Condition is satisfied").isTrue(); assertThat(events.getViolating()).isEmpty(); assertThat(events.getAllowed()).is(containingMessageFor(callToAnalyze)); } @Test @UseDataProvider("calls_to_analyze") public void condition_is_not_satisfied_on_target_mismatch(MethodCallToAnalyze callToAnalyze) { CodeUnitCallCondition targetCodeUnitCallCondition = callConditionBuilderMatching(callToAnalyze) .withTarget(getClass()) .build(); ConditionEvents events = new ConditionEvents(); targetCodeUnitCallCondition.check(callToAnalyze.call, events); boolean satisfied = !events.containViolation(); assertThat(satisfied).as("Condition is satisfied").isFalse(); assertThat(events.getViolating()).is(containingMessageFor(callToAnalyze)); assertThat(events.getAllowed()).isEmpty(); } @Test @UseDataProvider("calls_to_analyze") public void condition_is_not_satisfied_on_name_mismatch(MethodCallToAnalyze callToAnalyze) { CodeUnitCallCondition targetCodeUnitCallCondition = callConditionBuilderMatching(callToAnalyze) .withName("wrong") .build(); ConditionEvents events = new ConditionEvents(); targetCodeUnitCallCondition.check(callToAnalyze.call, events); boolean satisfied = !events.containViolation(); assertThat(satisfied).as("Condition is satisfied").isFalse(); assertThat(events.getViolating()).is(containingMessageFor(callToAnalyze)); assertThat(events.getAllowed()).isEmpty(); } @Test @UseDataProvider("calls_to_analyze") public void condition_is_not_satisfied_on_parameter_type_mismatch(MethodCallToAnalyze callToAnalyze) { CodeUnitCallCondition targetCodeUnitCallCondition = callConditionBuilderMatching(callToAnalyze) .withParameters(getClass()) .build(); ConditionEvents events = new ConditionEvents(); targetCodeUnitCallCondition.check(callToAnalyze.call, events); boolean satisfied = !events.containViolation(); assertThat(satisfied).as("Condition is satisfied").isFalse(); assertThat(events.getViolating()).is(containingMessageFor(callToAnalyze)); assertThat(events.getAllowed()).isEmpty(); } private MethodCallConditionBuilder callConditionBuilderMatching(MethodCallToAnalyze callToAnalyze) { return new MethodCallConditionBuilder(callToAnalyze); } private static MethodCallToAnalyze methodCallToTargetFrom(JavaClass callerClass) { return new MethodCallToAnalyze(callerClass.getMethodCallsFromSelf()); } private static MethodCallToAnalyze constructorCallToTargetFrom(JavaClass callerClass) { return new MethodCallToAnalyze(callerClass.getConstructorCallsFromSelf()); } private static Condition<Iterable<? extends ConditionEvent>> containingMessageFor(final MethodCallToAnalyze callToAnalyze) { final String originName = callToAnalyze.call.getOrigin().getFullName(); final String targetName = callToAnalyze.call.getTarget().getFullName(); final String ideJumpHook = ideJumpHookFor(callToAnalyze); return new Condition<Iterable<? extends ConditionEvent>>() { @Override public boolean matches(Iterable<? extends ConditionEvent> value) { boolean matches = false; for (ConditionEvent event : value) { matches = matches || eventHasCorrectDetails(event); } return matches; } private boolean eventHasCorrectDetails(ConditionEvent event) { return event.toString().contains(originName) && event.toString().contains(targetName) && event.toString().contains(ideJumpHook); } }.as(String.format("Event from call with origin %s, target %s and IDE Hook %s", originName, targetName, ideJumpHook)); } private static String ideJumpHookFor(MethodCallToAnalyze callToAnalyze) { String simpleCallerName = callToAnalyze.call.getOriginOwner().getSimpleName(); int lineNumber = callToAnalyze.call.getLineNumber(); return String.format("(%s.java:%d)", simpleCallerName, lineNumber); } private static class MethodCallToAnalyze { private final JavaCall<?> call; private MethodCallToAnalyze(Collection<? extends JavaCall<?>> calls) { call = callToTargetIn(calls); } private JavaCall<?> callToTargetIn(Collection<? extends JavaCall<?>> calls) { for (JavaCall<?> call : calls) { if (call.getTarget().getOwner().equals(TARGET_CLASS)) { return call; } } throw new RuntimeException("Couldn't find any matching call to " + TARGET_CLASS + " in " + calls); } @Override public String toString() { return call.getDescription(); } } private static class MethodCallConditionBuilder { private Class<?> targetClass; private String methodName; private List<String> paramTypes; private MethodCallConditionBuilder(MethodCallToAnalyze callToAnalyze) { targetClass = callToAnalyze.call.getTarget().getOwner().reflect(); methodName = callToAnalyze.call.getTarget().getName(); paramTypes = callToAnalyze.call.getTarget().getParameters().getNames(); } private MethodCallConditionBuilder withTarget(Class<?> targetClass) { this.targetClass = targetClass; return this; } public MethodCallConditionBuilder withName(String methodName) { this.methodName = methodName; return this; } public MethodCallConditionBuilder withParameters(Class<?>... paramTypes) { this.paramTypes = namesOf(ImmutableList.copyOf(paramTypes)); return this; } private CodeUnitCallCondition build() { DescribedPredicate<JavaCall<?>> ownerAndNameMatch = targetOwner(is(equivalentTo(targetClass))) .and(target(has(name(methodName)))).forSubType(); return new CodeUnitCallCondition(ownerAndNameMatch.and(target(has(parameterTypes(paramTypes))))); } } }