Skip to content

Commit

Permalink
Start work on displayingDiffsPairedBy for Smart Diffs for Fuzzy Truth.
Browse files Browse the repository at this point in the history
This adds the methods and stores the specified values but doesn't do anything with them. Since the methods are useless, they're package visibility for now. (I figured it would be useful to review the APIs and the documentation separately from reviewing the implementation and the choice of failure message.)

RELNOTES=n/a

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=184553216
  • Loading branch information
peteg authored and ronshapiro committed Feb 6, 2018
1 parent 8e36eae commit e600383
Show file tree
Hide file tree
Showing 3 changed files with 408 additions and 0 deletions.
114 changes: 114 additions & 0 deletions core/src/main/java/com/google/common/truth/IterableSubject.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import static com.google.common.truth.SubjectUtils.retainMatchingToString;
import static java.util.Arrays.asList;

import com.google.common.base.Function;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.BiMap;
Expand Down Expand Up @@ -702,11 +703,108 @@ public static class UsingCorrespondence<A, E> {

private final IterableSubject subject;
private final Correspondence<? super A, ? super E> correspondence;
private final Optional<Pairer> pairer;

UsingCorrespondence(
IterableSubject subject, Correspondence<? super A, ? super E> correspondence) {
this.subject = checkNotNull(subject);
this.correspondence = checkNotNull(correspondence);
this.pairer = Optional.absent();
}

UsingCorrespondence(
IterableSubject subject,
Correspondence<? super A, ? super E> correspondence,
Pairer pairer) {
this.subject = checkNotNull(subject);
this.correspondence = checkNotNull(correspondence);
this.pairer = Optional.of(pairer);
}

/**
* Specifies a way to pair up unexpected and missing elements in the message when an assertion
* fails. For example:
*
* <pre>{@code
* assertThat(actualRecords)
* .comparingElementsUsing(RECORD_CORRESPONDENCE)
* .displayingDiffsPairedBy(Record::getId)
* .containsExactlyElementsIn(expectedRecords);
* }</pre>
*
* <p><b>Important</b>: The {code keyFunction} function must be able to accept both the actual
* and the unexpected elements, i.e. it must satisfy {@code Function<? super A, ? extends
* Object>} as well as {@code Function<? super E, ? extends Object>}. If that constraint is not
* met then a subsequent method may throw {@link ClassCastException}. Use the two-parameter
* overload if you need to specify different key functions for the actual and expected elements.
*
* <p>On assertions where it makes sense to do so, the elements are paired as follows: they are
* keyed by {@code keyFunction}, and if an unexpected element and a missing element have the
* same non-null key then the they are paired up. (Elements with null keys are not paired.) The
* failure message will show paired elements together, and a diff will be shown if the {@link
* Correspondence#formatDiff} method returns non-null.
*
* <p>The expected elements given in the assertion should be uniquely keyed by {@link
* keyFunction}. If multiple missing elements have the same key then the pairing will be
* skipped.
*
* <p>Useful key functions will have the property that key equality is less strict than the
* correspondence, i.e. given {@code actual} and {@code expected} values with keys {@code
* actualKey} and {@code expectedKey}, if {@code correspondence.compare(actual, expected)} is
* true then it is guaranteed that {@code actualKey} is equal to {@code expectedKey}, but there
* are cases where {@code actualKey} is equal to {@code expectedKey} but {@code
* correspondence.compare(actual, expected)} is false.
*
* <p>Note that calling this method makes no difference to whether a test passes or fails, it
* just improves the message if it fails.
*/
// TODO(b/32960783): Make this actually do something, and make it public.
/* public */ UsingCorrespondence<A, E> displayingDiffsPairedBy(
Function<? super E, ? extends Object> keyFunction) {
@SuppressWarnings("unchecked") // throwing ClassCastException is the correct behaviour
Function<? super A, ? extends Object> actualKeyFunction =
(Function<? super A, ? extends Object>) keyFunction;
return displayingDiffsPairedBy(actualKeyFunction, keyFunction);
}

/**
* Specifies a way to pair up unexpected and missing elements in the message when an assertion
* fails. For example:
*
* <pre>{@code
* assertThat(actualFoos)
* .comparingElementsUsing(FOO_BAR_CORRESPONDENCE)
* .displayingDiffsPairedBy(Foo::getId, Bar::getFooId)
* .containsExactlyElementsIn(expectedBar);
* }</pre>
*
* <p>On assertions where it makes sense to do so, the elements are paired as follows: the
* unexpected elements are keyed by {@code actualKeyFunction}, the missing elements are keyed by
* {@code expectedKeyFunction}, and if an unexpected element and a missing element have the same
* non-null key then the they are paired up. (Elements with null keys are not paired.) The
* failure message will show paired elements together, and a diff will be shown if the {@link
* Correspondence#formatDiff} method returns non-null.
*
* <p>The expected elements given in the assertion should be uniquely keyed by {@link
* expectedKeyFunction}. If multiple missing elements have the same key then the pairing will be
* skipped.
*
* <p>Useful key functions will have the property that key equality is less strict than the
* correspondence, i.e. given {@code actual} and {@code expected} values with keys {@code
* actualKey} and {@code expectedKey}, if {@code correspondence.compare(actual, expected)} is
* true then it is guaranteed that {@code actualKey} is equal to {@code expectedKey}, but there
* are cases where {@code actualKey} is equal to {@code expectedKey} but {@code
* correspondence.compare(actual, expected)} is false.
*
* <p>Note that calling this method makes no difference to whether a test passes or fails, it
* just improves the message if it fails.
*/
// TODO(b/32960783): Make this actually do something, and make it public.
/* public */ UsingCorrespondence<A, E> displayingDiffsPairedBy(
Function<? super A, ? extends Object> actualKeyFunction,
Function<? super E, ? extends Object> expectedKeyFunction) {
return new UsingCorrespondence<>(
subject, correspondence, new Pairer(actualKeyFunction, expectedKeyFunction));
}

/**
Expand Down Expand Up @@ -885,6 +983,7 @@ boolean failIfCandidateMappingHasMissingOrExtra(
*/
private Optional<String> describeMissingOrExtra(
List<? extends A> extra, List<? extends E> missing) {
// TODO(b/32960783): Use the pairer here, if present.
if (missing.size() == 1 && extra.size() == 1) {
@Nullable String diff = correspondence.formatDiff(extra.get(0), missing.get(0));
if (diff != null) {
Expand Down Expand Up @@ -1260,5 +1359,20 @@ private void containsNone(String excludedPrefix, Iterable<? extends E> excluded)
private Iterable<A> getCastActual() {
return (Iterable<A>) subject.actual();
}

/**
* A class which knows how to pair the actual and expected elements (see {@link
* #displayingDiffsPairedBy}).
*/
private final class Pairer {

private final Function<? super A, ?> actualKeyFunction;
private final Function<? super E, ?> expectedKeyFunction;

Pairer(Function<? super A, ?> actualKeyFunction, Function<? super E, ?> expectedKeyFunction) {
this.actualKeyFunction = actualKeyFunction;
this.expectedKeyFunction = expectedKeyFunction;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@
*/
package com.google.common.truth;

import static com.google.common.base.Functions.identity;
import static com.google.common.collect.Collections2.permutations;
import static com.google.common.truth.Correspondence.tolerance;
import static com.google.common.truth.TestCorrespondences.PARSED_RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10;
import static com.google.common.truth.TestCorrespondences.PARSED_RECORD_ID;
import static com.google.common.truth.TestCorrespondences.RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10;
import static com.google.common.truth.TestCorrespondences.RECORD_ID;
import static com.google.common.truth.TestCorrespondences.STRING_PARSES_TO_INTEGER_CORRESPONDENCE;
import static com.google.common.truth.TestCorrespondences.WITHIN_10_OF;
import static com.google.common.truth.Truth.assertThat;
import static java.util.Arrays.asList;
import static org.junit.Assert.fail;

import com.google.common.collect.ImmutableList;
import com.google.common.truth.TestCorrespondences.Record;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
Expand Down Expand Up @@ -292,6 +298,113 @@ public void comparingElementsUsing_containsExactlyElementsIn_diffOneMissingAndEx
+ "<90> and has unexpected elements <[101 (diff: 11)]>");
}

@Test
public void comparingElementsUsing_displayingDiffsPairedBy_1arg_containsExactlyElementsIn() {
ImmutableList<Record> expected =
ImmutableList.of(
Record.create(1, 100),
Record.create(2, 200),
Record.create(3, 300),
Record.createWithoutId(900));
ImmutableList<Record> actual =
ImmutableList.of(
Record.create(1, 100),
Record.create(2, 211),
Record.create(4, 400),
Record.createWithoutId(999));
expectFailure
.whenTesting()
.that(actual)
.comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
.displayingDiffsPairedBy(RECORD_ID)
.containsExactlyElementsIn(expected);
assertThat(expectFailure.getFailure())
.hasMessageThat()
.isEqualTo(
"Not true that <[1/100, 2/211, 4/400, none/999]> contains exactly one element that has "
+ "the same id as and a score is within 10 of each element of "
+ "<[1/100, 2/200, 3/300, none/900]>. It is missing an element that has the same "
+ "id as and a score is within 10 of each of <[2/200, 3/300, none/900]> and has "
+ "unexpected elements <[2/211, 4/400, none/999]>");
// TODO(b/32960783): Update expected message to show the diff between the records with key=2.
}

@Test
public void comparingElementsUsing_displayingDiffsPairedBy_2arg_containsExactlyElementsIn() {
ImmutableList<Record> expected =
ImmutableList.of(
Record.create(1, 100),
Record.create(2, 200),
Record.create(3, 300),
Record.createWithoutId(900));
ImmutableList<String> actual = ImmutableList.of("1/100", "2/211", "4/400", "none/999");
expectFailure
.whenTesting()
.that(actual)
.comparingElementsUsing(PARSED_RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
.displayingDiffsPairedBy(PARSED_RECORD_ID, RECORD_ID)
.containsExactlyElementsIn(expected);
assertThat(expectFailure.getFailure())
.hasMessageThat()
.isEqualTo(
"Not true that <[1/100, 2/211, 4/400, none/999]> contains exactly one element that "
+ "parses to a record that has the same id as and a score is within 10 of each "
+ "element of <[1/100, 2/200, 3/300, none/900]>. It is missing an element that "
+ "parses to a record that has the same id as and a score is within 10 of each of "
+ "<[2/200, 3/300, none/900]> and has unexpected elements "
+ "<[2/211, 4/400, none/999]>");
// TODO(b/32960783): Update expected message to show the diff between the records with key=2.
}

@Test
public void comparingElementsUsing_displayingDiffsPairedBy_containsExactlyElementsIn_passing() {
// The contract on displayingDiffsPairedBy requires that it should not affect whether the test
// passes or fails. This test asserts that a test which would pass on the basis of its
// correspondence still passes even if the user specifies a key function such that none of the
// elements match by key. (We advise against assertions where key function equality is stricter
// than correspondence, but we should still do the thing we promised we'd do in that case.)
ImmutableList<Double> expected = ImmutableList.of(1.0, 1.1, 1.2);
ImmutableList<Double> actual = ImmutableList.of(1.05, 1.15, 0.95);
assertThat(actual)
.comparingElementsUsing(tolerance(0.1))
.displayingDiffsPairedBy(identity())
.containsExactlyElementsIn(expected);
}

@Test
public void comparingElementsUsing_displayingDiffsPairedBy_containsExactlyElementsIn_notUnique() {
// The missing elements here are not uniquely keyed by the key function, so the key function
// should be ignored, but a warning about this should be appended to the failure message.
ImmutableList<Record> expected =
ImmutableList.of(
Record.create(1, 100),
Record.create(2, 200),
Record.create(3, 300),
Record.create(3, 301),
Record.createWithoutId(900));
ImmutableList<Record> actual =
ImmutableList.of(
Record.create(1, 100),
Record.create(2, 211),
Record.create(4, 400),
Record.createWithoutId(999));
expectFailure
.whenTesting()
.that(actual)
.comparingElementsUsing(RECORDS_EQUAL_WITH_SCORE_TOLERANCE_10)
.displayingDiffsPairedBy(RECORD_ID)
.containsExactlyElementsIn(expected);
assertThat(expectFailure.getFailure())
.hasMessageThat()
.isEqualTo(
"Not true that <[1/100, 2/211, 4/400, none/999]> contains exactly one element that has "
+ "the same id as and a score is within 10 of each element of "
+ "<[1/100, 2/200, 3/300, 3/301, none/900]>. It is missing an element that has the "
+ "same id as and a score is within 10 of each of <[2/200, 3/300, 3/301, none/900]>"
+ " and has unexpected elements <[2/211, 4/400, none/999]>");
// TODO(b/32960783): Update expected message to show the warning about non-uniqueness.
}

@Test
public void comparingElementsUsing_containsExactlyElementsIn_failsMissingElementInOneToOne() {
ImmutableList<Integer> expected = ImmutableList.of(64, 128, 256, 128);
Expand Down
Loading

0 comments on commit e600383

Please sign in to comment.