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