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