Skip to content

Commit 5afe94e

Browse files
tommysitujoel-costigliola
authored andcommitted
Add ignoreFieldsOfTypes to RecursiveComparisonConfiguration
1 parent bff1a7f commit 5afe94e

12 files changed

+682
-104
lines changed

src/main/java/org/assertj/core/api/RecursiveComparisonAssert.java

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ public SELF ignoringActualNullFields() {
208208
}
209209

210210
/**
211-
* Makes the recursive comparison to ignore the given the object under test fields. Nested fields can be specified like this: {@code home.address.street}.
211+
* Makes the recursive comparison to ignore the given object under test fields. Nested fields can be specified like this: {@code home.address.street}.
212212
* <p>
213213
* Example:
214214
* <pre><code class='java'> public class Person {
@@ -302,6 +302,54 @@ public SELF ignoringFieldsMatchingRegexes(String... regexes) {
302302
return myself;
303303
}
304304

305+
/**
306+
* Makes the recursive comparison to ignore the object under test fields of the given types.
307+
* 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.
308+
* <p>
309+
* If some object under test fields are null it is not possible to evaluate their types unless in {@link #withStrictTypeChecking() strictTypeChecking mode},
310+
* in that case the corresponding expected field's type is evaluated instead but if strictTypeChecking mode is disabled then null fields are not ignored.
311+
* <p>
312+
* Example:
313+
* <pre><code class='java'> public class Person {
314+
* String name;
315+
* double height;
316+
* Home home = new Home();
317+
* }
318+
*
319+
* public class Home {
320+
* Address address = new Address();
321+
* }
322+
*
323+
* public static class Address {
324+
* int number;
325+
* String street;
326+
* }
327+
*
328+
* Person sherlock = new Person("Sherlock", 1.80);
329+
* sherlock.home.address.street = "Baker Street";
330+
* sherlock.home.address.number = 221;
331+
*
332+
* Person sherlock2 = new Person("Sherlock", 1.90);
333+
* sherlock2.home.address.street = "Butcher Street";
334+
* sherlock2.home.address.number = 221;
335+
*
336+
* // assertion succeeds as we ignore Address and height
337+
* assertThat(sherlock).usingRecursiveComparison()
338+
* .ignoringFieldsOfTypes(double.class, Address.class)
339+
* .isEqualTo(sherlock2);
340+
*
341+
* // now this assertion fails as expected since the home.address.street fields and height differ
342+
* assertThat(sherlock).usingRecursiveComparison()
343+
* .isEqualTo(sherlock2);</code></pre>
344+
*
345+
* @param typesToIgnore the types we want to ignore in the object under test fields.
346+
* @return this {@link RecursiveComparisonAssert} to chain other methods.
347+
*/
348+
public RecursiveComparisonAssert<?> ignoringFieldsOfTypes(Class<?>... typesToIgnore) {
349+
recursiveComparisonConfiguration.ignoreFieldsOfTypes(typesToIgnore);
350+
return myself;
351+
}
352+
305353
/**
306354
* By default the recursive comparison uses overridden {@code equals} methods to compare fields,
307355
* this method allows to compare recursively all fields <b>except fields with java types</b> (at some point we need to compare something!).

src/main/java/org/assertj/core/api/recursive/comparison/DualValue.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import static java.util.Collections.unmodifiableList;
1717
import static org.assertj.core.util.Arrays.array;
1818
import static org.assertj.core.util.Arrays.isArray;
19+
import static org.assertj.core.util.Lists.newArrayList;
1920
import static org.assertj.core.util.Strings.join;
2021

2122
import java.util.LinkedHashSet;
@@ -39,7 +40,7 @@ final class DualValue {
3940

4041

4142
DualValue(List<String> path, Object actual, Object expected) {
42-
this.path = path;
43+
this.path = newArrayList(path);
4344
this.concatenatedPath = join(path).with(".");
4445
this.actual = actual;
4546
this.expected = expected;
@@ -48,6 +49,10 @@ final class DualValue {
4849
hashCode = h1 + h2;
4950
}
5051

52+
DualValue(List<String> parentPath, String fieldName, Object actual, Object expected) {
53+
this(fiedlPath(parentPath, fieldName), actual, expected);
54+
}
55+
5156
@Override
5257
public boolean equals(Object other) {
5358
if (!(other instanceof DualValue)) return false;
@@ -73,6 +78,11 @@ public String getConcatenatedPath() {
7378
return concatenatedPath;
7479
}
7580

81+
public String getFieldName() {
82+
if (path.isEmpty()) return "";
83+
return path.get(path.size() - 1);
84+
}
85+
7686
public boolean isJavaType() {
7787
if (actual == null) return false;
7888
return actual.getClass().getName().startsWith("java.");
@@ -153,4 +163,10 @@ private static boolean isContainer(Object o) {
153163
isArray(o);
154164
}
155165

156-
}
166+
private static List<String> fiedlPath(List<String> parentPath, String fieldName) {
167+
List<String> fieldPath = newArrayList(parentPath);
168+
fieldPath.add(fieldName);
169+
return fieldPath;
170+
}
171+
172+
}

src/main/java/org/assertj/core/api/recursive/comparison/RecursiveComparisonConfiguration.java

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,27 @@
1313
package org.assertj.core.api.recursive.comparison;
1414

1515
import static java.lang.String.format;
16+
import static java.util.Arrays.stream;
1617
import static java.util.stream.Collectors.toList;
18+
import static java.util.stream.Collectors.toSet;
1719
import static org.assertj.core.configuration.ConfigurationProvider.CONFIGURATION_PROVIDER;
1820
import static org.assertj.core.internal.TypeComparators.defaultTypeComparators;
1921
import static org.assertj.core.util.Lists.list;
22+
import static org.assertj.core.util.Lists.newArrayList;
2023
import static org.assertj.core.util.Strings.join;
24+
import static org.assertj.core.util.introspection.PropertyOrFieldSupport.COMPARISON;
2125

2226
import java.util.ArrayList;
2327
import java.util.Comparator;
2428
import java.util.LinkedHashSet;
2529
import java.util.List;
2630
import java.util.Map.Entry;
2731
import java.util.Set;
28-
import java.util.function.Predicate;
2932
import java.util.regex.Pattern;
3033
import java.util.stream.Stream;
3134

3235
import org.assertj.core.api.RecursiveComparisonAssert;
36+
import org.assertj.core.internal.Objects;
3337
import org.assertj.core.internal.TypeComparators;
3438
import org.assertj.core.presentation.Representation;
3539
import org.assertj.core.util.VisibleForTesting;
@@ -43,6 +47,7 @@ public class RecursiveComparisonConfiguration {
4347
private boolean ignoreAllActualNullFields = false;
4448
private Set<FieldLocation> ignoredFields = new LinkedHashSet<>();
4549
private List<Pattern> ignoredFieldsRegexes = new ArrayList<>();
50+
private Set<Class<?>> ignoredTypes = new LinkedHashSet<>();
4651

4752
// overridden equals method to ignore section
4853
private List<Class<?>> ignoredOverriddenEqualsForTypes = new ArrayList<>();
@@ -132,6 +137,33 @@ public void ignoreFieldsMatchingRegexes(String... regexes) {
132137
.collect(toList()));
133138
}
134139

140+
/**
141+
* Adds the given types to the list of the object under test fields types to ignore in the recursive comparison.
142+
* The fields are ignored if their types exactly match one of the ignored types, if a field is a subtype of an ignored type it won't be ignored.
143+
* <p>
144+
* Note that if some object under test fields are null, they are not ignored by this method as their type can't be evaluated.
145+
* <p>
146+
* See {@link RecursiveComparisonAssert#ignoringFields(String...) RecursiveComparisonAssert#ignoringFields(String...)} for examples.
147+
*
148+
* @param types the types of the object under test to ignore in the comparison.
149+
*/
150+
public void ignoreFieldsOfTypes(Class<?>... types) {
151+
stream(types).map(type -> asWrapperIfPrimitiveType(type)).forEach(ignoredTypes::add);
152+
}
153+
154+
private static Class<?> asWrapperIfPrimitiveType(Class<?> type) {
155+
if (!type.isPrimitive()) return type;
156+
if (type.equals(boolean.class)) return Boolean.class;
157+
if (type.equals(byte.class)) return Byte.class;
158+
if (type.equals(int.class)) return Integer.class;
159+
if (type.equals(short.class)) return Short.class;
160+
if (type.equals(char.class)) return Character.class;
161+
if (type.equals(float.class)) return Float.class;
162+
if (type.equals(double.class)) return Double.class;
163+
// should not arrive here since we have tested primitive types first
164+
return type;
165+
}
166+
135167
/**
136168
* Returns the list of the object under test fields to ignore in the recursive comparison.
137169
*
@@ -141,6 +173,15 @@ public Set<FieldLocation> getIgnoredFields() {
141173
return ignoredFields;
142174
}
143175

176+
/**
177+
* Returns the set of the object under test fields types to ignore in the recursive comparison.
178+
*
179+
* @return the set of the object under test fields types to ignore in the recursive comparison.
180+
*/
181+
public Set<Class<?>> getIgnoredTypes() {
182+
return ignoredTypes;
183+
}
184+
144185
/**
145186
* Force a recursive comparison on all fields (except java types).
146187
* <p>
@@ -324,6 +365,7 @@ public String multiLineDescription(Representation representation) {
324365
describeIgnoreAllActualNullFields(description);
325366
describeIgnoredFields(description);
326367
describeIgnoredFieldsRegexes(description);
368+
describeIgnoredFieldsForTypes(description);
327369
describeOverriddenEqualsMethodsUsage(description, representation);
328370
describeIgnoreCollectionOrder(description);
329371
describeIgnoredCollectionOrderInFields(description);
@@ -334,25 +376,58 @@ public String multiLineDescription(Representation representation) {
334376
return description.toString();
335377
}
336378

337-
// non public stuff
338-
339-
boolean shouldIgnore(DualValue dualKey) {
340-
return matchesAnIgnoredNullField(dualKey)
341-
|| matchesAnIgnoredField(dualKey)
342-
|| matchesAnIgnoredFieldRegex(dualKey);
379+
boolean shouldIgnore(DualValue dualValue) {
380+
String concatenatedPath = dualValue.concatenatedPath;
381+
return matchesAnIgnoredField(concatenatedPath)
382+
|| matchesAnIgnoredFieldRegex(concatenatedPath)
383+
|| shouldIgnoreNotEvalutingFieldName(dualValue);
343384
}
344385

345-
Predicate<String> shouldKeepField(String parentConcatenatedPath) {
346-
return fieldName -> shouldKeepField(parentConcatenatedPath, fieldName);
386+
Set<String> getNonIgnoredActualFieldNames(DualValue dualValue) {
387+
Set<String> actualFieldsNames = Objects.getFieldsNames(dualValue.actual.getClass());
388+
// we are doing the same as shouldIgnore(DualValue dualKey) but in two steps for performance reasons:
389+
// - we filter first ignored field by names that don't need building DualValues
390+
// - then we filter field DualValues with the remaining criteria (shouldIgnoreNotEvalutingFieldName)
391+
// DualValuea are built introspecting fields which is expensive.
392+
return actualFieldsNames.stream()
393+
// evaluate field name ignoring criteria
394+
.filter(fieldName -> !shouldIgnore(dualValue.path, fieldName))
395+
.map(fieldName -> dualValueForField(dualValue, fieldName))
396+
// evaluate field value ignoring criteria
397+
.filter(fieldDualValue -> !shouldIgnoreNotEvalutingFieldName(fieldDualValue))
398+
// back to field name
399+
.map(DualValue::getFieldName)
400+
.filter(fieldName -> !fieldName.isEmpty())
401+
.collect(toSet());
347402
}
348403

349-
private boolean shouldKeepField(String parentPath, String fieldName) {
350-
String fieldConcatenatedPath = concatenatedPath(parentPath, fieldName);
351-
return !matchesAnIgnoredField(fieldConcatenatedPath) && !matchesAnIgnoredFieldRegex(fieldConcatenatedPath);
352-
}
404+
// non public stuff
353405

354-
private static String concatenatedPath(String parentPath, String name) {
355-
return parentPath.isEmpty() ? name : format("%s.%s", parentPath, name);
406+
private boolean shouldIgnoreNotEvalutingFieldName(DualValue dualKey) {
407+
return matchesAnIgnoredNullField(dualKey) || matchesAnIgnoredFieldType(dualKey);
408+
}
409+
410+
private boolean shouldIgnore(List<String> parentConcatenatedPath, String fieldName) {
411+
List<String> fieldConcatenatedPathList = newArrayList(parentConcatenatedPath);
412+
fieldConcatenatedPathList.add(fieldName);
413+
String fieldConcatenatedPath = join(fieldConcatenatedPathList).with(".");
414+
return matchesAnIgnoredField(fieldConcatenatedPath) || matchesAnIgnoredFieldRegex(fieldConcatenatedPath);
415+
}
416+
417+
private static DualValue dualValueForField(DualValue parentDualValue, String fieldName) {
418+
List<String> path = newArrayList(parentDualValue.path);
419+
path.add(fieldName);
420+
Object actualFieldValue = COMPARISON.getSimpleValue(fieldName, parentDualValue.actual);
421+
// no guarantees we have a field in expected named as fieldName
422+
Object expectedFieldValue;
423+
try {
424+
expectedFieldValue = COMPARISON.getSimpleValue(fieldName, parentDualValue.expected);
425+
} catch (@SuppressWarnings("unused") Exception e) {
426+
// set the field to null to express it is absent, this not 100% accurate as the value could be null
427+
// but it works to evaluate if dualValue should be ignored with matchesAnIgnoredFieldType
428+
expectedFieldValue = null;
429+
}
430+
return new DualValue(path, actualFieldValue, expectedFieldValue);
356431
}
357432

358433
boolean hasCustomComparator(DualValue dualValue) {
@@ -393,6 +468,11 @@ private void describeIgnoredFields(StringBuilder description) {
393468
description.append(format("- the following fields were ignored in the comparison: %s%n", describeIgnoredFields()));
394469
}
395470

471+
private void describeIgnoredFieldsForTypes(StringBuilder description) {
472+
if (!ignoredTypes.isEmpty())
473+
description.append(format("- the following types were ignored in the comparison: %s%n", describeIgnoredTypes()));
474+
}
475+
396476
private void describeIgnoreAllActualNullFields(StringBuilder description) {
397477
if (ignoreAllActualNullFields) description.append(format("- all actual null fields were ignored in the comparison%n"));
398478
}
@@ -469,21 +549,24 @@ private boolean matchesAnIgnoredOverriddenEqualsField(DualValue dualKey) {
469549
.anyMatch(fieldLocation -> fieldLocation.matches(dualKey.concatenatedPath));
470550
}
471551

472-
private boolean matchesAnIgnoredNullField(DualValue dualKey) {
473-
return ignoreAllActualNullFields && dualKey.actual == null;
552+
private boolean matchesAnIgnoredNullField(DualValue dualValue) {
553+
return ignoreAllActualNullFields && dualValue.actual == null;
474554
}
475555

476556
private boolean matchesAnIgnoredFieldRegex(String fieldConcatenatedPath) {
477557
return ignoredFieldsRegexes.stream()
478558
.anyMatch(regex -> regex.matcher(fieldConcatenatedPath).matches());
479559
}
480560

481-
private boolean matchesAnIgnoredFieldRegex(DualValue dualKey) {
482-
return matchesAnIgnoredFieldRegex(dualKey.concatenatedPath);
483-
}
484-
485-
private boolean matchesAnIgnoredField(DualValue dualKey) {
486-
return matchesAnIgnoredField(dualKey.concatenatedPath);
561+
private boolean matchesAnIgnoredFieldType(DualValue dualKey) {
562+
Object actual = dualKey.actual;
563+
if (actual != null) return ignoredTypes.contains(actual.getClass());
564+
Object expected = dualKey.expected;
565+
// actual is null => we can't evaluate its type, we can only reliably check dualKey.expected's type if
566+
// strictTypeChecking is enabled which guarantees expected is of the same type.
567+
if (strictTypeChecking && expected != null) return ignoredTypes.contains(expected.getClass());
568+
// if strictTypeChecking is disabled, we can't safely ignore the field (if we did, we would ignore all null fields!).
569+
return false;
487570
}
488571

489572
private boolean matchesAnIgnoredField(String fieldConcatenatedPath) {
@@ -508,6 +591,13 @@ private String describeIgnoredFields() {
508591
return join(fieldsDescription).with(", ");
509592
}
510593

594+
private String describeIgnoredTypes() {
595+
List<String> typesDescription = ignoredTypes.stream()
596+
.map(Class::getName)
597+
.collect(toList());
598+
return join(typesDescription).with(", ");
599+
}
600+
511601
private String describeIgnoredCollectionOrderInFields() {
512602
List<String> fieldsDescription = ignoredCollectionOrderInFields.stream()
513603
.map(FieldLocation::getFieldPath)

0 commit comments

Comments
 (0)