1 /* 2 * Copyright (C) 2019 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.server.wm; 18 19 import android.graphics.Bitmap; 20 import android.os.Environment; 21 import android.util.Log; 22 23 import androidx.annotation.NonNull; 24 import androidx.annotation.Nullable; 25 26 import com.android.compatibility.common.util.BitmapUtils; 27 28 import org.junit.AssumptionViolatedException; 29 import org.junit.rules.TestRule; 30 import org.junit.runner.Description; 31 import org.junit.runners.model.Statement; 32 33 import java.io.File; 34 import java.io.FileOutputStream; 35 import java.util.HashMap; 36 import java.util.Map; 37 import java.util.regex.Pattern; 38 39 /** 40 * A {@code TestRule} that allows dumping data on test failure. 41 * 42 * <p>Note: when using other {@code TestRule}s, make sure to use a {@code RuleChain} to ensure it is 43 * applied outside of other rules that can fail a test (otherwise this rule may not know that the 44 * test failed). 45 * 46 * <p>To capture the output of this rule, add the following to AndroidTest.xml: 47 * 48 * <pre>{@code 49 * <!-- Collect output of DumpOnFailure --> 50 * <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> 51 * <option name="directory-keys" value="/sdcard/DumpOnFailure" /> 52 * <option name="collect-on-run-ended-only" value="true" /> 53 * <option name="clean-up" value="true" /> 54 * </metrics_collector> 55 * }</pre> 56 * 57 * <p>And the following to AndroidManifest.xml: 58 * 59 * <pre>{@code 60 * <!-- Enable writing output of DumpOnFailure to external storage --> 61 * <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> 62 * }</pre> 63 */ 64 public final class DumpOnFailure implements TestRule { 65 66 private static final String TAG = "DumpOnFailure"; 67 68 /** Regex pattern for strings that contain at least one whitespace character. */ 69 @NonNull 70 private static final Pattern PATTERN_WHITESPACE = Pattern.compile("(.*?)\\s(.*?)"); 71 72 /** The output directory where the data should be dumped. */ 73 @NonNull 74 private static final File OUT_DIR = 75 new File(Environment.getExternalStorageDirectory(), "DumpOnFailure"); 76 77 /** 78 * Map of data to be dumped on test failure. The key must contain the name, followed by 79 * the file extension type. 80 */ 81 @NonNull 82 private final Map<String, DumpItem<?>> mDumpOnFailureItems = new HashMap<>(); 83 84 @Override apply(Statement base, Description description)85 public Statement apply(Statement base, Description description) { 86 return new Statement() { 87 @Override 88 public void evaluate() throws Throwable { 89 onTestSetup(); 90 try { 91 base.evaluate(); 92 } catch (AssumptionViolatedException t) { 93 throw t; 94 } catch (Throwable t) { 95 onTestFailure(description); 96 throw t; 97 } finally { 98 onTestTeardown(); 99 } 100 } 101 }; 102 } 103 104 private void onTestSetup() { 105 mDumpOnFailureItems.clear(); 106 } 107 108 private void onTestTeardown() { 109 mDumpOnFailureItems.clear(); 110 // Files are cleaned up through FilePullerLogCollector. 111 } 112 113 private void onTestFailure(@NonNull Description description) { 114 if (!OUT_DIR.exists() && !OUT_DIR.mkdirs()) { 115 Log.e(TAG, "onTestFailure, unable to create directory: " + OUT_DIR); 116 return; 117 } 118 119 for (var entry : mDumpOnFailureItems.entrySet()) { 120 final var fileName = getFilename(description, entry.getKey()); 121 Log.i(TAG, "Dumping " + OUT_DIR + "/" + fileName); 122 entry.getValue().writeToFile(OUT_DIR, fileName); 123 } 124 } 125 126 /** 127 * Gets the complete file name for the file to dump the data in. This includes the Test Suite, 128 * Test Method and given unique dump item name. 129 * 130 * @param description the test description. 131 * @param nameAndExtension the unique dump item name, followed by the file extension. 132 */ 133 @NonNull 134 private static String getFilename(@NonNull Description description, 135 @NonNull String nameAndExtension) { 136 return description.getTestClass().getSimpleName() + "_" + description.getMethodName() 137 + "__" + nameAndExtension; 138 } 139 140 /** 141 * Dumps the Bitmap if the test fails. 142 * 143 * @param name unique identifier (e.g. assertWindowShown). 144 * @param bitmap information to dump (e.g. screenshot). 145 */ 146 public void dumpOnFailure(@NonNull String name, @Nullable Bitmap bitmap) { 147 if (bitmap == null) { 148 Log.i(TAG, "dumpOnFailure cannot dump null bitmap"); 149 return; 150 } 151 if (containsWhitespace(name)) { 152 Log.i(TAG, "dumpOnFailure name cannot contain whitespaces"); 153 return; 154 } 155 156 mDumpOnFailureItems.put(getNextAvailableKey(name, "png"), new BitmapItem(bitmap)); 157 } 158 159 /** 160 * Dumps the String if the test fails. 161 * 162 * @param name unique identifier (e.g. assertWindowShown). 163 * @param string information to dump (e.g. logs). 164 */ 165 public void dumpOnFailure(@NonNull String name, @Nullable String string) { 166 if (string == null) { 167 Log.i(TAG, "dumpOnFailure cannot dump null string"); 168 return; 169 } 170 if (containsWhitespace(name)) { 171 Log.i(TAG, "dumpOnFailure name cannot contain whitespaces"); 172 return; 173 } 174 175 mDumpOnFailureItems.put(getNextAvailableKey(name, "txt"), new StringItem(string)); 176 } 177 178 /** 179 * Gets the next available key in the hashmap for the given name and file extension. 180 * If the hashmap already contains an entry with the given name-extension pair, this appends 181 * the next consecutive integer that is not used for that key. 182 * 183 * @param name the name to get the key for. 184 * @param extension the name of the file extension. 185 */ 186 @NonNull 187 private String getNextAvailableKey(@NonNull String name, @NonNull String extension) { 188 if (!mDumpOnFailureItems.containsKey(name + "." + extension)) { 189 return name + "." + extension; 190 } 191 192 int i = 1; 193 while (mDumpOnFailureItems.containsKey(name + "_" + i + "." + extension)) { 194 i++; 195 } 196 return name + "_" + i + "." + extension; 197 } 198 199 /** 200 * Whether the given string contains any whitespace characters. 201 * 202 * @param string the string to check. 203 */ 204 private static boolean containsWhitespace(@NonNull String string) { 205 return PATTERN_WHITESPACE.matcher(string).matches(); 206 } 207 208 /** Generic item containing data to be dumped on test failure. */ 209 private abstract static class DumpItem<T> { 210 211 /** The data to be dumped. */ 212 @NonNull 213 protected final T mData; 214 215 private DumpItem(@NonNull T data) { 216 mData = data; 217 } 218 219 /** 220 * Writes the given data to a file created in the given directory, with the given filename. 221 * 222 * @param directory the directory where the file should be created. 223 * @param fileName the name of the file to be created. 224 */ 225 abstract void writeToFile(@NonNull File directory, @NonNull String fileName); 226 } 227 228 private static final class BitmapItem extends DumpItem<Bitmap> { 229 230 BitmapItem(@NonNull Bitmap bitmap) { 231 super(bitmap); 232 } 233 234 @Override 235 void writeToFile(@NonNull File directory, @NonNull String fileName) { 236 BitmapUtils.saveBitmap(mData, directory.getPath(), fileName); 237 } 238 } 239 240 private static final class StringItem extends DumpItem<String> { 241 242 StringItem(@NonNull String string) { 243 super(string); 244 } 245 246 @Override 247 void writeToFile(@NonNull File directory, @NonNull String fileName) { 248 Log.i(TAG, "Writing to file: " + fileName + " in directory: " + directory); 249 250 final var file = new File(directory, fileName); 251 try (var fileStream = new FileOutputStream(file)) { 252 fileStream.write(mData.getBytes()); 253 fileStream.flush(); 254 } catch (Exception e) { 255 Log.e(TAG, "Writing to file failed", e); 256 } 257 } 258 } 259 } 260