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