aboutsummaryrefslogtreecommitdiff
path: root/src/test/java/org/zwobble/mammoth/tests/DeepReflectionMatcher.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/test/java/org/zwobble/mammoth/tests/DeepReflectionMatcher.java')
-rw-r--r--src/test/java/org/zwobble/mammoth/tests/DeepReflectionMatcher.java136
1 files changed, 136 insertions, 0 deletions
diff --git a/src/test/java/org/zwobble/mammoth/tests/DeepReflectionMatcher.java b/src/test/java/org/zwobble/mammoth/tests/DeepReflectionMatcher.java
new file mode 100644
index 0000000..8ee0d8f
--- /dev/null
+++ b/src/test/java/org/zwobble/mammoth/tests/DeepReflectionMatcher.java
@@ -0,0 +1,136 @@
+package org.zwobble.mammoth.tests;
+
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterables;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.hamcrest.TypeSafeDiagnosingMatcher;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.List;
+
+import static com.google.common.collect.Iterables.filter;
+import static com.google.common.collect.Iterables.transform;
+import static java.util.Arrays.asList;
+
+public class DeepReflectionMatcher<T> extends TypeSafeDiagnosingMatcher<T> {
+ private final T expected;
+
+ public DeepReflectionMatcher(T expected) {
+ this.expected = expected;
+ }
+
+ @Override
+ protected boolean matchesSafely(T item, Description mismatchDescription) {
+ return matchesSafely("", expected, item, mismatchDescription);
+ }
+
+ private static <T> boolean matchesSafely(String path, T expected, T actual, Description mismatchDescription) {
+ if (expected instanceof List && actual instanceof List) {
+ return matchesList(path, (List)expected, (List)actual, mismatchDescription);
+ }
+
+ if (!expected.getClass().equals(actual.getClass())) {
+ mismatchDescription.appendText("was " + actual.getClass().getName());
+ return false;
+ }
+
+ if (expected instanceof String) {
+ return matchesString(path, expected, actual, mismatchDescription);
+ }
+
+ for (Field field : fields(expected.getClass())) {
+ if (!matchesSafely(path + "." + field.getName(), readField(expected, field), readField(actual, field), mismatchDescription)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static <T> boolean matchesList(String path, List<?> expected, List<?> actual, Description mismatchDescription) {
+ if (actual.size() > expected.size()) {
+ appendPath(mismatchDescription, path);
+ mismatchDescription.appendText("extra elements:" +
+ indentedList(transform(Iterables.skip(actual, expected.size()), DeepReflectionMatcher::generateDescriptionOfValue)));
+ return false;
+ }
+
+ if (actual.size() < expected.size()) {
+ appendPath(mismatchDescription, path);
+ mismatchDescription.appendText("missing elements:" +
+ indentedList(transform(Iterables.skip(expected, actual.size()), DeepReflectionMatcher::generateDescriptionOfValue)));
+ return false;
+ }
+
+ for (int index = 0; index < expected.size(); index++) {
+ if (!matchesSafely(path + "[" + index + "]", expected.get(index), actual.get(index), mismatchDescription)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ private static <T> boolean matchesString(String path, T expected, T actual, Description mismatchDescription) {
+ Matcher<Object> matcher = Matchers.equalTo(expected);
+ if (!matcher.matches(actual)) {
+ appendPath(mismatchDescription, path);
+ matcher.describeMismatch(actual, mismatchDescription);
+ return false;
+ } else {
+ return true;
+ }
+ }
+
+ private static void appendPath(Description mismatchDescription, String path) {
+ mismatchDescription.appendText(path + ": ");
+ }
+
+ @Override
+ public void describeTo(Description description) {
+ description.appendText(generateDescriptionOfValue(expected));
+ }
+
+ private static String generateDescriptionOfValue(Object value) {
+ if (value instanceof String) {
+ return value.toString();
+ } else if (value instanceof List) {
+ List<?> list = (List)value;
+ return "[" + indentedList(transform(list, DeepReflectionMatcher::generateDescriptionOfValue)) + "]";
+ } else {
+ Class<?> clazz = value.getClass();
+ List<Field> fields = fields(clazz);
+ Iterable<String> fieldStrings = transform(
+ fields,
+ field -> field.getName() + "=" + generateDescriptionOfValue(readField(value, field)));
+ return String.format("%s(%s)", clazz.getSimpleName(), indentedList(fieldStrings));
+ }
+ }
+
+ private static String indentedList(Iterable<String> values) {
+ return Joiner.on(",").join(transform(values, value -> "\n " + indent(value)));
+ }
+
+ private static String indent(String value) {
+ return value.replaceAll("\n", "\n ");
+ }
+
+ private static ImmutableList<Field> fields(Class<?> clazz) {
+ return ImmutableList.copyOf(filter(
+ asList(clazz.getDeclaredFields()),
+ field -> !Modifier.isStatic(field.getModifiers())));
+ }
+
+ private static Object readField(Object obj, Field field) {
+ try {
+ field.setAccessible(true);
+ return field.get(obj);
+ } catch (IllegalAccessException exception) {
+ throw new RuntimeException(exception);
+ }
+ }
+}