diff --git a/src/main/java/org/assertj/core/configuration/Configuration.java b/src/main/java/org/assertj/core/configuration/Configuration.java index b3744ca608..e4012ea56d 100644 --- a/src/main/java/org/assertj/core/configuration/Configuration.java +++ b/src/main/java/org/assertj/core/configuration/Configuration.java @@ -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; diff --git a/src/main/java/org/assertj/core/error/ShouldContainExactly.java b/src/main/java/org/assertj/core/error/ShouldContainExactly.java index 5245b2709f..947a6795d8 100644 --- a/src/main/java/org/assertj/core/error/ShouldContainExactly.java +++ b/src/main/java/org/assertj/core/error/ShouldContainExactly.java @@ -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 @@ -40,6 +44,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); } @@ -63,6 +70,45 @@ public static ErrorMessageFactory shouldContainExactly(Object actual, Iterable expected, + List 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 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" + @@ -100,6 +146,29 @@ private ShouldContainExactly(Object actual, Object expected, ComparisonStrategy actual, expected, unexpected, comparisonStrategy); } + private ShouldContainExactly(Object actual, Object expected, List 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 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 {@link ShouldContainExactly} for the case where actual and expected have the same * elements in different order according to the given {@link ComparisonStrategy}. diff --git a/src/main/java/org/assertj/core/internal/IndexedDiff.java b/src/main/java/org/assertj/core/internal/IndexedDiff.java new file mode 100644 index 0000000000..d3bf9c1c74 --- /dev/null +++ b/src/main/java/org/assertj/core/internal/IndexedDiff.java @@ -0,0 +1,58 @@ +/* + * 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; + +/** + * 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; + } +} diff --git a/src/main/java/org/assertj/core/internal/Iterables.java b/src/main/java/org/assertj/core/internal/Iterables.java index dfc1236ac1..e7ccddeba1 100644 --- a/src/main/java/org/assertj/core/internal/Iterables.java +++ b/src/main/java/org/assertj/core/internal/Iterables.java @@ -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; @@ -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; @@ -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 actualAsList = newArrayList(actual); - // length check - if (actualAsList.size() != values.length) { - IterableDiff 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 actualAsList) { + IterableDiff 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 actualAsList) { + List indexDifferences = compareOrder(values, actualAsList); + if (!indexDifferences.isEmpty()) { + throw shouldContainExactlyWithIndexAssertionError(actual, values, indexDifferences, info); + } + } + + private List compareOrder(Object[] values, List actualAsList) { + List 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 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 indexedDiffs, AssertionInfo info) { + return failures.failure(info, shouldContainExactlyWithIndexes(actual, list(values), indexedDiffs, comparisonStrategy)); } private AssertionError shouldContainExactlyWithDiffAssertionError(IterableDiff diff, Iterable actual, diff --git a/src/test/java/org/assertj/core/error/ShouldContainExactly_create_Test.java b/src/test/java/org/assertj/core/error/ShouldContainExactly_create_Test.java index 2a4ab0eef6..b863e215c7 100644 --- a/src/test/java/org/assertj/core/error/ShouldContainExactly_create_Test.java +++ b/src/test/java/org/assertj/core/error/ShouldContainExactly_create_Test.java @@ -14,22 +14,99 @@ 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.shouldContainExactlyWithIndexes; +import static org.assertj.core.error.ShouldContainExactly.elementsDifferAtIndex; 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.test.CaseInsensitiveStringComparator; +import org.assertj.core.internal.IndexedDiff; +import org.assertj.core.internal.StandardComparisonStrategy; import org.junit.jupiter.api.Test; 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_with_index_when_order_does_not_match() { + // GIVEN + List actual = list("Yoda", "Han", "Luke", "Anakin"); + List expected = list("Yoda", "Luke", "Han", "Anakin"); + List 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")); + + // THEN + then(message).isEqualTo(format("[Test] %n" + + "Expecting actual:%n" + + " [\"Yoda\", \"Han\", \"Luke\", \"Anakin\"]%n" + + "to contain exactly (and in same order):%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 expected = IntStream.rangeClosed(0, Configuration.MAX_INDICES_FOR_PRINTING) + .boxed() + .collect(Collectors.toList()); + List actual = IntStream.rangeClosed(0, Configuration.MAX_INDICES_FOR_PRINTING) + .boxed() + .sorted(Comparator.reverseOrder()) + .collect(Collectors.toList()); + List 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 + void should_display_full_expected_and_actual_sets_with_missing_and_unexpected_elements() { + // GIVEN + ErrorMessageFactory factory = shouldContainExactly(list("Yoda", "Han", "Luke", "Anakin"), list("Yoda", "Han", "Anakin", "Anakin"), + list("Anakin"), list("Luke")); + + // WHEN + final String message = factory.create(new TextDescription("Test")); + + // THEN + then(message).isEqualTo(format("[Test] %n" + + "Expecting actual:%n" + + " [\"Yoda\", \"Han\", \"Luke\", \"Anakin\"]%n" + + "to contain exactly (and in same order):%n" + + " [\"Yoda\", \"Han\", \"Anakin\", \"Anakin\"]%n" + + "but some elements were not found:%n" + + " [\"Anakin\"]%n" + + "and others were not expected:%n" + + " [\"Luke\"]%n")); + } + @Test void should_display_missing_and_unexpected_elements() { // GIVEN diff --git a/src/test/java/org/assertj/core/internal/iterables/Iterables_assertContainsExactly_Test.java b/src/test/java/org/assertj/core/internal/iterables/Iterables_assertContainsExactly_Test.java index 171ec135f5..70c07cae9c 100644 --- a/src/test/java/org/assertj/core/internal/iterables/Iterables_assertContainsExactly_Test.java +++ b/src/test/java/org/assertj/core/internal/iterables/Iterables_assertContainsExactly_Test.java @@ -13,12 +13,13 @@ package org.assertj.core.internal.iterables; import static java.util.Collections.emptyList; +import static java.util.stream.Collectors.toList; 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.elementsDifferAtIndex; 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; @@ -29,9 +30,14 @@ import static org.assertj.core.util.Lists.newArrayList; import static org.mockito.Mockito.verify; +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; @@ -102,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 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, elementsDifferAtIndex("Yoda", "Leia", 1)); + verify(failures).failure(info, shouldContainExactlyWithIndexes(actual, newArrayList(expected), indexDiffs)); } @Test @@ -150,12 +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 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, elementsDifferAtIndex("Yoda", "Leia", 1, comparisonStrategy)); + verify(failures).failure(info, shouldContainExactlyWithIndexes(actual, newArrayList(expected), indexDiffs, + comparisonStrategy)); } @Test @@ -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 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); + } }