AssertionHelper.java

package fr.sii.ogham.testing.assertion.util;

import org.hamcrest.Description;
import org.hamcrest.Matcher;
import org.hamcrest.MatcherAssert;
import org.hamcrest.StringDescription;
import org.junit.ComparisonFailure;

import fr.sii.ogham.testing.assertion.context.Context;
import fr.sii.ogham.testing.assertion.hamcrest.ComparisonAwareMatcher;
import fr.sii.ogham.testing.assertion.hamcrest.CustomDescriptionProvider;
import fr.sii.ogham.testing.assertion.hamcrest.CustomReason;
import fr.sii.ogham.testing.assertion.hamcrest.DecoratorMatcher;
import fr.sii.ogham.testing.assertion.hamcrest.ExpectedValueProvider;
import fr.sii.ogham.testing.assertion.hamcrest.OverrideDescription;

/**
 * Utility class for Ogham assertions.
 * 
 * @author Aurélien Baudet
 *
 */
public final class AssertionHelper {

	/**
	 * Copy of {@link MatcherAssert#assertThat(Object, Matcher)} with the
	 * following additions:
	 * <ul>
	 * <li>If the matcher can provide expected value, a
	 * {@link ComparisonFailure} exception is thrown instead of
	 * {@link AssertionError} in order to display differences between expected
	 * string and actual string in the IDE.</li>
	 * <li>If the matcher is a {@link CustomReason} matcher and no reason is
	 * provided, the reason of the matcher is used to provide more information
	 * about the context (which message has failed for example)</li>
	 * </ul>
	 * 
	 * @param actual
	 *            the actual value
	 * @param matcher
	 *            the matcher to apply
	 * @param <T>
	 *            the type used for the matcher
	 */
	public static <T> void assertThat(T actual, Matcher<? super T> matcher) {
		assertThat("", actual, matcher);
	}

	/**
	 * Copy of {@link MatcherAssert#assertThat(String, Object, Matcher)} with
	 * the following additions:
	 * <ul>
	 * <li>If the matcher can provide expected value, a
	 * {@link ComparisonFailure} exception is thrown instead of
	 * {@link AssertionError} in order to display differences between expected
	 * string and actual string in the IDE.</li>
	 * <li>If the matcher is a {@link CustomReason} matcher and no reason is
	 * provided, the reason of the matcher is used to provide more information
	 * about the context (which message has failed for example)</li>
	 * </ul>
	 * 
	 * @param reason
	 *            the reason
	 * @param actual
	 *            the actual value
	 * @param matcher
	 *            the matcher to apply
	 * @param <T>
	 *            the type used for the matcher
	 */
	public static <T> void assertThat(String reason, T actual, Matcher<? super T> matcher) {
		if (!matcher.matches(actual)) {
			Description description = getDescription(reason, actual, matcher);

			if (hasExpectedValue(matcher)) {
				ExpectedValueProvider<T> comparable = getComparable(matcher);
				throw new ComparisonFailure(description.toString(), String.valueOf(comparable == null ? null : comparable.getExpectedValue()), String.valueOf(actual));
			} else {
				throw new AssertionError(description.toString());
			}
		}
	}

	/**
	 * Ogham helper for keeping context information when using fluent
	 * assertions.
	 * 
	 * @param reasonTemplate
	 *            the template for the reason
	 * @param context
	 *            the evaluation context
	 * @param delegate
	 *            the matcher to decorate
	 * @param <T>
	 *            the type used for the matcher
	 * @return the matcher
	 */
	public static <T> Matcher<T> usingContext(String reasonTemplate, Context context, Matcher<T> delegate) {
		return new CustomReason<>(context.evaluate(reasonTemplate), delegate);
	}

	/**
	 * Ogham helper for overriding default description.
	 * 
	 * @param description
	 *            the description to display
	 * @param delegate
	 *            the matcher to decorate
	 * @param <T>
	 *            the type used for the matcher
	 * @return the matcher
	 */
	public static <T> Matcher<T> overrideDescription(String description, Matcher<T> delegate) {
		return new OverrideDescription<>(description, delegate);
	}
	
	@SuppressWarnings("unchecked")
	private static <T> boolean hasExpectedValue(Matcher<? super T> matcher) {
		if (matcher instanceof ExpectedValueProvider) {
			return true;
		}
		if (matcher instanceof DecoratorMatcher) {
			return hasExpectedValue(((DecoratorMatcher<T>) matcher).getDecoree());
		}
		return false;
	}

	@SuppressWarnings("unchecked")
	private static <T> ExpectedValueProvider<T> getComparable(Matcher<? super T> matcher) {
		if (matcher instanceof ExpectedValueProvider) {
			return (ExpectedValueProvider<T>) matcher;
		}
		if (matcher instanceof DecoratorMatcher) {
			return getComparable(((DecoratorMatcher<T>) matcher).getDecoree());
		}
		return null;
	}

	private static <T> Description getDescription(String reason, T actual, Matcher<? super T> matcher) {
		String additionalText = null;
		ComparisonAwareMatcher cam = getComparisonAwareMatcher(matcher);
		if (cam != null) {
			additionalText = cam.comparisonMessage();
		}
		return getDescription(reason, actual, matcher, additionalText);
	}

	@SuppressWarnings("unchecked")
	private static <T> ComparisonAwareMatcher getComparisonAwareMatcher(Matcher<? super T> matcher) {
		if (matcher instanceof ComparisonAwareMatcher) {
			return (ComparisonAwareMatcher) matcher;
		}
		if (matcher instanceof DecoratorMatcher) {
			return getComparisonAwareMatcher(((DecoratorMatcher<T>) matcher).getDecoree());
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	private static <T> Description getDescription(String reason, T actual, Matcher<? super T> matcher, String additionalText) {
		if (matcher instanceof CustomDescriptionProvider) {
			return ((CustomDescriptionProvider<T>) matcher).describe(reason, actual, additionalText);
		}
		// @formatter:off
		Description description = new StringDescription();
		description.appendText(getReason(reason, matcher))
					.appendText(additionalText==null ? "" : ("\n"+additionalText))
					.appendText("\nExpected: ")
					.appendDescriptionOf(matcher)
					.appendText("\n     but: ");
		matcher.describeMismatch(actual, description);
		// @formatter:on
		return description;
	}

	private static <T> String getReason(String reason, Matcher<? super T> matcher) {
		if (reason != null && !reason.isEmpty()) {
			return reason;
		}
		if (matcher instanceof CustomReason) {
			return ((CustomReason<?>) matcher).getReason();
		}
		return "";
	}

	private AssertionHelper() {
		super();
	}

}