diff --git a/core/src/main/java/com/google/common/truth/StreamSubject.java b/core/src/main/java/com/google/common/truth/StreamSubject.java index 45ed8f5f2..913a29d34 100644 --- a/core/src/main/java/com/google/common/truth/StreamSubject.java +++ b/core/src/main/java/com/google/common/truth/StreamSubject.java @@ -15,10 +15,12 @@ */ package com.google.common.truth; +import static com.google.common.base.Suppliers.memoize; +import static com.google.common.truth.Fact.fact; import static java.util.stream.Collectors.toCollection; +import com.google.common.base.Supplier; import com.google.errorprone.annotations.CanIgnoreReturnValue; -import com.google.errorprone.annotations.DoNotCall; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -28,35 +30,61 @@ /** * Propositions for {@link Stream} subjects. * - *
Note: the wrapped stream will be drained immediately into a private collection to - * provide more readable failure messages. You should not use this class if you intend to leave the - * stream un-consumed or if the stream is very large or infinite. + *
Note: When you perform an assertion based on the contents of the stream, or when + * any assertion fails, the wrapped stream will be drained immediately into a private + * collection to provide more readable failure messages. This consumes the stream. Take care if you + * intend to leave the stream un-consumed or if the stream is very large or infinite. * - *
If you intend to make multiple assertions on the same stream of data you should instead first - * collect the contents of the stream into a collection, and then assert directly on that. + *
If you intend to make multiple assertions on the contents of the same stream, you should + * instead first collect the contents of the stream into a collection and then assert directly on + * that. * *
For very large or infinite streams you may want to first {@linkplain Stream#limit limit} the
* stream before asserting on it.
*
* @author Kurt Alfred Kluever
*/
-@SuppressWarnings({
- "deprecation", // TODO(b/134064106): design an alternative to no-arg check()
- "Java7ApiChecker", // used only from APIs with Java 8 in their signatures
-})
+@SuppressWarnings("Java7ApiChecker") // used only from APIs with Java 8 in their signatures
@IgnoreJRERequirement
public final class StreamSubject extends Subject {
+ // Storing the FailureMetadata instance is not usually advisable.
+ private final FailureMetadata metadata;
+ private final Stream> actual;
+ private final Supplier<@Nullable List>> listSupplier;
- private final List> actualList;
+ StreamSubject(
+ FailureMetadata metadata,
+ @Nullable Stream> actual,
+ Supplier<@Nullable List>> listSupplier) {
+ super(metadata, actual);
+ this.metadata = metadata;
+ this.actual = actual;
+ this.listSupplier = listSupplier;
+ }
- StreamSubject(FailureMetadata failureMetadata, @Nullable Stream> stream) {
- super(failureMetadata, stream);
- this.actualList = (stream == null) ? null : stream.collect(toCollection(ArrayList::new));
+ StreamSubject(FailureMetadata metadata, @Nullable Stream> actual) {
+ /*
+ * As discussed in the Javadoc, we're a *little* accommodating of streams that have already been
+ * collected (or are outright broken, like some mocks), and we avoid collecting the contents
+ * until we want them. So, if you want to perform an assertion like
+ * `assertThat(previousStream).isSameInstanceAs(firstStream)`, we'll let you do that, even if
+ * you've already collected the stream. This way, `assertThat(Stream)` works as well as
+ * `assertThat(Object)` for streams, following the usual rules of overloading. (This would also
+ * help if we someday make `assertThat(Object)` automatically delegate to `assertThat(Stream)`
+ * when passed a `Stream`.)
+ */
+ this(metadata, actual, memoize(listCollector(actual)));
}
@Override
protected String actualCustomStringRepresentation() {
- return String.valueOf(actualList);
+ List> asList;
+ try {
+ asList = listSupplier.get();
+ } catch (IllegalStateException e) {
+ return "Stream that has already been operated upon or closed: " + actual();
+ }
+ return String.valueOf(asList);
}
public static Subject.Factory when given a Stream...). And we have to call a special constructor to avoid
+ * re-collecting the stream.
+ */
+ new StreamSubject(
+ metadata.withMessage(
+ "%s",
+ new Object[] {
+ "Warning: Stream equality is based on object identity. To compare Stream"
+ + " contents, use methods like containsExactly."
+ }),
+ actual,
+ listSupplier)
+ .superIsEqualTo(expected);
+ }
+
+ private void superIsEqualTo(@Nullable Object expected) {
+ super.isEqualTo(expected);
}
/**
* @deprecated {@code streamA.isNotEqualTo(streamB)} always passes, except when passed the exact
- * same stream reference
+ * same stream reference. If you really want to test object identity, you can eliminate this
+ * deprecation warning by using {@link #isNotSameInstanceAs}. If you instead want to test the
+ * contents of the stream, collect both streams to lists and perform assertions like {@link
+ * IterableSubject#isNotEqualTo} on them. In some cases, you may be able to use {@link
+ * StreamSubject} assertions like {@link #doesNotContain}.
*/
@Override
- @DoNotCall(
- "StreamSubject.isNotEqualTo() is not supported because Streams do not have well-defined"
- + " equality semantics")
@Deprecated
public void isNotEqualTo(@Nullable Object unexpected) {
- throw new UnsupportedOperationException(
- "StreamSubject.isNotEqualTo() is not supported because Streams do not have well-defined"
- + " equality semantics");
+ if (actual() == unexpected) {
+ /*
+ * We override the supermethod's message: That method would ask for both
+ * `String.valueOf(stream)` (for `unexpected`) and `actualCustomStringRepresentation()` (for
+ * `actual()`). The two strings are almost certain to differ, since `valueOf` is normally
+ * based on identity and `actualCustomStringRepresentation()` is based on contents. That can
+ * lead to a confusing error message.
+ *
+ * We could include isEqualTo's warning about Stream's identity-based equality here, too. But
+ * it doesn't seem necessary: The people we really want to warn are the people whose
+ * assertions *pass*. And we've already attempted to do that with deprecation.
+ */
+ failWithoutActual(
+ fact("expected not to be", actualCustomStringRepresentationForPackageMembersToCall()));
+ return;
+ }
+ /*
+ * But, if the objects aren't identical, we delegate to the supermethod (which checks equals())
+ * just in case someone has decided to override Stream.equals in a strange way. (I haven't
+ * checked whether this comes up in Google's codebase. I hope that it doesn't.)
+ */
+ super.isNotEqualTo(unexpected);
}
// TODO(user): Do we want to support comparingElementsUsing() on StreamSubject?
+
+ // TODO: b/134064106 - Migrate off no-arg check (to a direct IterableSubject constructor call?)
+ @SuppressWarnings("deprecation")
+ private IterableSubject checkThatContentsList() {
+ return check().that(listSupplier.get());
+ }
+
+ private static Supplier<@Nullable List>> listCollector(@Nullable Stream> actual) {
+ return () -> actual == null ? null : actual.collect(toCollection(ArrayList::new));
+ }
}
diff --git a/extensions/java8/src/test/java/com/google/common/truth/StreamSubjectTest.java b/extensions/java8/src/test/java/com/google/common/truth/StreamSubjectTest.java
index 3efc4866c..8973e5077 100644
--- a/extensions/java8/src/test/java/com/google/common/truth/StreamSubjectTest.java
+++ b/extensions/java8/src/test/java/com/google/common/truth/StreamSubjectTest.java
@@ -15,12 +15,12 @@
*/
package com.google.common.truth;
+import static com.google.common.truth.ExpectFailure.assertThat;
import static com.google.common.truth.FailureAssertions.assertFailureKeys;
import static com.google.common.truth.FailureAssertions.assertFailureValue;
import static com.google.common.truth.StreamSubject.streams;
import static com.google.common.truth.Truth8.assertThat;
import static java.util.Arrays.asList;
-import static org.junit.Assert.assertThrows;
import static org.junit.Assert.fail;
import java.util.stream.Stream;
@@ -33,22 +33,78 @@
*
* @author Kurt Alfred Kluever
*/
+// TODO: b/113905249 - Move this and other tests from extensions to core
@RunWith(JUnit4.class)
public final class StreamSubjectTest {
- @SuppressWarnings({"DoNotCall", "deprecation"}) // test of a mistaken call
+ @SuppressWarnings("deprecation") // test of a possibly mistaken call
@Test
- public void testIsEqualTo() throws Exception {
+ public void testIsEqualToSameInstancePreviouslyConsumed() throws Exception {
Stream