Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/3165 recursive comparison assert #3179

Open
wants to merge 3 commits into
base: 3.x
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
*/
package org.assertj.core.api;

import static java.util.Objects.requireNonNull;
import static org.assertj.core.error.ShouldBeEqualByComparingFieldByFieldRecursively.shouldBeEqualByComparingFieldByFieldRecursively;
import static org.assertj.core.error.ShouldNotBeEqualComparingFieldByFieldRecursively.shouldNotBeEqualComparingFieldByFieldRecursively;

Expand All @@ -32,6 +33,7 @@
import org.assertj.core.api.recursive.comparison.RecursiveComparisonIntrospectionStrategy;
import org.assertj.core.internal.TypeComparators;
import org.assertj.core.util.CheckReturnValue;
import org.assertj.core.util.DualClass;
import org.assertj.core.util.introspection.IntrospectionError;

public class RecursiveComparisonAssert<SELF extends RecursiveComparisonAssert<SELF>> extends AbstractAssert<SELF, Object> {
Expand Down Expand Up @@ -1529,7 +1531,8 @@ public <T> SELF withComparatorForType(Comparator<? super T> comparator, Class<T>
* Allows to register a {@link BiPredicate} to compare the fields with the given type.
* A typical usage is to compare double/float fields with a given precision.
* <p>
* BiPredicates registered with this method have less precedence than the one registered with {@link #withEqualsForFields(BiPredicate, String...) withEqualsForFields(BiPredicate, String...)}
* BiPredicates registered with this method have less precedence than the one registered with {@link #withEqualsForTypes(BiPredicate, Class, Class) withEqualsForTypes(BiPredicate, Class, Class)},
* {@link #withEqualsForFields(BiPredicate, String...) withEqualsForFields(BiPredicate, String...)}
* or comparators registered with {@link #withComparatorForFields(Comparator, String...) withComparatorForFields(Comparator, String...)}.
* <p>
* Note that registering a {@link BiPredicate} for a given type will override the previously registered BiPredicate/Comparator (if any).
Expand Down Expand Up @@ -1569,6 +1572,53 @@ public <T> SELF withEqualsForType(BiPredicate<? super T, ? super T> equals, Clas
return myself;
}

/**
* Allows to register a {@link BiPredicate} to compare the fields with the given types.
* A typical usage is to compare fields belonging to different types.
* <p>
* BiPredicates registered with this method have less precedence than the one registered with {@link #withEqualsForFields(BiPredicate, String...) withEqualsForFields(BiPredicate, String...)}
* or comparators registered with {@link #withComparatorForFields(Comparator, String...) withComparatorForFields(Comparator, String...)}.
* <p>
* Note that registering a {@link BiPredicate} for a given type will override the previously registered BiPredicate/Comparator (if any).
* <p>
* Example:
* <pre><code class='java'> public class TolkienCharacter {
* LocalDate birthday;
* }
* public class TolkienCharacterDto {
* String birthday;
* }
*
* TolkienCharacter frodo = new TolkienCharacter(LocalDate.of(2968, Month.SEPTEMBER, 22));
* TolkienCharacterDto frodoDto = new TolkienCharacterDto("2968-09-22");
* TolkienCharacterDto bilboDto = new TolkienCharacterDto("2890-09-22");
*
* BiPredicate&lt;LocalDate, String&gt; sameDate = (d, s) -&gt; LocalDate.parse(s).equals(d);
*
* // assertion succeeds
* assertThat(frodo).usingRecursiveComparison()
* .withEqualsForTypes(sameDate, LocalDate.class, String.class)
* .isEqualTo(frodoDto);
*
* // assertion fails
* assertThat(frodo).usingRecursiveComparison()
* .withEqualsForTypes(sameDate, LocalDate.class, String.class)
* .isEqualTo(bilboDto);</code></pre>
amodolo marked this conversation as resolved.
Show resolved Hide resolved
*
* @param <T> the left element's class type to register a BiPredicate for
* @param <U> the right element's class type to register a BiPredicate for
* @param equals the {@link BiPredicate} to use to compare the given fields
* @param type the type of the left element to be compared with the given comparator.
* @param otherType the type of the right element to be compared with the given comparator.
*
* @return this {@link RecursiveComparisonAssert} to chain other methods.
* @throws NullPointerException if the given BiPredicate is null.
*/
public <T, U> SELF withEqualsForTypes(BiPredicate<? super T, ? super U> equals, Class<T> type, Class<U> otherType) {
recursiveComparisonConfiguration.registerEqualsForTypes(equals, type, otherType);
return myself;
}

/**
* Overrides an error message which would be shown when differences in the given fields while comparison occurred
* with the giving error message.
Expand Down Expand Up @@ -1751,9 +1801,9 @@ SELF withTypeComparators(TypeComparators typeComparators) {
return myself;
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private void registerComparatorForType(Entry<Class<?>, Comparator<?>> entry) {
withComparatorForType((Comparator) entry.getValue(), entry.getKey());
@SuppressWarnings({ "unchecked" })
private void registerComparatorForType(Entry<DualClass<?, ?>, Comparator<?>> entry) {
withEqualsForTypes(toBiPredicate(entry.getValue()), entry.getKey().actual(), entry.getKey().expected());
}

/**
Expand All @@ -1768,4 +1818,10 @@ public RecursiveComparisonConfiguration getRecursiveComparisonConfiguration() {
private List<ComparisonDifference> determineDifferencesWith(Object expected) {
return recursiveComparisonDifferenceCalculator.determineDifferences(actual, expected, recursiveComparisonConfiguration);
}

@SuppressWarnings({ "unchecked", "rawtypes" })
private BiPredicate toBiPredicate(Comparator comparator) {
requireNonNull(comparator, "Expecting a non null Comparator");
return (o, o2) -> comparator.compare(o, o2) == 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import org.assertj.core.internal.TypeComparators;
import org.assertj.core.internal.TypeMessages;
import org.assertj.core.presentation.Representation;
import org.assertj.core.util.DualClass;
import org.assertj.core.util.VisibleForTesting;

public class RecursiveComparisonConfiguration extends AbstractRecursiveOperationConfiguration {
Expand Down Expand Up @@ -137,15 +138,23 @@ public FieldComparators getFieldComparators() {
}

public boolean hasComparatorForType(Class<?> keyType) {
return typeComparators.hasComparatorForType(keyType);
return hasComparatorForDualTypes(keyType, null);
}

public boolean hasComparatorForDualTypes(Class<?> keyType, Class<?> otherKeyType) {
return typeComparators.hasComparatorForDualTypes(keyType, otherKeyType);
}

public boolean hasCustomComparators() {
return !typeComparators.isEmpty() || !fieldComparators.isEmpty();
}

public Comparator<?> getComparatorForType(Class<?> fieldType) {
return typeComparators.getComparatorForType(fieldType);
return getComparatorForType(fieldType, null);
}

public Comparator<?> getComparatorForType(Class<?> fieldType, Class<?> otherFieldType) {
return typeComparators.getComparatorForDualTypes(fieldType, otherFieldType);
}

public boolean hasCustomMessageForType(Class<?> fieldType) {
Expand All @@ -160,7 +169,7 @@ public TypeComparators getTypeComparators() {
return typeComparators;
}

Stream<Entry<Class<?>, Comparator<?>>> comparatorByTypes() {
Stream<Entry<DualClass<?, ?>, Comparator<?>>> comparatorByTypes() {
return typeComparators.comparatorByTypes();
}

Expand Down Expand Up @@ -423,7 +432,7 @@ public <T> void registerComparatorForType(Comparator<? super T> comparator, Clas
* Registers the given {@link BiPredicate} to compare the fields with the given type.
* <p>
* BiPredicates specified with this method have less precedence than the ones registered with
* {@link #registerEqualsForFields(BiPredicate, String...)}
* {@link #registerEqualsForTypes(BiPredicate, Class, Class)}, {@link #registerEqualsForFields(BiPredicate, String...)}
* or comparators registered with {@link #registerComparatorForFields(Comparator, String...)}.
* <p>
* Note that registering a {@link BiPredicate} for a given type will override the previously registered BiPredicate/Comparator (if any).
Expand All @@ -441,6 +450,30 @@ public <T> void registerEqualsForType(BiPredicate<? super T, ? super T> equals,
registerComparatorForType(toComparator(equals), type);
}

/**
* Registers the given {@link BiPredicate} to compare the fields with the given types.
* <p>
* BiPredicates specified with this method have less precedence than the ones registered with
* {@link #registerEqualsForFields(BiPredicate, String...)}
* or comparators registered with {@link #registerComparatorForFields(Comparator, String...)}.
* <p>
* Note that registering a {@link BiPredicate} for a given types will override the previously registered BiPredicate/Comparator (if any).
* <p>
* See {@link RecursiveComparisonAssert#withEqualsForTypes(BiPredicate, Class, Class)} for examples.
*
* @param <T> the class of the left element to register a comparator for
* @param <U> the class of the right element to register a comparator for
* @param equals the equals implementation to compare the given type
* @param type the type of the left element to be compared with the given equals implementation.
* @param otherType the type of right left element to be compared with the given equals implementation.
* @throws NullPointerException if the given BiPredicate is null.
*/
@SuppressWarnings("unchecked")
public <T, U> void registerEqualsForTypes(BiPredicate<? super T, ? super U> equals, Class<T> type, Class<U> otherType) {
requireNonNull(equals, "Expecting a non null BiPredicate");
typeComparators.registerComparator(type, otherType, toComparator(equals));
}

/**
* Registers the given {@link Comparator} to compare the fields at the given locations.
* <p>
Expand Down Expand Up @@ -764,8 +797,10 @@ boolean hasCustomComparator(DualValue dualValue) {
if (hasComparatorForField(fieldName)) return true;
if (dualValue.actual == null && dualValue.expected == null) return false;
// best effort assuming actual and expected have the same type (not 100% true as we can compare object of different types)
Class<?> valueType = dualValue.actual != null ? dualValue.actual.getClass() : dualValue.expected.getClass();
return hasComparatorForType(valueType);
Class<?> actualType = dualValue.actual != null ? dualValue.actual.getClass() : dualValue.expected.getClass();
Class<?> expectedType = dualValue.expected != null ? dualValue.expected.getClass() : null;
if (hasComparatorForDualTypes(actualType, expectedType)) return true;
else return hasComparatorForType(actualType);
}

boolean shouldIgnoreOverriddenEqualsOf(DualValue dualValue) {
Expand Down Expand Up @@ -971,8 +1006,13 @@ private void describeComparatorForTypes(StringBuilder description) {
.forEach(description::append);
}

private String formatRegisteredComparatorByType(Entry<Class<?>, Comparator<?>> next) {
return format("%s %s -> %s%n", INDENT_LEVEL_2, next.getKey().getName(), next.getValue());
private String formatRegisteredComparatorByType(Entry<DualClass<?, ?>, Comparator<?>> next) {
if (next.getKey().expected() == null) {
return format("%s %s -> %s%n", INDENT_LEVEL_2, next.getKey().actual().getName(), next.getValue());
} else {
return format("%s [%s - %s] -> %s%n", INDENT_LEVEL_2, next.getKey().actual().getName(), next.getKey().expected().getName(),
next.getValue());
}
}

private void describeRegisteredComparatorForFields(StringBuilder description) {
Expand Down Expand Up @@ -1048,7 +1088,7 @@ private void describeRegisteredErrorMessagesForTypes(StringBuilder description)

private void describeErrorMessagesForType(StringBuilder description) {
String types = typeMessages.messageByTypes()
.map(it -> it.getKey().getName())
.map(this::formatErrorMessageForType)
.collect(joining(DEFAULT_DELIMITER));
description.append(format("%s %s%n", INDENT_LEVEL_2, types));
}
Expand Down Expand Up @@ -1499,4 +1539,10 @@ private static Comparator toComparator(BiPredicate equals) {
return (o1, o2) -> equals.test(o1, o2) ? 0 : 1;
}

private String formatErrorMessageForType(Map.Entry<DualClass<?, ?>, String> entry) {
return entry.getKey().expected() == null
? entry.getKey().actual().getName()
: format("[%s - %s]", entry.getKey().actual().getName(), entry.getKey().expected().getName());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -839,8 +839,10 @@ private static boolean areDualValueEqual(DualValue dualValue,
Comparator fieldComparator = recursiveComparisonConfiguration.getComparatorForField(fieldName);
if (fieldComparator != null) return areEqualUsingComparator(actualFieldValue, expectedFieldValue, fieldComparator, fieldName);
// check if a type comparators exist for the field type
Class fieldType = actualFieldValue != null ? actualFieldValue.getClass() : expectedFieldValue.getClass();
Comparator typeComparator = recursiveComparisonConfiguration.getComparatorForType(fieldType);
Class actualFieldType = actualFieldValue != null ? actualFieldValue.getClass() : expectedFieldValue.getClass();
Class expectedFieldType = expectedFieldValue != null ? expectedFieldValue.getClass() : null;
Comparator typeComparator = recursiveComparisonConfiguration.getComparatorForType(actualFieldType, expectedFieldType);
if (typeComparator == null) typeComparator = recursiveComparisonConfiguration.getComparatorForType(actualFieldType);
if (typeComparator != null) return areEqualUsingComparator(actualFieldValue, expectedFieldValue, typeComparator, fieldName);
// default comparison using equals
return deepEquals(actualFieldValue, expectedFieldValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
import java.util.Map.Entry;
import java.util.stream.Stream;

import org.assertj.core.util.DoubleComparator;
import org.assertj.core.util.FloatComparator;
import org.assertj.core.util.PathNaturalOrderComparator;
import org.assertj.core.util.*;

import static java.util.Objects.requireNonNull;

/**
* An internal holder of the comparators for type. It is used to store comparators for registered classes.
Expand Down Expand Up @@ -57,7 +57,27 @@ public static TypeComparators defaultTypeComparators() {
* @return the most relevant comparator, or {@code null} if no comparator could be found
*/
public Comparator<?> getComparatorForType(Class<?> clazz) {
return super.get(clazz);
return getComparatorForDualTypes(clazz, null);
}

/**
* This method returns the most relevant comparator for the given class pair. The most relevant comparator is the
* comparator which is registered for the class pair that is closest in the inheritance chain of the given {@code clazz} and {@code otherClazz}.
* The order of checks is the following:
* 1. If there is a registered comparator for {@code clazz} and {@code otherClazz} then this one is used
* 2. We check if there is a registered comparator for a superclass of {@code clazz} and {@code otherClazz}
* 3. We check if there is a registered comparator for {@code clazz} and a superclass of {@code otherClazz}
* 4. We check if there is a registered comparator for a superclass of {@code clazz} and a superclass of {@code otherClazz}
* 5. We check if there is a registered comparator for an interface of {@code clazz} and {@code otherClazz}
* 6. We check if there is a registered comparator for {@code clazz} and an interface of {@code otherClazz}
* 7. We check if there is a registered comparator for an interface of {@code clazz} and an interface of {@code otherClazz}
*
* @param clazz the class of the left element for which to find a comparator
* @param otherClazz the class of the right element for which to find a comparator
* @return the most relevant comparator, or {@code null} if no comparator could be found
*/
public Comparator<?> getComparatorForDualTypes(Class<?> clazz, Class<?> otherClazz) {
return super.get(clazz, otherClazz);
}

/**
Expand All @@ -67,7 +87,18 @@ public Comparator<?> getComparatorForType(Class<?> clazz) {
* @return is the giving type associated with any custom comparator
*/
public boolean hasComparatorForType(Class<?> type) {
return super.hasEntity(type);
return hasComparatorForDualTypes(type, null);
}

/**
* Checks, whether an any custom comparator is associated with the giving types.
*
* @param type the type of the left element for which to check a comparator
* @param otherType the type of the right element for which to check a comparator
* @return is the giving type associated with any custom comparator
*/
public boolean hasComparatorForDualTypes(Class<?> type, Class<?> otherType) {
return super.hasEntity(type, otherType);
}

/**
Expand All @@ -81,12 +112,25 @@ public <T> void registerComparator(Class<T> clazz, Comparator<? super T> compara
super.put(clazz, comparator);
}

/**
* Puts the {@code comparator} for the given {@code clazz} and {@code otherClazz}.
*
* @param clazz the class of the left element for the comparator
* @param otherClazz the class of the right element for the comparator
* @param comparator the comparator itself
* @param <T> the type of the left objects for the comparator
* @param <U> the type of the right objects for the comparator
*/
public <T, U> void registerComparator(Class<T> clazz, Class<U> otherClazz, Comparator<? super T> comparator) {
super.put(clazz, otherClazz, comparator);
}

/**
* Returns a sequence of all type-comparator pairs which the current holder supplies.
*
* @return sequence of field-comparator pairs
*/
public Stream<Entry<Class<?>, Comparator<?>>> comparatorByTypes() {
public Stream<Entry<DualClass<?, ?>, Comparator<?>>> comparatorByTypes() {
return super.entityByTypes();
}
}