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

Add test case for slowness in deeply-nested usingRecursiveComparison().isEqualTo() uses in #3350

Closed
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@

import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableList;
import static java.util.Collections.unmodifiableSet;
import static java.util.Objects.requireNonNull;
import static java.util.stream.Collectors.joining;
import static java.util.stream.Collectors.toList;
import static org.assertj.core.util.Lists.list;
import static org.assertj.core.util.Sets.newLinkedHashSet;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;

/**
Expand All @@ -33,7 +36,7 @@ public final class FieldLocation implements Comparable<FieldLocation> {

private final String pathToUseInRules;
private final List<String> decomposedPath;
private final List<String> pathsHierarchyToUseInRules;
private final Set<String> pathsHierarchyToUseInRules;

public FieldLocation(List<String> path) {
decomposedPath = unmodifiableList(requireNonNull(path, "path cannot be null"));
Expand Down Expand Up @@ -163,7 +166,10 @@ public boolean equals(Object obj) {

@Override
public int hashCode() {
return Objects.hash(pathToUseInRules, decomposedPath, pathsHierarchyToUseInRules);
int result = Objects.hashCode(pathToUseInRules);
result = 31 * result + Objects.hashCode(decomposedPath);
result = 31 * result + Objects.hashCode(pathsHierarchyToUseInRules);
return result;
scordio marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
Expand Down Expand Up @@ -195,6 +201,10 @@ public String getFieldName() {
public boolean isRoot() {
// root is the top level object compared or in case of the top level is a iterable/array the elements are considered as roots.
// we don't do it for optional it has a 'value' field so for the moment
return isRootPath(pathToUseInRules);
}

private boolean isRootPath(String pathToUseInRules) {
return pathToUseInRules.isEmpty();
}

Expand Down Expand Up @@ -256,21 +266,23 @@ public boolean hasChild(FieldLocation child) {
return child.hasParent(this);
}

private List<String> pathsHierarchyToUseInRules() {
List<FieldLocation> fieldAndParentFields = list();
FieldLocation currentLocation = this;
while (!currentLocation.isRoot()) {
fieldAndParentFields.add(currentLocation);
currentLocation = currentLocation.parent();
private Set<String> pathsHierarchyToUseInRules() {
// using LinkedHashSet to maintain leaf to root iteration order
// so that hierarchyMatchesRegex can try matching longest to shorted path
Set<String> fieldAndParentFields = newLinkedHashSet();
String currentPath = this.pathToUseInRules;
while (!isRootPath(currentPath)) {
fieldAndParentFields.add(currentPath);
currentPath = parent(currentPath);
}
return fieldAndParentFields.stream()
.map(fieldLocation -> fieldLocation.pathToUseInRules)
.collect(toList());
return unmodifiableSet(fieldAndParentFields);
}

private FieldLocation parent() {
List<String> parentPath = new ArrayList<>(decomposedPath);
parentPath.remove(decomposedPath.size() - 1);
return new FieldLocation(parentPath);
private String parent(String currentPath) {
int lastDot = currentPath.lastIndexOf('.');
if (lastDot < 0) {
return "";
}
return currentPath.substring(0, lastDot);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
import static org.assertj.core.api.BDDAssertions.then;
import static org.assertj.core.util.Lists.list;

import java.time.Duration;
import java.util.Collections;
import java.util.List;

import com.google.common.base.Stopwatch;
import org.assertj.core.api.recursive.comparison.FieldLocation;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -73,4 +75,13 @@ void should_build_from_string_nested_path() {
then(underTest.getDecomposedPath()).isEqualTo(list("name", "first", "second"));
}

@Test
void should_build_from_long_nested_path_in_reasonable_time() {
// WHEN
Stopwatch stopwatch = Stopwatch.createStarted();
FieldLocation underTest = new FieldLocation("1.2.3.4.5.6.7.8.9.10");
// THEN
then(stopwatch.elapsed()).isLessThan(Duration.ofSeconds(10));
then(underTest.getDecomposedPath()).isEqualTo(list("1", "2", "3", "4", "5", "6", "7", "8", "9", "10"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.sql.Timestamp;
import java.time.Duration;
import java.util.Date;
import java.util.List;
import java.util.Map;
Expand All @@ -40,6 +41,7 @@

import javax.xml.datatype.DatatypeFactory;

import com.google.common.base.Stopwatch;
import org.assertj.core.api.RecursiveComparisonAssert_isEqualTo_BaseTest;
import org.assertj.core.internal.objects.data.AlwaysEqualPerson;
import org.assertj.core.internal.objects.data.FriendlyPerson;
Expand Down Expand Up @@ -538,6 +540,124 @@ void performance_for_comparing_lots_of_similar_types() {
// config.setIntrospectionStrategy(ComparingFields.COMPARING_FIELDS);

new RecursiveComparisonDifferenceCalculator().determineDifferences(actual, expected, config);
}

@Test
void can_compare_deeply_nested_objects_in_reasonable_time() {
Person p1a = new Person("Alice");
Person p1b = new Person("Alice");
Person p2a = new Person("Brian");
Person p2b = new Person("Brian");
Person p3a = new Person("Christina");
Person p3b = new Person("Christina");
Person p4a = new Person("David");
Person p4b = new Person("David");
Person p5a = new Person("Emily");
Person p5b = new Person("Emily");
Person p6a = new Person("Francisco");
Person p6b = new Person("Francisco");
Person p7a = new Person("Gabriella");
Person p7b = new Person("Gabriella");
Person p8a = new Person("Henry");
Person p8b = new Person("Henry");
Person p9a = new Person("Isabelle");
Person p9b = new Person("Isabelle");
Person p10a = new Person("Jackson");
Person p10b = new Person("Jackson");
Person p11a = new Person("Kimberly");
Person p11b = new Person("Kimberly");
Person p12a = new Person("Lucas");
Person p12b = new Person("Lucas");
Person p13a = new Person("Melissa");
Person p13b = new Person("Melissa");
Person p14a = new Person("Nathan");
Person p14b = new Person("Nathan");
Person p15a = new Person("Olivia");
Person p15b = new Person("Olivia");
Person p16a = new Person("Penelope");
Person p16b = new Person("Penelope");
Person p17a = new Person("Quentin");
Person p17b = new Person("Quentin");
Person p18a = new Person("Rebecca");
Person p18b = new Person("Rebecca");
Person p19a = new Person("Samuel");
Person p19b = new Person("Samuel");
Person p20a = new Person("Tanya");
Person p20b = new Person("Tanya");
Person p21a = new Person("Ursula");
Person p21b = new Person("Ursula");
Person p22a = new Person("Victor");
Person p22b = new Person("Victor");
Person p23a = new Person("Whitney");
Person p23b = new Person("Whitney");
Person p24a = new Person("Xavier");
Person p24b = new Person("Xavier");
Person p25a = new Person("Yasmine");
Person p25b = new Person("Yasmine");
Person p26a = new Person("Zachary");
Person p26b = new Person("Zachary");
p1a.neighbour = p2a;
p1b.neighbour = p2b;
p2a.neighbour = p3a;
p2b.neighbour = p3b;
p3a.neighbour = p4a;
p3b.neighbour = p4b;
p4a.neighbour = p5a;
p4b.neighbour = p5b;
p5a.neighbour = p6a;
p5b.neighbour = p6b;
p6a.neighbour = p7a;
p6b.neighbour = p7b;
p7a.neighbour = p8a;
p7b.neighbour = p8b;
p8a.neighbour = p9a;
p8b.neighbour = p9b;
p9a.neighbour = p10a;
p9b.neighbour = p10b;
p10a.neighbour = p11a;
p10b.neighbour = p11b;
p11a.neighbour = p12a;
p11b.neighbour = p12b;
p12a.neighbour = p13a;
p12b.neighbour = p13b;
p13a.neighbour = p14a;
p13b.neighbour = p14b;
p14a.neighbour = p15a;
p14b.neighbour = p15b;
p15a.neighbour = p16a;
p15b.neighbour = p16b;
p16a.neighbour = p17a;
p16b.neighbour = p17b;
p17a.neighbour = p18a;
p17b.neighbour = p18b;
p18a.neighbour = p19a;
p18b.neighbour = p19b;
p19a.neighbour = p20a;
p19b.neighbour = p20b;

// This fails at 15sec > 10sec on my 2021 Apple M1 Pro. Uncomment more references below to increase the time. Every
// additional link roughly doubles the execution time.

p20a.neighbour = p21a;
p20b.neighbour = p21b;

p21a.neighbour = p22a;
p21b.neighbour = p22b;

p22a.neighbour = p23a;
p22b.neighbour = p23b;

p23a.neighbour = p24a;
p23b.neighbour = p24b;

p24a.neighbour = p25a;
p24b.neighbour = p25b;

p25a.neighbour = p26a;
p25b.neighbour = p26b;

Stopwatch stopwatch = Stopwatch.createStarted();
assertThat(p1a).usingRecursiveComparison().isEqualTo(p1b);
assertThat(stopwatch.elapsed()).isLessThan(Duration.ofSeconds(10));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
import static org.assertj.core.api.recursive.comparison.FieldLocation.rootFieldLocation;
import static org.assertj.core.util.Lists.list;

import java.time.Duration;
import java.util.Date;
import java.util.Set;

import com.google.common.base.Stopwatch;
import org.assertj.core.internal.objects.data.Person;
import org.assertj.core.internal.objects.data.PersonDtoWithPersonNeighbour;
import org.assertj.core.test.Employee;
Expand Down Expand Up @@ -94,6 +96,62 @@ void should_only_return_fields_from_compareOnlyFields_list() {
then(fields).containsExactly("name");
}

@Test
void should_return_fields_in_a_reasonable_amount_of_time_for_deeply_nested_object() {
// GIVEN
Person p1 = new Person("Alice");
Person p2 = new Person("Brian");
Person p3 = new Person("Christina");
Person p4 = new Person("David");
Person p5 = new Person("Emily");
Person p6 = new Person("Francisco");
Person p7 = new Person("Gabriella");
Person p8 = new Person("Henry");
Person p9 = new Person("Isabelle");
Person p10 = new Person("Jackson");
Person p11 = new Person("Kimberly");
Person p12 = new Person("Lucas");
Person p13 = new Person("Melissa");
Person p14 = new Person("Nathan");
Person p15 = new Person("Olivia");
Person p16 = new Person("Penelope");
Person p17 = new Person("Quentin");
Person p18 = new Person("Rebecca");
Person p19 = new Person("Samuel");
Person p20 = new Person("Tanya");
p1.neighbour = p2;
p2.neighbour = p3;
p3.neighbour = p4;
p4.neighbour = p5;
p5.neighbour = p6;
p6.neighbour = p7;
p7.neighbour = p8;
p8.neighbour = p9;
p9.neighbour = p10;
p10.neighbour = p11;
p11.neighbour = p12;
p12.neighbour = p13;
p13.neighbour = p14;
p14.neighbour = p15;
p15.neighbour = p16;
p16.neighbour = p17;
p17.neighbour = p18;
p18.neighbour = p19;
p19.neighbour = p20;

Person p1b = new Person("Anders");
p1b.neighbour = p2;

DualValue dualValue = new DualValue(rootFieldLocation(), p1, p2);
recursiveComparisonConfiguration.ignoreFieldsMatchingRegexes(".*id");
// WHEN
Stopwatch stopwatch = Stopwatch.createStarted();
Set<String> fields = recursiveComparisonConfiguration.getActualChildrenNodeNamesToCompare(dualValue);
// THEN
then(stopwatch.elapsed()).isLessThan(Duration.ofSeconds(10));
then(fields).doesNotContain("id");
}

@Test
void should_return_all_fields_when_some_compared_types_are_specified_as_a_value_not_to_compare_could_have_a_field_to_compare() {
// GIVEN
Expand Down