Skip to content

Commit

Permalink
Add ignoringFieldsOfTypesMatchingRegexes
Browse files Browse the repository at this point in the history
Fix #3369

(cherry picked from commit ac2d76229beb1693ef9d3556180f217f879441fb)
  • Loading branch information
joel-costigliola committed Mar 31, 2024
1 parent 02985b6 commit 8a7843e
Show file tree
Hide file tree
Showing 9 changed files with 422 additions and 65 deletions.
Expand Up @@ -104,7 +104,7 @@ public RecursiveComparisonAssert(Object actual, RecursiveComparisonConfiguration
* <p>
* <strong>Example</strong>
* <p>
* Here is a basic example with a default {@link RecursiveComparisonConfiguration}, you can find other examples for each of the method changing the recursive comparison behavior
* Here is a basic example with a default {@link RecursiveComparisonConfiguration}, you can find other examples for each of the methods changing the recursive comparison behavior
* like {@link #ignoringFields(String...)}.
* <pre><code class='java'> class Person {
* String name;
Expand Down Expand Up @@ -140,20 +140,19 @@ public RecursiveComparisonAssert(Object actual, RecursiveComparisonConfiguration
* @return {@code this} assertion object.
* @throws AssertionError if the actual object is {@code null}.
* @throws AssertionError if the actual and the given objects are not deeply equal property/field by property/field.
* @throws IntrospectionError if one property/field to compare can not be found.
* @throws IntrospectionError if one property/field to compare cannot be found.
*/
@Override
public SELF isEqualTo(Object expected) {
// deals with both actual and expected being null
if (actual == expected) return myself;
if (expected == null) {
// for the assertion to pass, actual must be null but this is not the case since actual != expected
// => we fail expecting actual to be null
// for the assertion to pass, actual must be null, but this is not the case since actual != expected
objects.assertNull(info, actual);
}
// at this point expected is not null, which means actual must not be null for the assertion to pass
objects.assertNotNull(info, actual);
// at this point both actual and expected are not null, we can compare them recursively!
// at this point, both actual and expected are not null, we can compare them recursively!
List<ComparisonDifference> differences = determineDifferencesWith(expected);
if (!differences.isEmpty()) throw objects.getFailures().failure(info, shouldBeEqualByComparingFieldByFieldRecursively(actual,
expected,
Expand Down Expand Up @@ -212,7 +211,7 @@ public SELF isNotEqualTo(Object other) {
recursiveComparisonConfiguration,
info.representation()));
}
// either one of actual or other was null (but not both) or there were no differences
// either actual or other was null (but not both) or there were no differences
return myself;
}

Expand Down Expand Up @@ -375,7 +374,7 @@ public SELF isNotIn(Object... values) {
}

/**
* Verifies that the actual value is not present in the given iterable, comparing values with the recursive comparison..
* Verifies that the actual value is not present in the given iterable, comparing values with the recursive comparison.
* <p>
* This assertion always succeeds if the given iterable is empty.
* <p>
Expand Down Expand Up @@ -504,7 +503,7 @@ public SELF comparingOnlyFields(String... fieldNamesToCompare) {
* the resulting compared fields = {specified compared fields of types} {@code -} {specified ignored fields}.<br>
* For example, we specify the following compared types: {@code {String.class, Integer.class, Double.class}}, and the
* object to compare has fields {@code String foo}, {@code Integer baz} and {@code Double bar},
* if we ignore the {"bar"} field with {@link RecursiveComparisonAssert#ignoringFields(String...)} the comparison will only report differences on {@code {foo, baz}} fields..
* if we ignore the {"bar"} field with {@link RecursiveComparisonAssert#ignoringFields(String...)} the comparison will only report differences on {@code {foo, baz}} fields.
* <p>
* Usage example:
* <pre><code class='java'> class Person {
Expand Down Expand Up @@ -793,10 +792,14 @@ public SELF ignoringFieldsMatchingRegexes(String... regexes) {

/**
* Makes the recursive comparison to ignore the object under test fields of the given types.
* The fields are ignored if their types <b>exactly match one of the ignored types</b>, for example if a field is a subtype of an ignored type it is not ignored.
* <p>
* If some object under test fields are null it is not possible to evaluate their types unless in {@link #withStrictTypeChecking() strictTypeChecking mode},
* in that case the corresponding expected field's type is evaluated instead but if strictTypeChecking mode is disabled then null fields are not ignored.
* The fields are ignored if their types <b>exactly match one of the ignored types</b>, for example,
* if a field is a subtype of an ignored type it is not ignored.
* <p>
* If {@code strictTypeChecking} mode is disabled then null fields are ignored since their types cannot be known.
* <p>
* If {@code strictTypeChecking} mode is enabled and a field of the object under test is null, the recursive
* comparison evaluates the corresponding expected field's type.
* <p>
* Example:
* <pre><code class='java'> class Person {
Expand Down Expand Up @@ -839,6 +842,63 @@ public RecursiveComparisonAssert<?> ignoringFieldsOfTypes(Class<?>... typesToIgn
return myself;
}

/**
* Makes the recursive comparison to ignore the fields of the object under test having types matching one of the given regexes.
* The fields are ignored if their types <b>exactly match one of the regexes</b>, if a field is a subtype of a matched type it is not ignored.
* <p>
* One use case of this method is to ignore types that can't be introspected.
* <p>
* If {@code strictTypeChecking} mode is enabled and a field of the object under test is null, the recursive
* comparison evaluates the corresponding expected field's type (if not null), if it is disabled then the field is evaluated as
* usual (i.e. it is not ignored).
* <p>
* <b>Warning</b>: primitive types are not directly supported because under the hood they are converted to their
* corresponding wrapping types, for example {@code int} to {@code java.lang.Integer}. The preferred way to ignore
* primitive types is to use {@link #ignoringFieldsOfTypes(Class[])}.
* Another way is to ignore the wrapping type, for example ignoring {@code java.lang.Integer} ignores both
* {@code java.lang.Integer} and {@code int} fields.
* <p>
* Example:
* <pre><code class='java'> class Person {
* String name;
* double height;
* Home home = new Home();
* }
*
* class Home {
* Address address = new Address();
* }
*
* class Address {
* int number;
* String street;
* }
*
* Person sherlock = new Person("Sherlock", 1.80);
* sherlock.home.address.street = "Baker Street";
* sherlock.home.address.number = 221;
*
* Person cherlock = new Person("Cherlock", 1.80);
* cherlock.home.address.street = "Butcher Street";
* cherlock.home.address.number = 221;
*
* // assertion succeeds as we ignore Address and height
* assertThat(sherlock).usingRecursiveComparison()
* .ignoringFieldsOfTypes(".*Address", "java\\.util\\.String")
* .isEqualTo(cherlock);
*
* // now this assertion fails as expected since the home.address.street fields and name differ
* assertThat(sherlock).usingRecursiveComparison()
* .isEqualTo(cherlock);</code></pre>
*
* @param regexes regexes specifying the types to ignore.
* @return this {@link RecursiveComparisonAssert} to chain other methods.
*/
public RecursiveComparisonAssert<?> ignoringFieldsOfTypesMatchingRegexes(String... regexes) {
recursiveComparisonConfiguration.ignoreFieldsOfTypesMatchingRegexes(regexes);
return myself;
}

/**
* This method instructs the recursive comparison to compare recursively all fields including the one whose type have overridden equals,
* <b>except fields with java types</b> (at some point we need to compare something!).
Expand Down Expand Up @@ -957,7 +1017,7 @@ public SELF usingOverriddenEquals() {

/**
* In case you have instructed the recursive to use overridden {@code equals} with {@link #usingOverriddenEquals()},
* this method allows to ignore overridden {@code equals} for the given fields (it adds them to the already registered ones).
* this method allows ignoring overridden {@code equals} for the given fields (it adds them to the already registered ones).
* <p>
* Since 3.17.0 all overridden {@code equals} so this method is only relevant if you have called {@link #usingOverriddenEquals()} before.
* <p>
Expand Down Expand Up @@ -1022,7 +1082,7 @@ public SELF ignoringOverriddenEqualsForFields(String... fields) {

/**
* By default, the recursive comparison uses overridden {@code equals} methods to compare fields,
* this method allows to force a recursive comparison for all fields of the given types (it adds them to the already registered ones).
* this method allows forcing a recursive comparison for all fields of the given types (it adds them to the already registered ones).
* <p>
* Since 3.17.0 all overridden {@code equals} so this method is only relevant if you have called {@link #usingOverriddenEquals()} before.
* <p>
Expand Down Expand Up @@ -1085,7 +1145,7 @@ public SELF ignoringOverriddenEqualsForTypes(Class<?>... types) {

/**
* In case you have instructed the recursive comparison to use overridden {@code equals} with {@link #usingOverriddenEquals()},
* this method allows to force a recursive comparison for the fields matching the given regexes (it adds them to the already registered ones).
* this method allows forcing a recursive comparison for the fields matching the given regexes (it adds them to the already registered ones).
* <p>
* Since 3.17.0 all overridden {@code equals} so this method is only relevant if you have called {@link #usingOverriddenEquals()} before.
* <p>
Expand Down Expand Up @@ -1149,7 +1209,7 @@ public SELF ignoringOverriddenEqualsForFieldsMatchingRegexes(String... regexes)
* Makes the recursive comparison to ignore collection order in all fields in the object under test.
* <p>
* <b>Important:</b> ignoring collection order has a high performance cost because each element of the actual collection must
* be compared to each element of the expected collection which is a O(n&sup2;) operation. For example with a collection of 100
* be compared to each element of the expected collection which is an O(n&sup2;) operation. For example with a collection of 100
* elements, the number of comparisons is 100x100 = 10 000!
* <p>
* Example:
Expand Down Expand Up @@ -1339,7 +1399,7 @@ public SELF withStrictTypeChecking() {
}

/**
* Allows to register a {@link BiPredicate} to compare fields with the given locations.
* Allows registering a {@link BiPredicate} to compare fields with the given locations.
* A typical usage is to compare double/float fields with a given precision.
* <p>
* BiPredicates specified with this method have precedence over the ones registered with {@link #withEqualsForType(BiPredicate, Class)}
Expand Down Expand Up @@ -1386,7 +1446,7 @@ public SELF withEqualsForFields(BiPredicate<?, ?> equals, String... fieldLocatio
}

/**
* Allows to register a {@link BiPredicate} to compare fields whose location matches the given regexes.
* Allows registering a {@link BiPredicate} to compare fields whose location matches the given regexes.
* A typical usage is to compare double/float fields with a given precision.
* <p>
* The fields are evaluated from the root object, for example if {@code Foo} has a {@code Bar} field and both have an {@code id} field,
Expand Down Expand Up @@ -1425,7 +1485,7 @@ public SELF withEqualsForFields(BiPredicate<?, ?> equals, String... fieldLocatio
* .isEqualTo(hugeFrodo);</code></pre>
*
* @param equals the {@link BiPredicate} to use to compare the fields matching the given regexes
* @param regexes the regexes from the root object of the fields location the BiPredicate should be used for
* @param regexes the regexes from the root object of the field locations the BiPredicate should be used for
*
* @return this {@link RecursiveComparisonAssert} to chain other methods.
* @throws NullPointerException if the given BiPredicate is null.
Expand All @@ -1437,7 +1497,7 @@ public SELF withEqualsForFieldsMatchingRegexes(BiPredicate<?, ?> equals, String.
}

/**
* Allows to register a comparator to compare fields with the given locations.
* Allows registering a comparator to compare fields with the given locations.
* A typical usage is to compare double/float fields with a given precision.
* <p>
* Comparators registered with this method have precedence over comparators registered with {@link #withComparatorForType(Comparator, Class)}
Expand Down Expand Up @@ -1483,7 +1543,7 @@ public SELF withComparatorForFields(Comparator<?> comparator, String... fieldLoc
}

/**
* Allows to register a comparator to compare the fields with the given type.
* Allows registering a comparator to compare the fields with the given type.
* A typical usage is to compare double/float fields with a given precision.
* <p>
* Comparators registered with this method have less precedence than comparators registered with {@link #withComparatorForFields(Comparator, String...) withComparatorForFields(Comparator, String...)}
Expand Down Expand Up @@ -1527,7 +1587,7 @@ public <T> SELF withComparatorForType(Comparator<? super T> comparator, Class<T>
}

/**
* Allows to register a {@link BiPredicate} to compare the fields with the given type.
* Allows registering 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...)}
Expand Down
Expand Up @@ -36,6 +36,7 @@ public abstract class AbstractRecursiveOperationConfiguration {
private final Set<String> ignoredFields = new LinkedHashSet<>();
private final List<Pattern> ignoredFieldsRegexes = new ArrayList<>();
private final Set<Class<?>> ignoredTypes = new LinkedHashSet<>();
private final List<Pattern> ignoredTypesRegexes = new ArrayList<>();

protected AbstractRecursiveOperationConfiguration(AbstractBuilder<?> builder) {
ignoreFields(builder.ignoredFields);
Expand Down Expand Up @@ -76,9 +77,7 @@ public Set<String> getIgnoredFields() {
* @param regexes regexes used to ignore fields in the comparison.
*/
public void ignoreFieldsMatchingRegexes(String... regexes) {
List<Pattern> patterns = Stream.of(regexes)
.map(Pattern::compile)
.collect(toList());
List<Pattern> patterns = toPatterns(regexes);
ignoredFieldsRegexes.addAll(patterns);
}

Expand All @@ -92,37 +91,39 @@ public List<Pattern> getIgnoredFieldsRegexes() {
* <p>
* If some object under test fields are null it is not possible to evaluate their types and thus these fields are not ignored.
* <p>
* Example:
* <pre><code class='java'> public class Person {
* String name;
* String occupation;
* Address address = new Address();
* }
*
* public static class Address {
* int number;
* String street;
* }
*
* Person sherlock = new Person("Sherlock", "Detective");
* sherlock.address.street = "Baker Street";
* sherlock.address.number = 221;
*
* // assertion succeeds Person has only String fields except for address
* assertThat(sherlock).usingRecursiveAssertion()
* .ignoringFieldsOfTypes(Address.class)
* .allFieldsSatisfy(field -> field instanceof String);
*
* // assertion fails because of address and address.number
* assertThat(sherlock).usingRecursiveComparison()
* .allFieldsSatisfy(field -> field instanceof String);</code></pre>
* Example: see {@link RecursiveComparisonAssert#ignoringFieldsOfTypes(Class[])}.
*
* @param types the types of the object under test to ignore in the comparison.
*/
public void ignoreFieldsOfTypes(Class<?>... types) {
stream(types).map(AbstractRecursiveOperationConfiguration::asWrapperIfPrimitiveType).forEach(ignoredTypes::add);
}

/**
* Makes the recursive comparison to ignore the fields of the object under test having types matching one of the given regexes.
* The fields are ignored if their types <b>exactly match one of the regexes</b>, if a field is a subtype of a matched type it is not ignored.
* <p>
* One use case of this method is to ignore types that can't be introspected.
* <p>
* If {@code strictTypeChecking} mode is enabled and a field of the object under test is null, the recursive
* comparison evaluates the corresponding expected field's type (if not null), if it is disabled then the field is evaluated as
* usual (i.e. it is not ignored).
* <p>
* <b>Warning</b>: primitive types are not directly supported because under the hood they are converted to their
* corresponding wrapping types, for example {@code int} to {@code java.lang.Integer}. The preferred way to ignore
* primitive types is to use {@link #ignoreFieldsOfTypes(Class[])}.
* Another way is to ignore the wrapping type, for example ignoring {@code java.lang.Integer} ignores both
* {@code java.lang.Integer} and {@code int} fields.
* <p>
* Example: see {@link RecursiveComparisonAssert#ignoringFieldsOfTypesMatchingRegexes(String...)}.
*
* @param regexes regexes specifying the types to ignore.
*/
public void ignoreFieldsOfTypesMatchingRegexes(String... regexes) {
List<Pattern> patterns = toPatterns(regexes);
ignoredTypesRegexes.addAll(patterns);
}

protected static Class<?> asWrapperIfPrimitiveType(Class<?> type) {
if (!type.isPrimitive()) return type;
if (type.equals(boolean.class)) return Boolean.class;
Expand All @@ -145,6 +146,15 @@ public Set<Class<?>> getIgnoredTypes() {
return ignoredTypes;
}

/**
* Returns the regexes that will be used to ignore fields with types matching these regexes in the recursive comparison.
*
* @return the regexes that will be used to ignore fields with types matching these regexes in the recursive comparison.
*/
public List<Pattern> getIgnoredTypesRegexes() {
return ignoredTypesRegexes;
}

protected void describeIgnoredFields(StringBuilder description) {
if (!getIgnoredFields().isEmpty())
description.append(format("- the following fields were ignored in the comparison: %s%n", describeIgnoredFields()));
Expand Down Expand Up @@ -242,4 +252,11 @@ public BUILDER_TYPE withIgnoredFieldsOfTypes(Class<?>... types) {
return thisBuilder;
}
}

private static List<Pattern> toPatterns(String[] regexes) {
return Stream.of(regexes)
.map(Pattern::compile)
.collect(toList());
}

}

0 comments on commit 8a7843e

Please sign in to comment.