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