All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.amygdalum.testrecorder.runtime.GenericMatcher Maven / Gradle / Ivy

The newest version!
package net.amygdalum.testrecorder.runtime;

import static java.util.stream.Collectors.toList;
import static net.amygdalum.testrecorder.runtime.GenericComparatorResult.MATCH;
import static net.amygdalum.testrecorder.runtime.GenericComparatorResult.MISMATCH;
import static net.amygdalum.testrecorder.runtime.GenericComparatorResult.NOT_APPLYING;
import static net.amygdalum.testrecorder.util.Reflections.getValue;
import static net.amygdalum.testrecorder.util.Types.allFields;
import static org.hamcrest.CoreMatchers.instanceOf;

import java.lang.reflect.Field;
import java.util.List;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.SelfDescribing;
import org.hamcrest.StringDescription;
import org.hamcrest.TypeSafeMatcher;

import net.amygdalum.testrecorder.util.WorkSet;

public class GenericMatcher extends GenericObject {

	public  Matcher matching(Class clazz) {
		return new InternalsMatcher(clazz);
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public Matcher matching(Wrapped wrapped) {
		return (Matcher) new InternalsMatcher(wrapped.getWrappedClass());
	}

	public  Matcher matching(Class clazz, Class to) {
		return new CastingMatcher<>(to, new InternalsMatcher(clazz));
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public  Matcher matching(Wrapped clazz, Class to) {
		return (Matcher) new CastingMatcher(to, new InternalsMatcher(clazz.getWrappedClass()));
	}

	public List mismatchesWith(String root, Object o) {
		WorkSet remainder = new WorkSet<>();
		for (Field field : getGenericFields(o.getClass())) {
			GenericComparison comparison = getQualifiedField(o.getClass(), field.getName())
				.map(qfield -> GenericComparison.from(root, field, this, qfield, o))
				.orElseGet(() -> GenericComparison.from(root, field.getName(), this, o));

			remainder.add(comparison);
		}
		GenericComparison.compare(remainder, GenericMatcher::matching);
		return remainder.getDone().stream()
			.filter(done -> done.isMismatch())
			.collect(toList());
	}

	private static GenericComparatorResult matching(GenericComparison comparison, WorkSet todo) {
		Object left = comparison.getLeft();
		Object right = comparison.getRight();
		if (left instanceof RecursiveMatcher && right != null) {
			RecursiveMatcher matcher = (RecursiveMatcher) left;
			todo.addAll(matcher.mismatchesWith(comparison.getRoot(), right));
		}
		if (left instanceof Matcher) {
			Matcher matcher = (Matcher) left;
			if (matcher.matches(right)) {
				return MATCH;
			} else {
				return MISMATCH;
			}
		}
		return NOT_APPLYING;
	}

	public static  Matcher recursive(Class clazz) {
		return instanceOf(clazz);
	}

	public static Matcher recursive(Wrapped wrapped) {
		return instanceOf(wrapped.getWrappedClass());
	}

	private class InternalsMatcher extends TypeSafeMatcher implements RecursiveMatcher {

		private Class clazz;

		InternalsMatcher(Class clazz) {
			this.clazz = clazz;
		}

		@Override
		public void describeTo(Description description) {
			description.appendText(clazz.getName()).appendText(" {");
			for (Field field : allFields(clazz)) {
				describeField(description, field, GenericMatcher.this);
			}
			description.appendText("\n}");
		}

		@Override
		public List mismatchesWith(String root, Object item) {
			return GenericMatcher.this.mismatchesWith(root, item);
		}

		@Override
		protected boolean matchesSafely(T item) {
			Class itemClass = item.getClass();
			if (isSynthetic(itemClass)) {
				if (!clazz.isAssignableFrom(itemClass)) {
					return false;
				}
			} else if (clazz != itemClass) {
				return false;
			}
			
			List mismatches = mismatchesWith(null, item);
			return mismatches.isEmpty();
		}

		private boolean isSynthetic(Class itemClass) {
			return itemClass.isSynthetic()
				|| itemClass.getSimpleName().contains("$");
		}

		@Override
		protected void describeMismatchSafely(T item, Description mismatchDescription) {
			List mismatches = mismatchesWith(null, item);
			if (!mismatches.isEmpty()) {
				mismatchDescription.appendText(item.getClass().getName()).appendText(" {");
				for (Field field : allFields(item.getClass())) {
					describeField(mismatchDescription, field, item);
				}
				mismatchDescription.appendText("\n}");
				mismatchDescription.appendText("\nfound mismatches at:");
				for (GenericComparison mismatch : mismatches) {
					if (!(mismatch.getLeft() instanceof RecursiveMatcher)) {
						describeMismatch(mismatchDescription, mismatch);
					}
				}
			}
		}

		private void describeField(Description description, Field field, Object object) {
			try {
				description.appendText("\n\t")
					.appendText(field.getType().getSimpleName()).appendText(" ")
					.appendText(field.getName()).appendText(": ");
				Object value = getValue(field.getName(), object);
				describe(description, value);
				description.appendText(";");
			} catch (ReflectiveOperationException e) {
				description.appendText("\n\t")
					.appendValue(field.getType()).appendText(" ")
					.appendValue(field.getName()).appendText(":");
			}
		}

		private void describeMismatch(Description description, GenericComparison mismatch) {
			description.appendText("\n\t")
				.appendText(mismatch.getRoot()).appendText(": ");
			if (mismatch.getLeft() instanceof Matcher) {
				((Matcher) mismatch.getLeft()).describeMismatch(mismatch.getRight(), description);
			} else {
				describe(description, mismatch.getLeft());
				description.appendText(" != ");
				describe(description, mismatch.getRight());
			}
		}

		private void describe(Description description, Object value) {
			StringDescription subdescription = new StringDescription();
			if (value instanceof SelfDescribing) {
				subdescription.appendDescriptionOf((SelfDescribing) value);
			} else {
				subdescription.appendValue(value);
			}
			description.appendText(subdescription.toString().replace("\n", "\n\t"));
		}

	}

	private class CastingMatcher extends TypeSafeMatcher implements RecursiveMatcher {

		private Class clazz;
		private Matcher matcher;

		CastingMatcher(Class clazz, Matcher matcher) {
			this.clazz = clazz;
			this.matcher = matcher;
		}

		@Override
		public void describeTo(Description description) {
			description.appendDescriptionOf(matcher);
		}

		@Override
		public List mismatchesWith(String root, Object item) {
			return GenericMatcher.this.mismatchesWith(root, item);
		}

		@Override
		protected boolean matchesSafely(S item) {
			if (!clazz.isInstance(item)) {
				return false;
			}
			if (!matcher.matches(item)) {
				return false;
			}
			return true;
		}

	}

}