diff --git a/core/src/main/java/com/google/common/truth/Expect.java b/core/src/main/java/com/google/common/truth/Expect.java index 6bf5d02f9..534ba64ba 100644 --- a/core/src/main/java/com/google/common/truth/Expect.java +++ b/core/src/main/java/com/google/common/truth/Expect.java @@ -23,7 +23,8 @@ import static com.google.common.truth.Expect.TestPhase.DURING; import com.google.common.annotations.GwtIncompatible; -import com.google.common.truth.Truth.AssertionErrorWithCause; +import com.google.common.base.Throwables; +import com.google.common.truth.Truth.SimpleAssertionError; import com.google.errorprone.annotations.concurrent.GuardedBy; import java.util.ArrayList; import java.util.List; @@ -116,6 +117,9 @@ synchronized boolean hasFailures() { @Override public synchronized String toString() { + if (failures.isEmpty()) { + return "No expectation failed."; + } int numFailures = failures.size(); StringBuilder message = new StringBuilder( @@ -126,13 +130,29 @@ public synchronized String toString() { message.append(" "); message.append(count); message.append(". "); - message.append(showStackTrace ? getStackTraceAsString(failure) : failure.getMessage()); + if (count == 1) { + message.append(showStackTrace ? getStackTraceAsString(failure) : failure.getMessage()); + } else { + message.append( + showStackTrace + ? printSubsequentFailure(failures.get(0).getStackTrace(), failure) + : failure.getMessage()); + } message.append("\n"); } return message.toString(); } + private String printSubsequentFailure( + StackTraceElement[] baseTraceFrames, AssertionError toPrint) { + Exception e = new RuntimeException("__EXCEPTION_MARKER__", toPrint); + e.setStackTrace(baseTraceFrames); + String s = Throwables.getStackTraceAsString(e); + // Force single line reluctant matching + return s.replaceFirst("(?s)^.*?__EXCEPTION_MARKER__.*?Caused by:\\s+", ""); + } + @GuardedBy("this") private void doCheckInRuleContext(@Nullable AssertionError failure) { switch (inRuleContext) { @@ -158,7 +178,7 @@ private void doCheckInRuleContext(@Nullable AssertionError failure) { @GuardedBy("this") private void doLeaveRuleContext() { if (hasFailures()) { - throw new AssertionError(this); + throw SimpleAssertionError.createWithNoStack(this.toString()); } } @@ -169,8 +189,8 @@ private void doLeaveRuleContext(Throwable caught) throws Throwable { caught instanceof AssumptionViolatedException ? "Failures occurred before an assumption was violated" : "Failures occurred before an exception was thrown while the test was running"; - record(new AssertionErrorWithCause(message + ": " + caught, caught)); - throw new AssertionError(this); + record(SimpleAssertionError.createWithNoStack(message + ": " + caught, caught)); + throw SimpleAssertionError.createWithNoStack(this.toString()); } else { throw caught; } diff --git a/core/src/main/java/com/google/common/truth/ExpectFailure.java b/core/src/main/java/com/google/common/truth/ExpectFailure.java index d09bbef82..836cd32d3 100644 --- a/core/src/main/java/com/google/common/truth/ExpectFailure.java +++ b/core/src/main/java/com/google/common/truth/ExpectFailure.java @@ -20,7 +20,7 @@ import static com.google.common.truth.StringUtil.format; import com.google.common.annotations.GwtIncompatible; -import com.google.common.truth.Truth.AssertionErrorWithCause; +import com.google.common.truth.Truth.SimpleAssertionError; import javax.annotation.Nullable; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -89,7 +89,7 @@ public ExpectFailure() {} public StandardSubjectBuilder whenTesting() { checkState(inRuleContext, "ExpectFailure must be used as a JUnit @Rule"); if (failure != null) { - throw new AssertionErrorWithCause("ExpectFailure already captured a failure", failure); + throw SimpleAssertionError.create("ExpectFailure already captured a failure", failure); } if (failureExpected) { throw new AssertionError( diff --git a/core/src/main/java/com/google/common/truth/FailureMetadata.java b/core/src/main/java/com/google/common/truth/FailureMetadata.java index c79ab6e4e..157a45330 100644 --- a/core/src/main/java/com/google/common/truth/FailureMetadata.java +++ b/core/src/main/java/com/google/common/truth/FailureMetadata.java @@ -23,7 +23,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; -import com.google.common.truth.Truth.AssertionErrorWithCause; +import com.google.common.truth.Truth.SimpleAssertionError; import javax.annotation.Nullable; /** @@ -127,11 +127,11 @@ public String toString() { } void fail(String message) { - doFail(new AssertionErrorWithCause(addToMessage(message), rootCause())); + doFail(SimpleAssertionError.create(addToMessage(message), rootCause())); } void fail(String message, Throwable cause) { - doFail(new AssertionErrorWithCause(addToMessage(message), cause)); + doFail(SimpleAssertionError.create(addToMessage(message), cause)); // TODO(cpovirk): add rootCause() as a suppressed exception? } diff --git a/core/src/main/java/com/google/common/truth/Platform.java b/core/src/main/java/com/google/common/truth/Platform.java index d5ecc732d..46af19bdc 100644 --- a/core/src/main/java/com/google/common/truth/Platform.java +++ b/core/src/main/java/com/google/common/truth/Platform.java @@ -99,7 +99,7 @@ private static final class ComparisonFailureWithCause extends ComparisonFailure try { initCause(cause); } catch (IllegalStateException alreadyInitializedBecauseOfHarmonyBug) { - // See Truth.AssertionErrorWithCause. + // See Truth.SimpleAssertionError. } } diff --git a/core/src/main/java/com/google/common/truth/Truth.java b/core/src/main/java/com/google/common/truth/Truth.java index dcec64695..e161747e2 100644 --- a/core/src/main/java/com/google/common/truth/Truth.java +++ b/core/src/main/java/com/google/common/truth/Truth.java @@ -260,11 +260,15 @@ public static AtomicLongMapSubject assertThat(@Nullable AtomicLongMap actual) return assert_().that(actual); } - static final class AssertionErrorWithCause extends AssertionError { + /** + * An {@code AssertionError} that (a) always supports a cause, even under old versions of Android + * and (b) omits "java.lang.AssertionError:" from the beginning of its toString() representation. + */ + static final class SimpleAssertionError extends AssertionError { /** Separate cause field, in case initCause() fails. */ - private final Throwable cause; + @Nullable private final Throwable cause; - AssertionErrorWithCause(String message, Throwable cause) { + private SimpleAssertionError(String message, @Nullable Throwable cause) { super(message); this.cause = cause; @@ -278,6 +282,20 @@ static final class AssertionErrorWithCause extends AssertionError { } } + static SimpleAssertionError create(String message, Throwable cause) { + return new SimpleAssertionError(message, cause); + } + + static SimpleAssertionError createWithNoStack(String message, Throwable cause) { + SimpleAssertionError error = new SimpleAssertionError(message, cause); + error.setStackTrace(new StackTraceElement[0]); + return error; + } + + static SimpleAssertionError createWithNoStack(String message) { + return createWithNoStack(message, null); + } + @Override @SuppressWarnings("UnsynchronizedOverridesSynchronized") public Throwable getCause() { diff --git a/core/src/main/java/com/google/common/truth/super/com/google/common/truth/Platform.java b/core/src/main/java/com/google/common/truth/super/com/google/common/truth/Platform.java index 9426416ce..b00ec8cf5 100644 --- a/core/src/main/java/com/google/common/truth/super/com/google/common/truth/Platform.java +++ b/core/src/main/java/com/google/common/truth/super/com/google/common/truth/Platform.java @@ -20,7 +20,7 @@ import static java.lang.Float.parseFloat; import static jsinterop.annotations.JsPackage.GLOBAL; -import com.google.common.truth.Truth.AssertionErrorWithCause; +import com.google.common.truth.Truth.SimpleAssertionError; import jsinterop.annotations.JsProperty; import jsinterop.annotations.JsType; @@ -52,7 +52,7 @@ static boolean isInstanceOfType(Object instance, Class clazz) { static AssertionError comparisonFailure( String message, String expected, String actual, Throwable cause) { - return new AssertionErrorWithCause( + return SimpleAssertionError.create( format("%s expected:<[%s]> but was:<[%s]>", message, expected, actual), cause); } diff --git a/core/src/test/java/com/google/common/truth/ExpectWithStackTest.java b/core/src/test/java/com/google/common/truth/ExpectWithStackTest.java new file mode 100644 index 000000000..3b7bede6f --- /dev/null +++ b/core/src/test/java/com/google/common/truth/ExpectWithStackTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2018 Google, Inc. + * + * 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. + */ +package com.google.common.truth; + +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.common.base.Predicate; +import com.google.common.base.Predicates; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.junit.runners.model.Statement; + +@RunWith(JUnit4.class) +public class ExpectWithStackTest { + private final Expect expectWithTrace = Expect.createAndEnableStackTrace(); + + @Rule public final TestRuleVerifier verifyAssertionError = new TestRuleVerifier(expectWithTrace); + + @Test + public void testExpectTrace_simpleCase() { + verifyAssertionError.setErrorVerifier( + new Predicate() { + @Override + public boolean apply(AssertionError expected) { + assertThat(expected.getStackTrace()).hasLength(0); + assertThat(expected).hasMessageThat().startsWith("3 expectations failed:"); + return true; + } + }); + + expectWithTrace.that(true).isFalse(); + expectWithTrace.that("Hello").isNull(); + expectWithTrace.that(1).isEqualTo(2); + } + + @Test + public void testExpectTrace_loop() { + verifyAssertionError.setErrorVerifier( + new Predicate() { + @Override + public boolean apply(AssertionError expected) { + assertThat(expected.getStackTrace()).hasLength(0); + assertThat(expected).hasMessageThat().startsWith("4 expectations failed:"); + assertWithMessage("test method name should only show up once with following omitted") + .that(expected.getMessage().split("testExpectTrace_loop")) + .hasLength(2); + return true; + } + }); + + for (int i = 0; i < 4; i++) { + expectWithTrace.that(true).isFalse(); + } + } + + @Test + public void testExpectTrace_callerException() { + verifyAssertionError.setErrorVerifier( + new Predicate() { + @Override + public boolean apply(AssertionError expected) { + assertThat(expected.getStackTrace()).hasLength(0); + assertThat(expected).hasMessageThat().startsWith("2 expectations failed:"); + return true; + } + }); + + expectWithTrace.that(true).isFalse(); + expectWithTrace + .that(alwaysFailWithCause(getFirstException("First", getSecondException("Second", null)))) + .isEqualTo(5); + } + + @Test + public void testExpectTrace_onlyCallerException() { + verifyAssertionError.setErrorVerifier( + new Predicate() { + @Override + public boolean apply(AssertionError expected) { + assertWithMessage("Should throw exception as it is if only caller exception") + .that(expected.getStackTrace().length) + .isAtLeast(2); + return true; + } + }); + + expectWithTrace + .that(alwaysFailWithCause(getFirstException("First", getSecondException("Second", null)))) + .isEqualTo(5); + } + + private static long alwaysFailWithCause(Throwable throwable) { + throw new AssertionError("Always fail", throwable); + } + + private static Exception getFirstException(String messsage, Throwable cause) { + if (cause != null) { + return new RuntimeException(messsage, cause); + } else { + return new RuntimeException(messsage); + } + } + + private static Exception getSecondException(String messsage, Throwable cause) { + if (cause != null) { + return new RuntimeException(messsage, cause); + } else { + return new RuntimeException(messsage); + } + } + + private static class TestRuleVerifier implements TestRule { + protected Predicate errorVerifier = Predicates.alwaysFalse(); + + private final TestRule ruleToVerify; + + public TestRuleVerifier(TestRule ruleToVerify) { + this.ruleToVerify = ruleToVerify; + } + + public void setErrorVerifier(Predicate verifier) { + this.errorVerifier = verifier; + } + + @Override + public Statement apply(final Statement base, final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + try { + ruleToVerify.apply(base, description).evaluate(); + } catch (AssertionError caught) { + if (!errorVerifier.apply(caught)) { + throw new AssertionError("Caught error doesn't meet expectation", caught); + } + } + } + }; + } + } +} diff --git a/core/src/test/java/com/google/common/truth/StackTraceCleanerTest.java b/core/src/test/java/com/google/common/truth/StackTraceCleanerTest.java index 93d5aaa8b..c4be8df1f 100644 --- a/core/src/test/java/com/google/common/truth/StackTraceCleanerTest.java +++ b/core/src/test/java/com/google/common/truth/StackTraceCleanerTest.java @@ -64,7 +64,7 @@ public void collapseStreaks() { * Stripping doesn't actually work under j2cl (and presumably GWT): * StackTraceElement.getClassName() doesn't have real data. Some data is available in toString(), * albeit along the lines of - * "AssertionErrorWithCause.m_createError__java_lang_String_$pp_java_lang." StackTraceCleaner + * "SimpleAssertionError.m_createError__java_lang_String_$pp_java_lang." StackTraceCleaner * could maybe look through the toString() representations to count how many frames to strip, but * that's a bigger project. (While we're at it, we could strip the j2cl-specific boilerplate from * the _bottom_ of the stack, too.) And sadly, it's not necessarily as simple as looking at just