package com.tngtech.archunit.testutil; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import com.google.common.base.Strings; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.tngtech.archunit.base.Optional; import com.tngtech.archunit.core.domain.AccessTarget.FieldAccessTarget; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClassList; import com.tngtech.archunit.core.domain.JavaConstructor; import com.tngtech.archunit.core.domain.JavaEnumConstant; import com.tngtech.archunit.core.domain.JavaField; import com.tngtech.archunit.core.domain.JavaMember; import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.domain.JavaModifier; import com.tngtech.archunit.lang.CollectsLines; import com.tngtech.archunit.lang.ConditionEvent; import com.tngtech.archunit.lang.ConditionEvents; import org.assertj.core.api.AbstractCharSequenceAssert; import org.assertj.core.api.AbstractIterableAssert; import org.assertj.core.api.AbstractListAssert; import org.assertj.core.api.AbstractObjectAssert; import org.objectweb.asm.Type; import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Lists.newArrayList; import static com.tngtech.archunit.core.domain.Formatters.formatMethodParameterTypeNames; import static com.tngtech.archunit.core.domain.JavaClass.namesOf; import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; import static com.tngtech.archunit.core.domain.JavaModifier.ABSTRACT; import static com.tngtech.archunit.core.domain.JavaModifier.FINAL; import static com.tngtech.archunit.core.domain.JavaModifier.NATIVE; import static com.tngtech.archunit.core.domain.JavaModifier.PRIVATE; import static com.tngtech.archunit.core.domain.JavaModifier.PROTECTED; import static com.tngtech.archunit.core.domain.JavaModifier.PUBLIC; import static com.tngtech.archunit.core.domain.JavaModifier.STATIC; import static com.tngtech.archunit.core.domain.JavaModifier.SYNCHRONIZED; import static com.tngtech.archunit.core.domain.JavaModifier.TRANSIENT; import static com.tngtech.archunit.core.domain.JavaModifier.VOLATILE; import static com.tngtech.archunit.core.domain.TestUtils.classForName; import static com.tngtech.archunit.core.domain.TestUtils.invoke; public class Assertions extends org.assertj.core.api.Assertions { public static ConditionEventsAssert assertThat(ConditionEvents events) { return new ConditionEventsAssert(events); } public static <T> org.assertj.guava.api.OptionalAssert<T> assertThat(Optional<T> optional) { return org.assertj.guava.api.Assertions.assertThat(com.google.common.base.Optional.fromNullable(optional.orNull())); } public static JavaClassAssertion assertThat(JavaClass javaClass) { return new JavaClassAssertion(javaClass); } public static JavaClassesAssertion assertThatClasses(Iterable<JavaClass> javaClasses) { return new JavaClassesAssertion(javaClasses); } public static JavaClassesAssertion assertThat(JavaClass[] javaClasses) { return new JavaClassesAssertion(javaClasses); } public static JavaClassListAssertion assertThat(JavaClassList javaClasses) { return new JavaClassListAssertion(javaClasses); } public static JavaFieldAssertion assertThat(FieldAccessTarget target) { return assertThat(target.resolveField().get()); } public static JavaFieldAssertion assertThat(JavaField field) { return new JavaFieldAssertion(field); } public static JavaMethodAssertion assertThat(JavaMethod method) { return new JavaMethodAssertion(method); } public static JavaConstructorAssertion assertThat(JavaConstructor constructor) { return new JavaConstructorAssertion(constructor); } public static JavaEnumConstantAssertion assertThat(JavaEnumConstant enumConstant) { return new JavaEnumConstantAssertion(enumConstant); } public static JavaEnumConstantsAssertion assertThat(JavaEnumConstant[] enumConstants) { return new JavaEnumConstantsAssertion(enumConstants); } public static class JavaClassesAssertion extends AbstractObjectAssert<JavaClassesAssertion, JavaClass[]> { private JavaClassesAssertion(JavaClass[] actual) { super(actual, JavaClassesAssertion.class); } private JavaClassesAssertion(Iterable<JavaClass> actual) { super(sort(actual), JavaClassesAssertion.class); } private static JavaClass[] sort(Iterable<JavaClass> actual) { JavaClass[] result = Iterables.toArray(actual, JavaClass.class); Arrays.sort(result, new Comparator<JavaClass>() { @Override public int compare(JavaClass o1, JavaClass o2) { return o1.getName().compareTo(o2.getName()); } }); return result; } public void matchInAnyOrder(Iterable<Class<?>> classes) { Class<?>[] sorted = Iterables.toArray(classes, Class.class); Arrays.sort(sorted, new Comparator<Class<?>>() { @Override public int compare(Class<?> o1, Class<?> o2) { return o1.getName().compareTo(o2.getName()); } }); matchExactly(sorted); } public void matchInAnyOrder(Class<?>... classes) { matchInAnyOrder(ImmutableSet.copyOf(classes)); } public void matchExactly(Class<?>... classes) { assertThat((Object[]) actual).as("classes").hasSize(classes.length); for (int i = 0; i < actual.length; i++) { assertThat(actual[i]).as("Element %d", i).matches(classes[i]); } } public void contain(Class<?>... classes) { contain(ImmutableSet.copyOf(classes)); } public void dontContain(Class<?>... classes) { assertThat(actualNames()).doesNotContainAnyElementsOf(JavaClass.namesOf(classes)); } public void contain(Iterable<Class<?>> classes) { List<String> expectedNames = JavaClass.namesOf(Lists.newArrayList(classes)); assertThat(actualNames()).as("actual classes").containsAll(expectedNames); } private Set<String> actualNames() { Set<String> actualNames = new HashSet<>(); for (JavaClass javaClass : actual) { actualNames.add(javaClass.getName()); } return actualNames; } } public static class JavaClassAssertion extends AbstractObjectAssert<JavaClassAssertion, JavaClass> { private static final Pattern ARRAY_PATTERN = Pattern.compile("(\\[+)(.*)"); private JavaClassAssertion(JavaClass javaClass) { super(javaClass, JavaClassAssertion.class); } public void matches(Class<?> clazz) { assertThat(actual.getName()).as("Name of " + actual) .isEqualTo(clazz.getName()); assertThat(actual.getSimpleName()).as("Simple name of " + actual) .isEqualTo(ensureArrayName(clazz.getSimpleName())); assertThat(actual.getPackage()).as("Package of " + actual) .isEqualTo(clazz.getPackage() != null ? clazz.getPackage().getName() : ""); assertThat(actual.getModifiers()).as("Modifiers of " + actual) .isEqualTo(JavaModifier.getModifiersForClass(clazz.getModifiers())); assertThat(propertiesOf(actual.getAnnotations())).as("Annotations of " + actual) .isEqualTo(propertiesOf(clazz.getAnnotations())); } private String ensureArrayName(String name) { String suffix = ""; Matcher matcher = ARRAY_PATTERN.matcher(name); if (matcher.matches()) { name = Type.getType(matcher.group(2)).getClassName(); suffix = Strings.repeat("[]", matcher.group(1).length()); } return name + suffix; } } public static class JavaClassListAssertion extends AbstractListAssert<JavaClassListAssertion, List<? extends JavaClass>, JavaClass> { private JavaClassListAssertion(JavaClassList javaClasses) { super(javaClasses, JavaClassListAssertion.class); } public void matches(Class<?>... classes) { assertThat(actual).as("JavaClasses").hasSize(classes.length); for (int i = 0; i < actual.size(); i++) { assertThat(actual.get(i)).as("Element %d", i).matches(classes[i]); } } } public static class JavaFieldAssertion extends AbstractObjectAssert<JavaFieldAssertion, JavaField> { private JavaFieldAssertion(JavaField javaField) { super(javaField, JavaFieldAssertion.class); } public void isEquivalentTo(Field field) { assertEquivalent(actual, field); assertThat(actual.getName()).isEqualTo(field.getName()); assertThat(actual.getFullName()).isEqualTo(getExpectedNameOf(field, field.getName())); assertThat(actual.getType()).matches(field.getType()); } } public static class JavaMethodAssertion extends AbstractObjectAssert<JavaMethodAssertion, JavaMethod> { private JavaMethodAssertion(JavaMethod javaMethod) { super(javaMethod, JavaMethodAssertion.class); } public void isEquivalentTo(Method method) { assertEquivalent(actual, method); assertThat(actual.getName()).isEqualTo(method.getName()); assertThat(actual.getFullName()).isEqualTo(getExpectedNameOf(method, method.getName())); assertThat(actual.getParameters()).matches(method.getParameterTypes()); assertThat(actual.getReturnType()).matches(method.getReturnType()); } } public static class JavaConstructorAssertion extends AbstractObjectAssert<JavaConstructorAssertion, JavaConstructor> { private JavaConstructorAssertion(JavaConstructor javaConstructor) { super(javaConstructor, JavaConstructorAssertion.class); } public void isEquivalentTo(Constructor<?> constructor) { assertEquivalent(actual, constructor); assertThat(actual.getName()).isEqualTo(CONSTRUCTOR_NAME); assertThat(actual.getFullName()).isEqualTo(getExpectedNameOf(constructor, CONSTRUCTOR_NAME)); assertThat(actual.getParameters()).matches(constructor.getParameterTypes()); assertThat(actual.getReturnType()).matches(void.class); } } public static class JavaEnumConstantAssertion extends AbstractObjectAssert<JavaEnumConstantAssertion, JavaEnumConstant> { private JavaEnumConstantAssertion(JavaEnumConstant enumConstant) { super(enumConstant, JavaEnumConstantAssertion.class); } public void isEquivalentTo(Enum<?> enumConstant) { assertThat(actual).as(JavaEnumConstant.class.getSimpleName()).isNotNull(); assertThat(actual.getDeclaringClass().getName()).isEqualTo(enumConstant.getDeclaringClass().getName()); assertThat(actual.name()).isEqualTo(enumConstant.name()); } } public static class JavaEnumConstantsAssertion extends AbstractObjectAssert<JavaEnumConstantsAssertion, JavaEnumConstant[]> { private JavaEnumConstantsAssertion(JavaEnumConstant[] enumConstants) { super(enumConstants, JavaEnumConstantsAssertion.class); } public void matches(Enum<?>... enumConstants) { assertThat((Object[]) actual).as("Enum constants").hasSize(enumConstants.length); for (int i = 0; i < actual.length; i++) { assertThat(actual[i]).as("Element %d", i).isEquivalentTo(enumConstants[i]); } } } private static <T extends Member & AnnotatedElement> void assertEquivalent(JavaMember javaMember, T member) { assertThat(javaMember.getOwner().reflect()).isEqualTo(member.getDeclaringClass()); assertModifiersMatch(javaMember, member); assertThat(propertiesOf(javaMember.getAnnotations())).isEqualTo(propertiesOf(member.getAnnotations())); } private static <T extends Member> void assertModifiersMatch(JavaMember javaMember, T member) { assertThat(javaMember.getModifiers().contains(ABSTRACT)) .as("member is abstract") .isEqualTo(Modifier.isAbstract(member.getModifiers())); assertThat(javaMember.getModifiers().contains(FINAL)) .as("member is final") .isEqualTo(Modifier.isFinal(member.getModifiers())); assertThat(javaMember.getModifiers().contains(NATIVE)) .as("member is native") .isEqualTo(Modifier.isNative(member.getModifiers())); assertThat(javaMember.getModifiers().contains(PRIVATE)) .as("member is private") .isEqualTo(Modifier.isPrivate(member.getModifiers())); assertThat(javaMember.getModifiers().contains(PROTECTED)) .as("member is protected") .isEqualTo(Modifier.isProtected(member.getModifiers())); assertThat(javaMember.getModifiers().contains(PUBLIC)) .as("member is public") .isEqualTo(Modifier.isPublic(member.getModifiers())); assertThat(javaMember.getModifiers().contains(STATIC)) .as("member is static") .isEqualTo(Modifier.isStatic(member.getModifiers())); assertThat(javaMember.getModifiers().contains(SYNCHRONIZED)) .as("member is synchronized") .isEqualTo(Modifier.isSynchronized(member.getModifiers())); assertThat(javaMember.getModifiers().contains(TRANSIENT)) .as("member is transient") .isEqualTo(Modifier.isTransient(member.getModifiers())); assertThat(javaMember.getModifiers().contains(VOLATILE)) .as("member is volatile") .isEqualTo(Modifier.isVolatile(member.getModifiers())); } private static <T extends Member & AnnotatedElement> String getExpectedNameOf(T member, String name) { String base = member.getDeclaringClass().getName() + "." + name; if (member instanceof Method) { return base + expectedParametersOf(((Method) member).getParameterTypes()); } if (member instanceof Constructor<?>) { return base + expectedParametersOf(((Constructor<?>) member).getParameterTypes()); } return base; } private static String expectedParametersOf(Class<?>[] parameterTypes) { return String.format("(%s)", formatMethodParameterTypeNames(namesOf(parameterTypes))); } private static Set<Map<String, Object>> propertiesOf(Set<JavaAnnotation> annotations) { List<Annotation> converted = new ArrayList<>(); for (JavaAnnotation annotation : annotations) { converted.add(annotation.as((Class) classForName(annotation.getType().getName()))); } return propertiesOf(converted.toArray(new Annotation[converted.size()])); } private static Set<Map<String, Object>> propertiesOf(Annotation[] annotations) { Set<Map<String, Object>> result = new HashSet<>(); for (Annotation annotation : annotations) { result.add(propertiesOf(annotation)); } return result; } private static Map<String, Object> propertiesOf(Annotation annotation) { Map<String, Object> props = new HashMap<>(); for (Method method : annotation.annotationType().getDeclaredMethods()) { Object returnValue = invoke(method, annotation); props.put(method.getName(), valueOf(returnValue)); } return props; } private static Object valueOf(Object value) { if (value instanceof Class) { return new SimpleTypeReference(((Class<?>) value).getName()); } if (value instanceof Class[]) { return SimpleTypeReference.allOf((Class<?>[]) value); } if (value instanceof Enum) { return new SimpleEnumConstantReference((Enum<?>) value); } if (value instanceof Enum[]) { return SimpleEnumConstantReference.allOf((Enum[]) value); } if (value instanceof Annotation) { return propertiesOf((Annotation) value); } if (value instanceof Annotation[]) { return propertiesOf((Annotation[]) value); } return value; } private static class SimpleTypeReference { private final String typeName; private SimpleTypeReference(String typeName) { this.typeName = typeName; } @Override public int hashCode() { return Objects.hash(typeName); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } final SimpleTypeReference other = (SimpleTypeReference) obj; return Objects.equals(this.typeName, other.typeName); } @Override public String toString() { return typeName; } static List<SimpleTypeReference> allOf(Class<?>[] value) { ImmutableList.Builder<SimpleTypeReference> result = ImmutableList.builder(); for (Class<?> c : value) { result.add(new SimpleTypeReference(c.getName())); } return result.build(); } } private static class SimpleEnumConstantReference { private final SimpleTypeReference type; private final String name; SimpleEnumConstantReference(Enum<?> value) { this.type = new SimpleTypeReference(value.getDeclaringClass().getName()); this.name = value.name(); } @Override public int hashCode() { return Objects.hash(type, name); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } final SimpleEnumConstantReference other = (SimpleEnumConstantReference) obj; return Objects.equals(this.type, other.type) && Objects.equals(this.name, other.name); } @Override public String toString() { return type + "." + name; } static List<SimpleEnumConstantReference> allOf(Enum[] values) { ImmutableList.Builder<SimpleEnumConstantReference> result = ImmutableList.builder(); for (Enum value : values) { result.add(new SimpleEnumConstantReference(value)); } return result.build(); } } public static class ConditionEventsAssert extends AbstractIterableAssert<ConditionEventsAssert, ConditionEvents, ConditionEvent> { ConditionEventsAssert(ConditionEvents actual) { super(actual, ConditionEventsAssert.class); } public void containViolations(String violation, String... additional) { assertThat(actual.containViolation()).as("Condition is violated").isTrue(); List<String> expected = concat(violation, additional); if (!sorted(messagesOf(actual.getViolating())).equals(sorted(expected))) { failWithMessage("Expected %s to contain only violations %s", actual, expected); } } public void containAllowed(String message, String... additional) { assertThat(actual.getAllowed().size()).as("Allowed events occurred").isGreaterThan(0); List<String> expected = concat(message, additional); if (!sorted(messagesOf(actual.getAllowed())).equals(sorted(expected))) { failWithMessage("Expected %s to contain only allowed events %s", actual, expected); } } private List<String> messagesOf(Collection<ConditionEvent> events) { final List<String> result = new ArrayList<>(); CollectsLines messages = new CollectsLines() { @Override public void add(String message) { result.add(message); } }; for (ConditionEvent event : events) { event.describeTo(messages); } return result; } private List<String> concat(String violation, String[] additional) { ArrayList<String> list = newArrayList(additional); list.add(0, violation); return list; } private List<String> sorted(Collection<String> collection) { ArrayList<String> result = new ArrayList<>(collection); Collections.sort(result); return result; } public void containNoViolation() { assertThat(actual.containViolation()).as("Condition is violated").isFalse(); assertThat(messagesOf(actual.getViolating())).as("Violating messages").isEmpty(); } public ConditionEventsAssert haveOneViolationMessageContaining(String... messageParts) { return haveOneViolationMessageContaining(ImmutableSet.copyOf(messageParts)); } public ConditionEventsAssert haveOneViolationMessageContaining(Set<String> messageParts) { assertThat(messagesOf(actual.getViolating())).as("Number of violations").hasSize(1); AbstractCharSequenceAssert<?, String> assertion = assertThat(getOnlyElement(messagesOf(actual.getViolating()))); for (String part : messageParts) { assertion.as("Violation message").contains(part); } return this; } } }