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

[ISSUE 2629] Improve readability of containsExactly #2638

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
69 changes: 69 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 All @@ -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);
}
Expand All @@ -63,6 +70,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 +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
58 changes: 58 additions & 0 deletions 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;
}
}
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
Expand Up @@ -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<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"));

// 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<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
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
Expand Down