Skip to content

Commit

Permalink
Improve containsExactly error message
Browse files Browse the repository at this point in the history
If the lists are of the same length and contain the same elements, but are in not in the same order, print the indices - limited to 50 - where a mismatch occurred.
Fix 2629
  • Loading branch information
Giovds authored and joel-costigliola committed Jul 15, 2022
1 parent a4a615e commit 41179e5
Show file tree
Hide file tree
Showing 6 changed files with 371 additions and 135 deletions.
Expand Up @@ -39,6 +39,7 @@ public class Configuration {
// default values
public static final int MAX_LENGTH_FOR_SINGLE_LINE_DESCRIPTION = 80;
public static final int MAX_ELEMENTS_FOR_PRINTING = 1000;
public static final int MAX_INDICES_FOR_PRINTING = 50;
public static final boolean REMOVE_ASSERTJ_RELATED_ELEMENTS_FROM_STACK_TRACE = true;
public static final boolean ALLOW_COMPARING_PRIVATE_FIELDS = true;
public static final boolean ALLOW_EXTRACTING_PRIVATE_FIELDS = true;
Expand Down
70 changes: 70 additions & 0 deletions src/main/java/org/assertj/core/error/ShouldContainExactly.java
Expand Up @@ -12,9 +12,14 @@
*/
package org.assertj.core.error;

import static java.lang.String.format;
import static org.assertj.core.util.IterableUtil.isNullOrEmpty;

import java.util.List;

import org.assertj.core.configuration.Configuration;
import org.assertj.core.internal.ComparisonStrategy;
import org.assertj.core.internal.IndexedDiff;
import org.assertj.core.internal.StandardComparisonStrategy;

/**
Expand All @@ -40,6 +45,9 @@ public class ShouldContainExactly extends BasicErrorMessageFactory {
public static ErrorMessageFactory shouldContainExactly(Object actual, Iterable<?> expected,
Iterable<?> notFound, Iterable<?> notExpected,
ComparisonStrategy comparisonStrategy) {
if (isNullOrEmpty(notExpected) && isNullOrEmpty(notFound)) {
return new ShouldContainExactly(actual, expected, comparisonStrategy);
}
if (isNullOrEmpty(notExpected)) {
return new ShouldContainExactly(actual, expected, notFound, comparisonStrategy);
}
Expand All @@ -63,6 +71,45 @@ public static ErrorMessageFactory shouldContainExactly(Object actual, Iterable<?
return shouldContainExactly(actual, expected, notFound, notExpected, StandardComparisonStrategy.instance());
}

/**
* Creates a new {@link ShouldContainExactly}.
*
* @param actual the actual value in the failed assertion.
* @param expected values expected to be contained in {@code actual}.
* @param indexDifferences the {@link IndexedDiff} the actual and expected differ at.
* @param comparisonStrategy the {@link ComparisonStrategy} used to evaluate assertion.
* @return the created {@code ErrorMessageFactory}.
*
*/
public static ErrorMessageFactory shouldContainExactlyWithIndexes(Object actual, Iterable<?> expected,
List<IndexedDiff> indexDifferences,
ComparisonStrategy comparisonStrategy) {
return new ShouldContainExactly(actual, expected, indexDifferences, comparisonStrategy);
}

/**
* Creates a new {@link ShouldContainExactly}.
*
* @param actual the actual value in the failed assertion.
* @param expected values expected to be contained in {@code actual}.
* @param indexDifferences the {@link IndexedDiff} the actual and expected differ at.
* @return the created {@code ErrorMessageFactory}.
*
*/
public static ErrorMessageFactory shouldContainExactlyWithIndexes(Object actual, Iterable<?> expected,
List<IndexedDiff> indexDifferences) {
return new ShouldContainExactly(actual, expected, indexDifferences, StandardComparisonStrategy.instance());
}

private ShouldContainExactly(Object actual, Object expected, ComparisonStrategy comparisonStrategy) {
super("%n" +
"Expecting actual:%n" +
" %s%n" +
"to contain exactly (and in same order):%n" +
" %s%n",
actual, expected, comparisonStrategy);
}

private ShouldContainExactly(Object actual, Object expected, Object notFound, Object notExpected,
ComparisonStrategy comparisonStrategy) {
super("%n" +
Expand Down Expand Up @@ -100,6 +147,29 @@ private ShouldContainExactly(Object actual, Object expected, ComparisonStrategy
actual, expected, unexpected, comparisonStrategy);
}

private ShouldContainExactly(Object actual, Object expected, List<IndexedDiff> indexDiffs,
ComparisonStrategy comparisonStrategy) {
super("%n" +
"Expecting actual:%n" +
" %s%n" +
"to contain exactly (and in same order):%n" +
" %s%n" +
formatIndexDifferences(indexDiffs), actual, expected, comparisonStrategy);
}

private static String formatIndexDifferences(List<IndexedDiff> indexedDiffs) {
StringBuilder sb = new StringBuilder();
sb.append("but there were differences at these indexes");
if (indexedDiffs.size() >= Configuration.MAX_INDICES_FOR_PRINTING) {
sb.append(format(" (only showing the first %d mismatches)", Configuration.MAX_INDICES_FOR_PRINTING));
}
sb.append(":%n");
for (IndexedDiff diff : indexedDiffs) {
sb.append(format(" - element at index %d: expected \"%s\" but was \"%s\"%n", diff.index, diff.expected, diff.actual));
}
return sb.toString();
}

/**
* Creates a new <code>{@link ShouldContainExactly}</code> for the case where actual and expected have the same
* elements in different order according to the given {@link ComparisonStrategy}.
Expand Down
55 changes: 55 additions & 0 deletions src/main/java/org/assertj/core/internal/IndexedDiff.java
@@ -0,0 +1,55 @@
/*
* 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-2022 the original author or authors.
*/
package org.assertj.core.internal;

import java.util.Objects;

/**
* Immutable class modeling the actual and expected elements at a given index.
*/
public class IndexedDiff {
public final Object actual;
public final Object expected;
public final int index;

/**
* Create a {@link IndexedDiff}.
* @param actual the actual value of the diff.
* @param expected the expected value of the diff.
* @param index the index the diff occurred at.
*/
public IndexedDiff(Object actual, Object expected, int index) {
this.actual = actual;
this.expected = expected;
this.index = index;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
IndexedDiff that = (IndexedDiff) o;
return index == that.index && Objects.equals(actual, that.actual) && Objects.equals(expected, that.expected);
}

@Override
public String toString() {
return String.format("IndexedDiff(actual=%s, expected=%s, index=%s)", this.actual, this.expected, this.index);
}

@Override
public int hashCode() {
return Objects.hash(actual, expected, index);
}

}
39 changes: 29 additions & 10 deletions src/main/java/org/assertj/core/internal/Iterables.java
Expand Up @@ -39,7 +39,7 @@
import static org.assertj.core.error.ShouldBeSubsetOf.shouldBeSubsetOf;
import static org.assertj.core.error.ShouldContain.shouldContain;
import static org.assertj.core.error.ShouldContainAnyOf.shouldContainAnyOf;
import static org.assertj.core.error.ShouldContainExactly.elementsDifferAtIndex;
import static org.assertj.core.error.ShouldContainExactly.shouldContainExactlyWithIndexes;
import static org.assertj.core.error.ShouldContainExactly.shouldContainExactly;
import static org.assertj.core.error.ShouldContainExactlyInAnyOrder.shouldContainExactlyInAnyOrder;
import static org.assertj.core.error.ShouldContainNull.shouldContainNull;
Expand Down Expand Up @@ -104,6 +104,7 @@

import org.assertj.core.api.AssertionInfo;
import org.assertj.core.api.Condition;
import org.assertj.core.configuration.Configuration;
import org.assertj.core.error.UnsatisfiedRequirement;
import org.assertj.core.error.ZippedElementsShouldSatisfy.ZipSatisfyError;
import org.assertj.core.presentation.PredicateDescription;
Expand Down Expand Up @@ -1128,22 +1129,40 @@ public void assertContainsExactly(AssertionInfo info, Iterable<?> actual, Object
assertNotNull(info, actual);
// use actualAsList instead of actual in case actual is a singly-passable iterable
List<Object> actualAsList = newArrayList(actual);
// length check
if (actualAsList.size() != values.length) {
IterableDiff<Object> diff = diff(actualAsList, asList(values), comparisonStrategy);
assertEquivalency(info, actual, values, actualAsList);
assertElementOrder(info, actual, values, actualAsList);
}

private void assertEquivalency(AssertionInfo info, Iterable<?> actual, Object[] values, List<Object> actualAsList) {
IterableDiff<Object> diff = diff(actualAsList, asList(values), comparisonStrategy);
if (actualAsList.size() != values.length || diff.differencesFound()) {
throw shouldContainExactlyWithDiffAssertionError(diff, actual, values, info);
}
// actual and values have the same number elements but are they equivalent and in the same order?
}

private void assertElementOrder(AssertionInfo info, Iterable<?> actual, Object[] values, List<Object> actualAsList) {
List<IndexedDiff> indexDifferences = compareOrder(values, actualAsList);
if (!indexDifferences.isEmpty()) {
throw shouldContainExactlyWithIndexAssertionError(actual, values, indexDifferences, info);
}
}

private List<IndexedDiff> compareOrder(Object[] values, List<Object> actualAsList) {
List<IndexedDiff> indexDifferences = new ArrayList<>(Configuration.MAX_INDICES_FOR_PRINTING);
for (int i = 0; i < actualAsList.size(); i++) {
// if the objects are not equal, begin the error handling process
if (!areEqual(actualAsList.get(i), values[i])) {
IterableDiff<Object> diff = diff(actualAsList, asList(values), comparisonStrategy);
if (diff.differencesFound()) {
throw shouldContainExactlyWithDiffAssertionError(diff, actual, values, info);
indexDifferences.add(new IndexedDiff(actualAsList.get(i), values[i], i));
if (indexDifferences.size() >= Configuration.MAX_INDICES_FOR_PRINTING) {
break;
}
throw failures.failure(info, elementsDifferAtIndex(actualAsList.get(i), values[i], i, comparisonStrategy));
}
}
return indexDifferences;
}

private AssertionError shouldContainExactlyWithIndexAssertionError(Iterable<?> actual, Object[] values,
List<IndexedDiff> indexedDiffs, AssertionInfo info) {
return failures.failure(info, shouldContainExactlyWithIndexes(actual, list(values), indexedDiffs, comparisonStrategy));
}

private AssertionError shouldContainExactlyWithDiffAssertionError(IterableDiff<Object> diff, Iterable<?> actual,
Expand Down

0 comments on commit 41179e5

Please sign in to comment.