Skip to content

Commit

Permalink
Disambiguate isEqualToComparingFieldByFieldRecursively error message.
Browse files Browse the repository at this point in the history
When two fields differ but had the same default representation, add the type and hashCode to the representation to highlight the field difference.
  • Loading branch information
joel-costigliola committed Mar 12, 2017
1 parent 048bb14 commit 9c5ebab
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 55 deletions.
12 changes: 2 additions & 10 deletions src/main/java/org/assertj/core/error/ShouldBeEqual.java
Expand Up @@ -12,8 +12,6 @@
*/
package org.assertj.core.error;

import static java.lang.Integer.toHexString;
import static java.lang.String.format;
import static org.assertj.core.util.Arrays.array;
import static org.assertj.core.util.Objects.HASH_CODE_PRIME;
import static org.assertj.core.util.Objects.areEqual;
Expand Down Expand Up @@ -188,18 +186,12 @@ private Object[] msgArgs(String description) {
return array(description, representation.toStringOf(expected), representation.toStringOf(actual));
}

private String detailedToStringOf(Object obj) {
return obj == null ? null
: format("%s (%s@%s)", representation.toStringOf(obj), obj.getClass().getSimpleName(),
toHexString(obj.hashCode()));
}

private String detailedActual() {
return detailedToStringOf(actual);
return representation.unambiguousToStringOf(actual);
}

private String detailedExpected() {
return detailedToStringOf(expected);
return representation.unambiguousToStringOf(expected);
}

@Override
Expand Down
Expand Up @@ -19,22 +19,22 @@
import java.util.List;

import org.assertj.core.internal.DeepDifference.Difference;
import org.assertj.core.presentation.Representation;
import org.assertj.core.util.Objects;

public class ShouldBeEqualByComparingFieldByFieldRecursively extends BasicErrorMessageFactory {

public static ErrorMessageFactory shouldBeEqualByComparingFieldByFieldRecursive(Object actual, Object other,
List<Difference> differences) {
List<Difference> differences,
Representation representation) {
List<String> descriptionOfDifferences = new ArrayList<>(differences.size());
for (Difference difference : differences) {
descriptionOfDifferences.add(format("%nPath to difference: <%s>%n" +
"- expected: <%s>%n" +
"- actual : <%s>",
join(difference.getPath()).with("."), difference.getOther(),
difference.getActual()));
descriptionOfDifferences.add(describeDifference(difference, representation));
}
return new ShouldBeEqualByComparingFieldByFieldRecursively("Expecting:%n" +
return new ShouldBeEqualByComparingFieldByFieldRecursively("%n" +
"Expecting:%n" +
" <%s>%n" +
"to be equal to:%n"+
"to be equal to:%n" +
" <%s>%n" +
"when recursively comparing field by field, but found the following difference(s):%n"
+ join(descriptionOfDifferences).with(format("%n")),
Expand All @@ -44,4 +44,28 @@ public static ErrorMessageFactory shouldBeEqualByComparingFieldByFieldRecursive(
private ShouldBeEqualByComparingFieldByFieldRecursively(String message, Object actual, Object other) {
super(message, actual, other);
}

private static String describeDifference(Difference difference, Representation representation) {

String actualFieldValue = representation.toStringOf(difference.getActual());
String otherFieldValue = representation.toStringOf(difference.getOther());

boolean sameRepresentation = Objects.areEqual(actualFieldValue, otherFieldValue);

String actualFieldValueRepresentation = sameRepresentation
? representation.unambiguousToStringOf(difference.getActual())
: actualFieldValue;

String otherFieldValueRepresentation = sameRepresentation
? representation.unambiguousToStringOf(difference.getOther())
: otherFieldValue;

return format("%nPath to difference: <%s>%n" +
"- expected: <%s>%n" +
"- actual : <%s>",
join(difference.getPath()).with("."),
otherFieldValueRepresentation,
actualFieldValueRepresentation);
}

}
19 changes: 10 additions & 9 deletions src/main/java/org/assertj/core/internal/Objects.java
Expand Up @@ -45,14 +45,6 @@
import static org.assertj.core.util.Preconditions.checkNotNull;
import static org.assertj.core.util.Sets.newLinkedHashSet;

import org.assertj.core.api.AssertionInfo;
import org.assertj.core.internal.DeepDifference.Difference;
import org.assertj.core.util.VisibleForTesting;
import org.assertj.core.util.introspection.FieldSupport;
import org.assertj.core.util.introspection.IntrospectionError;
import org.assertj.core.util.introspection.PropertyOrFieldSupport;
import org.assertj.core.util.introspection.PropertySupport;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
Expand All @@ -63,6 +55,14 @@
import java.util.Map;
import java.util.Set;

import org.assertj.core.api.AssertionInfo;
import org.assertj.core.internal.DeepDifference.Difference;
import org.assertj.core.util.VisibleForTesting;
import org.assertj.core.util.introspection.FieldSupport;
import org.assertj.core.util.introspection.IntrospectionError;
import org.assertj.core.util.introspection.PropertyOrFieldSupport;
import org.assertj.core.util.introspection.PropertySupport;

/**
* Reusable assertions for {@code Object}s.
*
Expand Down Expand Up @@ -705,7 +705,8 @@ public <A> void assertIsEqualToComparingFieldByFieldRecursively(AssertionInfo in
assertNotNull(info, actual);
List<Difference> differences = determineDifferences(actual, other, comparatorByPropertyOrField, comparatorByType);
if (!differences.isEmpty()) {
throw failures.failure(info, shouldBeEqualByComparingFieldByFieldRecursive(actual, other, differences));
throw failures.failure(info, shouldBeEqualByComparingFieldByFieldRecursive(actual, other, differences,
info.representation()));
}
}

Expand Down
13 changes: 11 additions & 2 deletions src/main/java/org/assertj/core/presentation/Representation.java
Expand Up @@ -20,12 +20,21 @@
public interface Representation {

/**
* Returns the {@code toString} representation of the given object. It may or not the object's own implementation of
* Returns the {@code String} representation of the given object. It may or not the object's own implementation of
* {@code toString}.
*
* @param object the given object.
* @param object the object to represent.
* @return the {@code toString} representation of the given object.
*/
String toStringOf(Object object);

/**
* Returns the {@code String} representation of the given object with its type and hexadecimal hash code so that
* it can be differentied from other objects with the same {@link #toStringOf(Object)} representation.
*
* @param object the object to represent.
* @return the {@code toString} representation of the given object.
*/
String unambiguousToStringOf(Object object);

}
Expand Up @@ -12,6 +12,7 @@
*/
package org.assertj.core.presentation;

import static java.lang.Integer.toHexString;
import static java.lang.reflect.Array.getLength;
import static org.assertj.core.util.Arrays.isArray;
import static org.assertj.core.util.Arrays.isArrayTypePrimitive;
Expand Down Expand Up @@ -117,9 +118,9 @@ public String toStringOf(Object object) {
if (object instanceof Calendar) return toStringOf((Calendar) object);
if (object instanceof Class<?>) return toStringOf((Class<?>) object);
if (object instanceof Date) return toStringOf((Date) object);
if (object instanceof AtomicBoolean) return toStringOf((AtomicBoolean)object);
if (object instanceof AtomicInteger) return toStringOf((AtomicInteger)object);
if (object instanceof AtomicLong) return toStringOf((AtomicLong)object);
if (object instanceof AtomicBoolean) return toStringOf((AtomicBoolean) object);
if (object instanceof AtomicInteger) return toStringOf((AtomicInteger) object);
if (object instanceof AtomicLong) return toStringOf((AtomicLong) object);
if (object instanceof AtomicReference) return toStringOf((AtomicReference<?>) object);
if (object instanceof AtomicMarkableReference) return toStringOf((AtomicMarkableReference<?>) object);
if (object instanceof AtomicStampedReference) return toStringOf((AtomicStampedReference<?>) object);
Expand All @@ -140,6 +141,12 @@ public String toStringOf(Object object) {
return object == null ? null : object.toString();
}

@Override
public String unambiguousToStringOf(Object obj) {
return obj == null ? null
: String.format("%s (%s@%s)", toStringOf(obj), obj.getClass().getSimpleName(), toHexString(obj.hashCode()));
}

protected String toStringOf(Number number) {
if (number instanceof Float) return toStringOf((Float) number);
if (number instanceof Long) return toStringOf((Long) number);
Expand All @@ -154,11 +161,11 @@ protected String toStringOf(AtomicBoolean atomicBoolean) {
protected String toStringOf(AtomicInteger atomicInteger) {
return String.format("AtomicInteger(%s)", atomicInteger.get());
}

protected String toStringOf(AtomicLong atomicLong) {
return String.format("AtomicLong(%s)", atomicLong.get());
}

protected String toStringOf(Comparator<?> comparator) {
if (!comparator.toString().contains("@")) return quote(comparator.toString());
String comparatorSimpleClassName = comparator.getClass().getSimpleName();
Expand Down Expand Up @@ -247,17 +254,17 @@ private String format(Map<?, ?> map, Object o) {
protected String toStringOf(AtomicReference<?> atomicReference) {
return String.format("AtomicReference[%s]", toStringOf(atomicReference.get()));
}

protected String toStringOf(AtomicMarkableReference<?> atomicMarkableReference) {
return String.format("AtomicMarkableReference[marked=%s, reference=%s]", atomicMarkableReference.isMarked(),
toStringOf(atomicMarkableReference.getReference()));
}

protected String toStringOf(AtomicStampedReference<?> atomicStampedReference) {
return String.format("AtomicStampedReference[stamp=%s, reference=%s]", atomicStampedReference.getStamp(),
toStringOf(atomicStampedReference.getReference()));
}

@Override
public String toString() {
return this.getClass().getSimpleName();
Expand Down
@@ -0,0 +1,100 @@
/**
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*
* Copyright 2012-2017 the original author or authors.
*/
package org.assertj.core.error;

import static java.lang.Integer.toHexString;
import static java.lang.String.format;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.error.ShouldBeEqualByComparingFieldByFieldRecursively.shouldBeEqualByComparingFieldByFieldRecursive;
import static org.assertj.core.presentation.StandardRepresentation.STANDARD_REPRESENTATION;

import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.TreeMap;
import java.util.TreeSet;

import org.assertj.core.description.TextDescription;
import org.assertj.core.internal.DeepDifference;
import org.assertj.core.internal.DeepDifference.Difference;
import org.assertj.core.internal.objects.Objects_assertIsEqualToComparingFieldByFieldRecursive_Test.WithCollection;
import org.assertj.core.internal.objects.Objects_assertIsEqualToComparingFieldByFieldRecursive_Test.WithMap;
import org.junit.Test;

public class ShouldBeEqualByComparingFieldByFieldRecursively_create_Test {

@Test
public void should_use_unambiguous_fields_description_when_standard_description_of_actual_and_expected_collection_fields_values_are_identical() {
// GIVEN
WithCollection<String> withHashSet = new WithCollection<>(new LinkedHashSet<String>());
WithCollection<String> withSortedSet = new WithCollection<>(new TreeSet<String>());
withHashSet.collection.add("bar");
withHashSet.collection.add("foo");
withSortedSet.collection.addAll(withHashSet.collection);
List<Difference> differences = DeepDifference.determineDifferences(withHashSet, withSortedSet, null, null);
// WHEN
// @format:off
String message = shouldBeEqualByComparingFieldByFieldRecursive(withSortedSet,
withHashSet,
differences,
STANDARD_REPRESENTATION)
.create(new TextDescription("Test"), STANDARD_REPRESENTATION);
// @format:on
// THEN
assertThat(message).isEqualTo(format("[Test] %n" +
"Expecting:%n" +
" <WithCollection [collection=[bar, foo]]>%n" +
"to be equal to:%n" +
" <WithCollection [collection=[bar, foo]]>%n" +
"when recursively comparing field by field, but found the following difference(s):%n"
+ "%n" +
"Path to difference: <collection>%n" +
"- expected: <[\"bar\", \"foo\"] (TreeSet@%s)>%n" +
"- actual : <[\"bar\", \"foo\"] (LinkedHashSet@%s)>",
toHexString(withSortedSet.collection.hashCode()),
toHexString(withHashSet.collection.hashCode())));
}

@Test
public void should_use_unambiguous_fields_description_when_standard_description_of_actual_and_expected_map_fields_values_are_identical() {
// GIVEN
WithMap<Long, Boolean> withLinkedHashMap = new WithMap<>(new LinkedHashMap<Long, Boolean>());
WithMap<Long, Boolean> withTreeMap = new WithMap<>(new TreeMap<Long, Boolean>());
withLinkedHashMap.map.put(1L, true);
withLinkedHashMap.map.put(2L, false);
withTreeMap.map.putAll(withLinkedHashMap.map);
List<Difference> differences = DeepDifference.determineDifferences(withLinkedHashMap, withTreeMap, null, null);
// WHEN
// @format:off
String message = shouldBeEqualByComparingFieldByFieldRecursive(withTreeMap,
withLinkedHashMap,
differences,
STANDARD_REPRESENTATION)
.create(new TextDescription("Test"), STANDARD_REPRESENTATION);
// @format:on
// THEN
assertThat(message).isEqualTo(format("[Test] %n" +
"Expecting:%n" +
" <WithMap [map={1=true, 2=false}]>%n" +
"to be equal to:%n" +
" <WithMap [map={1=true, 2=false}]>%n" +
"when recursively comparing field by field, but found the following difference(s):%n"
+ "%n" +
"Path to difference: <map>%n" +
"- expected: <{1L=true, 2L=false} (TreeMap@%s)>%n" +
"- actual : <{1L=true, 2L=false} (LinkedHashMap@%s)>",
toHexString(withTreeMap.map.hashCode()),
toHexString(withLinkedHashMap.map.hashCode())));
}

}

0 comments on commit 9c5ebab

Please sign in to comment.