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