• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2011 Google, Inc.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.google.common.truth;
17 
18 import static com.google.common.base.Preconditions.checkNotNull;
19 import static com.google.common.base.Preconditions.checkState;
20 import static com.google.common.base.Strings.padStart;
21 import static com.google.common.base.Strings.repeat;
22 import static com.google.common.base.Throwables.getStackTraceAsString;
23 import static com.google.common.truth.Expect.TestPhase.AFTER;
24 import static com.google.common.truth.Expect.TestPhase.BEFORE;
25 import static com.google.common.truth.Expect.TestPhase.DURING;
26 
27 import com.google.common.annotations.GwtIncompatible;
28 import com.google.common.base.Throwables;
29 import com.google.common.truth.Truth.SimpleAssertionError;
30 import com.google.errorprone.annotations.concurrent.GuardedBy;
31 import java.util.ArrayList;
32 import java.util.List;
33 import org.jspecify.annotations.Nullable;
34 import org.junit.internal.AssumptionViolatedException;
35 import org.junit.rules.ErrorCollector;
36 import org.junit.rules.TestRule;
37 import org.junit.runner.Description;
38 import org.junit.runners.model.Statement;
39 
40 /**
41  * A {@link TestRule} that batches up all failures encountered during a test, and reports them all
42  * together at the end (similar to {@link ErrorCollector}). It is also useful for making assertions
43  * from other threads or from within callbacks whose exceptions would be swallowed or logged, rather
44  * than propagated out to fail the test. (<a href="https://joel-costigliola.github.io/assertj/"
45  * target="_top">AssertJ</a> has a similar feature called "soft assertions"; however, soft
46  * assertions are not safe for concurrent use.)
47  *
48  * <p>Usage:
49  *
50  * <pre>
51  * {@code @Rule public final Expect expect = Expect.create();}
52  *
53  * {@code ...}
54  *
55  * {@code   expect.that(results).containsExactly(...);}
56  * {@code   expect.that(errors).isEmpty();}
57  * </pre>
58  *
59  * If both of the assertions above fail, the test will fail with an exception that contains
60  * information about both.
61  *
62  * <p>{@code Expect} may be used concurrently from multiple threads. However, multithreaded tests
63  * still require care:
64  *
65  * <ul>
66  *   <li>{@code Expect} has no way of knowing when all your other test threads are done. It simply
67  *       checks for failures when the main thread finishes executing the test method. Thus, you must
68  *       ensure that any background threads complete their assertions before then, or your test may
69  *       ignore their results.
70  *   <li>Assertion failures are not the only exceptions that may occur in other threads. For maximum
71  *       safety, multithreaded tests should check for such exceptions regardless of whether they use
72  *       {@code Expect}. (Typically, this means calling {@code get()} on any {@code Future} returned
73  *       by a method like {@code executor.submit(...)}. It might also include checking for
74  *       unexpected log messages
75  *       or reading metrics that count failures.) If your tests already check for exceptions from a
76  *       thread, then that will cover any exception from plain {@code assertThat}.
77  * </ul>
78  *
79  * <p>To record failures for the purpose of testing that an assertion fails when it should, see
80  * {@link ExpectFailure}.
81  *
82  * <p>For more on this class, see <a href="https://truth.dev/expect">the documentation page</a>.
83  */
84 @GwtIncompatible("JUnit4")
85 @J2ktIncompatible
86 public final class Expect extends StandardSubjectBuilder implements TestRule {
87 
88   private static final class ExpectationGatherer implements FailureStrategy {
89     @GuardedBy("this")
90     private final List<AssertionError> failures = new ArrayList<>();
91 
92     @GuardedBy("this")
93     private TestPhase inRuleContext = BEFORE;
94 
ExpectationGatherer()95     ExpectationGatherer() {}
96 
97     @Override
fail(AssertionError failure)98     public synchronized void fail(AssertionError failure) {
99       record(failure);
100     }
101 
enterRuleContext()102     synchronized void enterRuleContext() {
103       checkState(inRuleContext == BEFORE);
104       inRuleContext = DURING;
105     }
106 
leaveRuleContext(@ullable Throwable caught)107     synchronized void leaveRuleContext(@Nullable Throwable caught) throws Throwable {
108       try {
109         if (caught == null) {
110           doLeaveRuleContext();
111         } else {
112           doLeaveRuleContext(caught);
113         }
114         /*
115          * We'd like to check this even if an exception was thrown, but we don't want to override
116          * the "real" failure. TODO(cpovirk): Maybe attach as a suppressed exception once we require
117          * a newer version of Android.
118          */
119         checkState(inRuleContext == DURING);
120       } finally {
121         inRuleContext = AFTER;
122       }
123     }
124 
checkInRuleContext()125     synchronized void checkInRuleContext() {
126       doCheckInRuleContext(null);
127     }
128 
hasFailures()129     synchronized boolean hasFailures() {
130       return !failures.isEmpty();
131     }
132 
133     @Override
toString()134     public synchronized String toString() {
135       if (failures.isEmpty()) {
136         return "No expectation failed.";
137       }
138       int numFailures = failures.size();
139       StringBuilder message =
140           new StringBuilder()
141               .append(numFailures)
142               .append(numFailures > 1 ? " expectations" : " expectation")
143               .append(" failed:\n");
144       int countLength = String.valueOf(failures.size() + 1).length();
145       int count = 0;
146       for (AssertionError failure : failures) {
147         count++;
148         message.append("  ");
149         message.append(padStart(String.valueOf(count), countLength, ' '));
150         message.append(". ");
151         if (count == 1) {
152           appendIndented(countLength, message, getStackTraceAsString(failure));
153         } else {
154           appendIndented(
155               countLength,
156               message,
157               printSubsequentFailure(failures.get(0).getStackTrace(), failure));
158         }
159         message.append("\n");
160       }
161 
162       return message.toString();
163     }
164 
165     // String.repeat is not available under Java 8 and old versions of Android.
166     @SuppressWarnings({"StringsRepeat", "InlineMeInliner"})
appendIndented(int countLength, StringBuilder builder, String toAppend)167     private static void appendIndented(int countLength, StringBuilder builder, String toAppend) {
168       int indent = countLength + 4; // "  " and ". "
169       builder.append(toAppend.replace("\n", "\n" + repeat(" ", indent)));
170     }
171 
printSubsequentFailure( StackTraceElement[] baseTraceFrames, AssertionError toPrint)172     private String printSubsequentFailure(
173         StackTraceElement[] baseTraceFrames, AssertionError toPrint) {
174       Exception e = new RuntimeException("__EXCEPTION_MARKER__", toPrint);
175       e.setStackTrace(baseTraceFrames);
176       String s = Throwables.getStackTraceAsString(e);
177       // Force single line reluctant matching
178       return s.replaceFirst("(?s)^.*?__EXCEPTION_MARKER__.*?Caused by:\\s+", "");
179     }
180 
181     @GuardedBy("this")
doCheckInRuleContext(@ullable AssertionError failure)182     private void doCheckInRuleContext(@Nullable AssertionError failure) {
183       switch (inRuleContext) {
184         case BEFORE:
185           throw new IllegalStateException(
186               "assertion made on Expect instance, but it's not enabled as a @Rule.", failure);
187         case DURING:
188           return;
189         case AFTER:
190           throw new IllegalStateException(
191               "assertion made on Expect instance, but its @Rule has already completed. Maybe "
192                   + "you're making assertions from a background thread and not waiting for them to "
193                   + "complete, or maybe you've shared an Expect instance across multiple tests? "
194                   + "We're throwing this exception to warn you that your assertion would have been "
195                   + "ignored. However, this exception might not cause any test to fail, or it "
196                   + "might cause some subsequent test to fail rather than the test that caused the "
197                   + "problem.",
198               failure);
199       }
200       throw new AssertionError();
201     }
202 
203     @GuardedBy("this")
doLeaveRuleContext()204     private void doLeaveRuleContext() {
205       if (hasFailures()) {
206         throw SimpleAssertionError.createWithNoStack(this.toString());
207       }
208     }
209 
210     @GuardedBy("this")
doLeaveRuleContext(Throwable caught)211     private void doLeaveRuleContext(Throwable caught) throws Throwable {
212       if (hasFailures()) {
213         String message =
214             caught instanceof AssumptionViolatedException
215                 ? "Also, after those failures, an assumption was violated:"
216                 : "Also, after those failures, an exception was thrown:";
217         record(SimpleAssertionError.createWithNoStack(message, caught));
218         throw SimpleAssertionError.createWithNoStack(this.toString());
219       } else {
220         throw caught;
221       }
222     }
223 
224     @GuardedBy("this")
record(AssertionError failure)225     private void record(AssertionError failure) {
226       doCheckInRuleContext(failure);
227       failures.add(failure);
228     }
229   }
230 
231   private final ExpectationGatherer gatherer;
232 
233   /** Creates a new instance. */
create()234   public static Expect create() {
235     return new Expect(new ExpectationGatherer());
236   }
237 
Expect(ExpectationGatherer gatherer)238   private Expect(ExpectationGatherer gatherer) {
239     super(FailureMetadata.forFailureStrategy(gatherer));
240     this.gatherer = checkNotNull(gatherer);
241   }
242 
hasFailures()243   public boolean hasFailures() {
244     return gatherer.hasFailures();
245   }
246 
247   @Override
checkStatePreconditions()248   void checkStatePreconditions() {
249     gatherer.checkInRuleContext();
250   }
251 
252   @Override
apply(Statement base, Description description)253   public Statement apply(Statement base, Description description) {
254     checkNotNull(base);
255     checkNotNull(description);
256     return new Statement() {
257       @Override
258       public void evaluate() throws Throwable {
259         gatherer.enterRuleContext();
260         Throwable caught = null;
261         try {
262           base.evaluate();
263         } catch (Throwable t) {
264           caught = t;
265         } finally {
266           gatherer.leaveRuleContext(caught);
267         }
268       }
269     };
270   }
271 
272   enum TestPhase {
273     BEFORE,
274     DURING,
275     AFTER;
276   }
277 }
278