1 /*
2  * Copyright (C) 2017 The Android Open Source Project
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 
17 package com.android.compatibility.common.util;
18 
19 import android.util.Log;
20 
21 import androidx.annotation.NonNull;
22 
23 import org.junit.AssumptionViolatedException;
24 import org.junit.rules.TestRule;
25 import org.junit.runner.Description;
26 import org.junit.runners.model.Statement;
27 
28 import java.io.IOException;
29 import java.io.PrintWriter;
30 import java.io.StringWriter;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.concurrent.Callable;
34 
35 /**
36  * Rule used to safely run clean up code after a test is finished, so that exceptions thrown by
37  * the cleanup code don't hide exception thrown by the test body
38  */
39 public final class SafeCleanerRule implements TestRule {
40 
41     private static final String TAG = "SafeCleanerRule";
42 
43     private final List<ThrowingRunnable> mCleaners = new ArrayList<>();
44     private final List<Callable<List<Throwable>>> mExtraThrowables = new ArrayList<>();
45     private final List<Throwable> mThrowables = new ArrayList<>();
46     private Dumper mDumper;
47 
48     /**
49      * Runs {@code cleaner} after the test is finished, catching any {@link Throwable} thrown by it.
50      */
run(@onNull ThrowingRunnable cleaner)51     public SafeCleanerRule run(@NonNull ThrowingRunnable cleaner) {
52         mCleaners.add(cleaner);
53         return this;
54     }
55 
56     /**
57      * Adds exceptions directly.
58      *
59      * <p>Typically used for exceptions caught asychronously during the test execution.
60      */
add(@onNull Callable<List<Throwable>> exceptions)61     public SafeCleanerRule add(@NonNull Callable<List<Throwable>> exceptions) {
62         mExtraThrowables.add(exceptions);
63         return this;
64     }
65 
66     /**
67      * Adds exceptions directly.
68      *
69      * <p>Typically used for exceptions caught during {@code finally} blocks.
70      */
add(Throwable exception)71     public SafeCleanerRule add(Throwable exception) {
72         Log.w(TAG, "Adding exception directly: " + exception);
73         mThrowables.add(exception);
74         return this;
75     }
76 
77     /**
78      * Sets a {@link Dumper} used to log errors.
79      */
setDumper(@onNull Dumper dumper)80     public SafeCleanerRule setDumper(@NonNull Dumper dumper) {
81         mDumper = dumper;
82         return this;
83     }
84 
85     @Override
apply(Statement base, Description description)86     public Statement apply(Statement base, Description description) {
87         return new Statement() {
88             @Override
89             public void evaluate() throws Throwable {
90                 // First run the test
91                 try {
92                     base.evaluate();
93                 } catch (Throwable t) {
94                     Log.w(TAG, "Adding exception from main test at index 0: " + t);
95                     mThrowables.add(0, t);
96                 }
97 
98                 // Then the cleanup runners
99                 for (ThrowingRunnable runner : mCleaners) {
100                     try {
101                         runner.run();
102                     } catch (Throwable t) {
103                         Log.w(TAG, "Adding exception from cleaner");
104                         mThrowables.add(t);
105                     }
106                 }
107 
108                 // And finally add the extra exceptions
109                 for (Callable<List<Throwable>> extraThrowablesCallable : mExtraThrowables) {
110                     final List<Throwable> extraThrowables = extraThrowablesCallable.call();
111                     if (extraThrowables != null && !extraThrowables.isEmpty()) {
112                         Log.w(TAG, "Adding " + extraThrowables.size() + " extra exceptions");
113                         mThrowables.addAll(extraThrowables);
114                     }
115                 }
116 
117                 // Ignore all instances of AssumptionViolatedExceptions
118                 mThrowables.removeIf(t -> t instanceof AssumptionViolatedException);
119 
120                 // Finally, throw up!
121                 if (mThrowables.isEmpty()) return;
122 
123                 final int numberExceptions = mThrowables.size();
124                 if (numberExceptions == 1) {
125                     fail(description, mThrowables.get(0));
126                 }
127                 fail(description, new MultipleExceptions(mThrowables));
128             }
129 
130         };
131     }
132 
133     private void fail(Description description, Throwable t) throws Throwable {
134         if (mDumper != null) {
135             mDumper.dump(description.getDisplayName(), t);
136         }
137         throw t;
138     }
139 
140     private static String toMesssage(List<Throwable> throwables) {
141         String msg = "D'OH!";
142         try {
143             try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {
144                 sw.write("Caught " + throwables.size() + " exceptions\n");
145                 for (int i = 0; i < throwables.size(); i++) {
146                     sw.write("\n---- Begin of exception #" + (i + 1) + " ----\n");
147                     final Throwable exception = throwables.get(i);
148                     exception.printStackTrace(pw);
149                     sw.write("---- End of exception #" + (i + 1) + " ----\n\n");
150                 }
151                 msg = sw.toString();
152             }
153         } catch (IOException e) {
154             // ignore close() errors - should not happen...
155             Log.e(TAG, "Exception closing StringWriter: " + e);
156         }
157         return msg;
158     }
159 
160     // VisibleForTesting
161     static class MultipleExceptions extends AssertionError {
162         private final List<Throwable> mThrowables;
163 
164         private MultipleExceptions(List<Throwable> throwables) {
165             super(toMesssage(throwables));
166 
167             this.mThrowables = throwables;
168         }
169 
170         List<Throwable> getThrowables() {
171             return mThrowables;
172         }
173     }
174 
175     /**
176      * Optional interface used to dump an error.
177      */
178     public interface Dumper {
179 
180         /**
181          * Dumps an error.
182          */
183         void dump(@NonNull String testName, @NonNull Throwable t);
184     }
185 }
186