• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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