Skip to content

Commit

Permalink
[ISSUE 2629] Add indices in containsExactly()
Browse files Browse the repository at this point in the history
Introduces an IndexedDiff to keep track of the diff at an index. 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.
  • Loading branch information
Giovds committed Jun 7, 2022
1 parent 619570e commit c52ba37
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 19 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
57 changes: 57 additions & 0 deletions src/main/java/org/assertj/core/error/ShouldContainExactly.java
Expand Up @@ -14,9 +14,13 @@

import static org.assertj.core.util.IterableUtil.isNullOrEmpty;

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

import java.util.List;

/**
* Creates an error message indicating that an assertion that verifies a group of elements contains exactly a given set
* of values and nothing else failed, exactly meaning same elements in same order. A group of elements can be a
Expand Down Expand Up @@ -66,6 +70,36 @@ 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" +
Expand Down Expand Up @@ -112,6 +146,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(String.format(" (only showing the first %d mismatches)", Configuration.MAX_INDICES_FOR_PRINTING));
}
sb.append(":%n");
for (IndexedDiff diff : indexedDiffs) {
sb.append(String.format(" element at index %d: expected \"%s\" but was \"%s\"%n", diff.getIndex(), diff.getExpected(), diff.getActual()));
}
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
46 changes: 46 additions & 0 deletions src/main/java/org/assertj/core/internal/IndexedDiff.java
@@ -0,0 +1,46 @@
package org.assertj.core.internal;

/**
* The actual and expected elements at a given index.
* */
public class IndexedDiff {
private final Object actual;
private final Object expected;
private 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;
}

/**
* Get the actual value of the diff.
* @return {@link Object} containing the actual value of the diff.
*/
public Object getActual() {
return actual;
}

/**
* Get the expected value of the diff.
* @return {@link Object} containing the expected value of the diff.
*/
public Object getExpected() {
return expected;
}

/**
* Get the index the diff occurred at.
* @return {@code int} containing the index the diff occurred at.
*/
public int getIndex() {
return index;
}
}
38 changes: 30 additions & 8 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 @@ -103,6 +103,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 @@ -1127,19 +1128,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);
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;
}
}
}
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
Expand Up @@ -14,15 +14,22 @@

import static java.lang.String.format;
import static org.assertj.core.api.BDDAssertions.then;
import static org.assertj.core.error.ShouldContainExactly.elementsDifferAtIndex;
import static org.assertj.core.error.ShouldContainExactly.shouldContainExactly;
import static org.assertj.core.error.ShouldContainExactly.*;
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.Comparator;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.assertj.core.configuration.Configuration;
import org.assertj.core.description.TextDescription;
import org.assertj.core.internal.ComparatorBasedComparisonStrategy;
import org.assertj.core.internal.IndexedDiff;
import org.assertj.core.internal.StandardComparisonStrategy;
import org.assertj.core.util.CaseInsensitiveStringComparator;
import org.junit.jupiter.api.Test;

Expand All @@ -31,10 +38,13 @@ class ShouldContainExactly_create_Test {
private static final ComparatorBasedComparisonStrategy CASE_INSENSITIVE_COMPARISON_STRATEGY = new ComparatorBasedComparisonStrategy(CaseInsensitiveStringComparator.instance);

@Test
void should_display_full_expected_and_actual_sets_when_order_does_not_match() {
void should_display_full_expected_and_actual_sets_with_index_when_order_does_not_match() {
// GIVEN
ErrorMessageFactory factory = shouldContainExactly(list("Yoda", "Han", "Luke", "Anakin"), list("Yoda", "Luke", "Han", "Anakin"),
Collections.emptyList(), Collections.emptyList());
List<String> actual = list("Yoda", "Han", "Luke", "Anakin");
List<String> expected = list("Yoda", "Luke", "Han", "Anakin");
List<IndexedDiff> indexDifferences = list(new IndexedDiff(actual.get(1), expected.get(1), 1),
new IndexedDiff(actual.get(2), expected.get(2), 2));
ErrorMessageFactory factory = shouldContainExactlyWithIndexes(actual, expected, indexDifferences, StandardComparisonStrategy.instance());

// WHEN
final String message = factory.create(new TextDescription("Test"));
Expand All @@ -44,7 +54,34 @@ void should_display_full_expected_and_actual_sets_when_order_does_not_match() {
+ "Expecting actual:%n"
+ " [\"Yoda\", \"Han\", \"Luke\", \"Anakin\"]%n"
+ "to contain exactly (and in same order):%n"
+ " [\"Yoda\", \"Luke\", \"Han\", \"Anakin\"]%n"));
+ " [\"Yoda\", \"Luke\", \"Han\", \"Anakin\"]%n"
+ "but there were differences at these indexes:%n"
+ " element at index 1: expected \"Luke\" but was \"Han\"%n"
+ " element at index 2: expected \"Han\" but was \"Luke\"%n"));
}

@Test
void should_display_only_configured_max_amount_of_indices() {
// GIVEN
List<Integer> expected = IntStream.rangeClosed(0, Configuration.MAX_INDICES_FOR_PRINTING)
.boxed()
.collect(Collectors.toList());
List<Integer> actual = IntStream.rangeClosed(0, Configuration.MAX_INDICES_FOR_PRINTING)
.boxed()
.sorted(Comparator.reverseOrder())
.collect(Collectors.toList());
List<IndexedDiff> indexDifferences = new ArrayList<>();
for (int i = 0; i < actual.size(); i++) {
indexDifferences.add(new IndexedDiff(actual.get(i), expected.get(i), i));
}

ErrorMessageFactory factory = shouldContainExactlyWithIndexes(actual, expected, indexDifferences, StandardComparisonStrategy.instance());

// WHEN
final String message = factory.create(new TextDescription("Test"));

// THEN
then(message).contains(format("only showing the first %d mismatches", Configuration.MAX_INDICES_FOR_PRINTING));
}

@Test
Expand Down
Expand Up @@ -13,11 +13,13 @@
package org.assertj.core.internal.iterables;

import static java.util.Collections.emptyList;
import static java.util.stream.Collectors.*;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
import static org.assertj.core.api.Assertions.catchThrowable;
import static org.assertj.core.error.ShouldContainExactly.shouldContainExactly;
import static org.assertj.core.error.ShouldContainExactly.shouldContainExactlyWithIndexes;
import static org.assertj.core.internal.ErrorMessages.valuesToLookForIsNull;
import static org.assertj.core.internal.iterables.SinglyIterableFactory.createSinglyIterable;
import static org.assertj.core.test.ObjectArrays.emptyArray;
Expand All @@ -26,11 +28,16 @@
import static org.assertj.core.util.Arrays.asList;
import static org.assertj.core.util.FailureMessages.actualIsNull;
import static org.assertj.core.util.Lists.newArrayList;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.IntStream;

import org.assertj.core.api.AssertionInfo;
import org.assertj.core.configuration.Configuration;
import org.assertj.core.internal.IndexedDiff;
import org.assertj.core.internal.Iterables;
import org.assertj.core.internal.IterablesBaseTest;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -101,11 +108,12 @@ void should_fail_if_actual_does_not_contain_given_values_exactly() {
void should_fail_if_actual_contains_all_given_values_in_different_order() {
AssertionInfo info = someInfo();
Object[] expected = { "Luke", "Leia", "Yoda" };

ArrayList<IndexedDiff> indexDiffs = newArrayList(new IndexedDiff("Yoda", "Leia", 1),
new IndexedDiff("Leia", "Yoda", 2));
Throwable error = catchThrowable(() -> iterables.assertContainsExactly(info, actual, expected));

assertThat(error).isInstanceOf(AssertionError.class);
verify(failures).failure(info, shouldContainExactly(actual, newArrayList(expected), emptyList(), emptyList()));
verify(failures).failure(info, shouldContainExactlyWithIndexes(actual, newArrayList(expected), indexDiffs));
}

@Test
Expand Down Expand Up @@ -149,13 +157,15 @@ void should_fail_if_actual_does_not_contain_given_values_exactly_according_to_cu
void should_fail_if_actual_contains_all_given_values_in_different_order_according_to_custom_comparison_strategy() {
AssertionInfo info = someInfo();
Object[] expected = { "Luke", "Leia", "Yoda" };
ArrayList<IndexedDiff> indexDiffs = newArrayList(new IndexedDiff("Yoda", "Leia", 1),
new IndexedDiff("Leia", "Yoda", 2));

Throwable error = catchThrowable(() -> iterablesWithCaseInsensitiveComparisonStrategy.assertContainsExactly(info, actual,
expected));

assertThat(error).isInstanceOf(AssertionError.class);
verify(failures).failure(info, shouldContainExactly(actual, newArrayList(expected), emptyList(), emptyList(),
comparisonStrategy));
verify(failures).failure(info, shouldContainExactlyWithIndexes(actual, newArrayList(expected), indexDiffs,
comparisonStrategy));
}

@Test
Expand All @@ -173,4 +183,19 @@ void should_fail_if_actual_contains_all_given_values_but_size_differ_according_t
comparisonStrategy));
}

@Test
void should_fail_if_order_does_not_match_and_total_printed_indexes_should_be_equal_to_max_elements_for_printing() {
AssertionInfo info = someInfo();
int maxIndex = Configuration.MAX_INDICES_FOR_PRINTING - 1;
List<Integer> actual = IntStream.rangeClosed(0, Configuration.MAX_INDICES_FOR_PRINTING)
.boxed().collect(toList());
Collections.shuffle(actual);
Object[] expected = IntStream.rangeClosed(0, Configuration.MAX_INDICES_FOR_PRINTING).boxed().toArray();

Throwable error = catchThrowable(() -> iterables.assertContainsExactly(info, actual, expected));

assertThat(error).isInstanceOf(AssertionError.class)
.hasMessageContaining("index " + maxIndex)
.hasMessageNotContaining("index " + maxIndex + 1);
}
}

0 comments on commit c52ba37

Please sign in to comment.