package org.testory.common; import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Objects.deepEquals; import static org.testory.common.Checks.checkArgument; import static org.testory.common.Checks.checkNotNull; import static org.testory.common.Classes.setAccessible; import static org.testory.common.Formatter.formatter; import static org.testory.common.Throwables.gently; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; public class Matchers { private static final Formatter formatter = formatter(); public static final Matcher anything = new Matcher() { public boolean matches(Object item) { return true; } public String toString() { return "anything"; } }; public static Matcher same(@Nullable final Object object) { return new Matcher() { public boolean matches(Object item) { return object == item; } public String toString() { return format("same(%s)", formatter.format(object)); } }; } public static Matcher equalDeep(@Nullable final Object object) { return new Matcher() { public boolean matches(Object item) { return deepEquals(object, item); } public String toString() { return format("equalDeep(%s)", formatter.format(object)); } }; } public static Matcher arrayOf(List<Matcher> elementsMatchers) { for (Matcher matcher : elementsMatchers) { checkNotNull(matcher); } final List<Matcher> matchers = new ArrayList<>(elementsMatchers); return new Matcher() { public boolean matches(Object item) { return item != null && item.getClass().isArray() && matchers.size() == Array.getLength(item) && matchesElements(item); } private boolean matchesElements(Object item) { for (int i = 0; i < matchers.size(); i++) { if (!matchers.get(i).matches(Array.get(item, i))) { return false; } } return true; } public String toString() { return format("arrayOf(%s)", formatter.formatSequence(matchers)); } }; } public static Matcher listOf(List<Matcher> elementsMatchers) { for (Matcher matcher : elementsMatchers) { checkNotNull(matcher); } final List<Matcher> matchers = new ArrayList<>(elementsMatchers); return new Matcher() { public boolean matches(Object item) { return item instanceof List<?> && matchers.size() == ((List<?>) item).size() && matchesElements((List<?>) item); } private boolean matchesElements(List<?> list) { for (int i = 0; i < matchers.size(); i++) { if (!matchers.get(i).matches(list.get(i))) { return false; } } return true; } public String toString() { return format("listOf(%s)", formatter.formatSequence(matchers)); } }; } public static boolean isMatcher(Object matcher) { return findMatchesMethod(matcher.getClass()).isPresent(); } public static Matcher asMatcher(Object matcher) { Optional<Method> matchesMethod = findMatchesMethod(matcher.getClass()); checkArgument(matchesMethod.isPresent()); Optional<Method> diagnoseMethod = findDiagnoseMethod(matcher.getClass()); return diagnoseMethod.isPresent() ? newDiagnosticMatcher(matcher, matchesMethod.get(), diagnoseMethod.get()) : newMatcher(matcher, matchesMethod.get()); } private static Matcher newMatcher(final Object dynamicMatcher, final Method matchesMethod) { return new Matcher() { public boolean matches(Object item) { try { setAccessible(matchesMethod); return (Boolean) matchesMethod.invoke(dynamicMatcher, item); } catch (InvocationTargetException e) { throw gently(e.getCause()); } catch (ReflectiveOperationException e) { throw new LinkageError(null, e); } } public String toString() { return dynamicMatcher.toString(); } }; } private static DiagnosticMatcher newDiagnosticMatcher( final Object dynamicMatcher, Method matchesMethod, final Method diagnoseMethod) { final Matcher matcher = newMatcher(dynamicMatcher, matchesMethod); return new DiagnosticMatcher() { public boolean matches(Object item) { return matcher.matches(item); } public String toString() { return matcher.toString(); } public String diagnose(@Nullable Object item) { try { ClassLoader loader = dynamicMatcher.getClass().getClassLoader(); Class<?> descriptionClass = Class.forName("org.hamcrest.StringDescription", true, loader); Object description = descriptionClass.newInstance(); setAccessible(diagnoseMethod); diagnoseMethod.invoke(dynamicMatcher, item, description); return description.toString(); } catch (InvocationTargetException e) { throw gently(e.getCause()); } catch (ReflectiveOperationException e) { throw new LinkageError(null, e); } } }; } private static Optional<Method> findMatchesMethod(Class<?> type) { for (String name : asList("matches", "apply")) { try { Method method = type.getMethod(name, Object.class); Class<?> returnType = method.getReturnType(); if ((returnType == boolean.class || returnType == Boolean.class) && method.getExceptionTypes().length == 0) { return Optional.of(method); } } catch (NoSuchMethodException e) {} } return Optional.empty(); } private static Optional<Method> findDiagnoseMethod(Class<?> type) { for (Method method : type.getMethods()) { Class<?>[] parameters = method.getParameterTypes(); if (method.getName().equals("describeMismatch") && parameters.length == 2 && parameters[0] == Object.class && parameters[1].getName().equals("org.hamcrest.Description") && method.getExceptionTypes().length == 0) { return Optional.of(method); } } return Optional.empty(); } }