1 // Copyright 2019 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.feed.library.common.testing; 6 7 import static com.google.common.truth.Fact.fact; 8 import static com.google.common.truth.Fact.simpleFact; 9 import static com.google.common.truth.Truth.assertAbout; 10 11 import com.google.common.truth.FailureMetadata; 12 import com.google.common.truth.Subject; 13 import com.google.common.truth.ThrowableSubject; 14 15 import org.chromium.chrome.browser.feed.library.common.testing.RunnableSubject.ThrowingRunnable; 16 17 /** A Truth subject for testing exceptions. */ 18 @SuppressWarnings("nullness") 19 public final class RunnableSubject extends Subject { 20 private static final Subject 21 .Factory<RunnableSubject, ThrowingRunnable> RUNNABLE_SUBJECT_FACTORY = 22 RunnableSubject::new; 23 24 /** Gets the subject factory of {@link RunnableSubject}. */ runnables()25 public static Subject.Factory<RunnableSubject, ThrowingRunnable> runnables() { 26 return RUNNABLE_SUBJECT_FACTORY; 27 } 28 29 private final ThrowingRunnable mActual; 30 RunnableSubject(FailureMetadata failureMetadata, ThrowingRunnable runnable)31 public RunnableSubject(FailureMetadata failureMetadata, ThrowingRunnable runnable) { 32 super(failureMetadata, runnable); 33 this.mActual = runnable; 34 } 35 36 /** 37 * Note: Doesn't work when both this method, and Truth.assertThat, are imported statically. 38 * There's a type inference clash. 39 */ assertThat(ThrowingRunnable runnable)40 public static RunnableSubject assertThat(ThrowingRunnable runnable) { 41 return assertAbout(RUNNABLE_SUBJECT_FACTORY).that(runnable); 42 } 43 44 /** 45 * Wraps a RunnableSubject around the given runnable. 46 * 47 * <p>Note: This disambiguated method only exists because just 'assertThat' doesn't work when 48 * both RunnableSubject.assertThat and Truth.assertThat are imported statically. There's a type 49 * inference clash. 50 */ assertThatRunnable(ThrowingRunnable runnable)51 public static RunnableSubject assertThatRunnable(ThrowingRunnable runnable) { 52 return assertAbout(RUNNABLE_SUBJECT_FACTORY).that(runnable); 53 } 54 55 @SuppressWarnings("unchecked") runAndCaptureOrFail(Class<E> clazz)56 private <E extends Throwable> E runAndCaptureOrFail(Class<E> clazz) { 57 if (mActual == null) { 58 failWithoutActual(fact("expected to throw", clazz.getName()), 59 simpleFact("but didn't run because it's null")); 60 return null; 61 } 62 63 try { 64 mActual.run(); 65 } catch (Throwable ex) { 66 if (!clazz.isInstance(ex)) { 67 check("thrownException()").that(ex).isInstanceOf(clazz); // fails 68 return null; 69 } 70 return (E) ex; 71 } 72 73 failWithoutActual(fact("expected to throw", clazz.getName()), 74 simpleFact("but ran to completion"), fact("runnable was", mActual)); 75 return null; 76 } 77 throwsAnExceptionOfType(Class<E> clazz)78 public <E extends Throwable> ThrowsThenClause<E> throwsAnExceptionOfType(Class<E> clazz) { 79 return new ThrowsThenClause<>(runAndCaptureOrFail(clazz), "thrownException()"); 80 } 81 82 /** 83 * Just a fluent class prompting you to type ".that" before asserting things about the 84 * exception. 85 */ 86 public class ThrowsThenClause<E extends Throwable> { 87 private final E mThrowable; 88 private final String mDescription; 89 ThrowsThenClause(E throwable, String description)90 private ThrowsThenClause(E throwable, String description) { 91 this.mThrowable = throwable; 92 this.mDescription = description; 93 } 94 that()95 public ThrowableSubject that() { 96 return check(mDescription).that(mThrowable); 97 } 98 causedByAnExceptionOfType(Class<C> clazz)99 public <C extends Throwable> ThrowsThenClause<C> causedByAnExceptionOfType(Class<C> clazz) { 100 if (!clazz.isInstance(mThrowable.getCause())) { 101 that().hasCauseThat().isInstanceOf(clazz); // fails 102 return null; 103 } 104 105 @SuppressWarnings("unchecked") 106 C cause = (C) mThrowable.getCause(); 107 108 return new ThrowsThenClause<>(cause, mDescription + ".getCause()"); 109 } 110 causedBy(Throwable cause)111 public void causedBy(Throwable cause) { 112 that().hasCauseThat().isSameInstanceAs(cause); 113 } 114 getCaught()115 public E getCaught() { 116 return mThrowable; 117 } 118 } 119 120 /** 121 * Runnable which is able to throw a {@link Throwable}. Used for subject in order to test that a 122 * block of code actually throws an appropriate exception. 123 */ run()124 public interface ThrowingRunnable { void run() throws Throwable; } 125 } 126