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.util.Log;
21 
22 import com.android.compatibility.common.util.BitmapUtils;
23 
24 import org.junit.rules.TestRule;
25 import org.junit.runner.Description;
26 import org.junit.runners.model.Statement;
27 
28 import java.io.File;
29 import java.nio.file.Path;
30 import java.nio.file.Paths;
31 import java.util.HashMap;
32 import java.util.Map;
33 
34 /**
35  * A {@code TestRule} that allows dumping data on test failure.
36  *
37  * <p>Note: when using other {@code TestRule}s, make sure to use a {@code RuleChain} to ensure it
38  * is applied outside of other rules that can fail a test (otherwise this rule may not know that the
39  * test failed).
40  *
41  * <p>To capture the output of this rule, add the following to AndroidTest.xml:
42  * <pre>
43  *  <!-- Collect output of DumpOnFailure. -->
44  *  <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
45  *    <option name="directory-keys" value="/sdcard/DumpOnFailure" />
46  *    <option name="collect-on-run-ended-only" value="true" />
47  *  </metrics_collector>
48  * </pre>
49  * <p>And disable external storage isolation:
50  * <pre>
51  *  <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
52  *  <application ... android:requestLegacyExternalStorage="true" ... >
53  * </pre>
54  */
55 public class DumpOnFailure implements TestRule {
56 
57     private static final String TAG = "DumpOnFailure";
58 
59     private final Map<String, Bitmap> mDumpOnFailureBitmaps = new HashMap<>();
60 
61     @Override
apply(Statement base, Description description)62     public Statement apply(Statement base, Description description) {
63         return new Statement() {
64             @Override
65             public void evaluate() throws Throwable {
66                 onTestSetup(description);
67                 try {
68                     base.evaluate();
69                 } catch (Throwable t) {
70                     onTestFailure(description, t);
71                     throw t;
72                 } finally {
73                     onTestTeardown(description);
74                 }
75             }
76         };
77     }
78 
79     private void onTestSetup(Description description) {
80         cleanDir(getDumpRoot(description).toFile());
81         mDumpOnFailureBitmaps.clear();
82     }
83 
84     private void onTestTeardown(Description description) {
85         mDumpOnFailureBitmaps.clear();
86     }
87 
88     private void onTestFailure(Description description, Throwable t) {
89         Path root = getDumpRoot(description);
90         File rootFile = root.toFile();
91         if (!rootFile.exists() && !rootFile.mkdirs()) {
92             throw new RuntimeException("Unable to create " + root);
93         }
94 
95         for (Map.Entry<String, Bitmap> entry : mDumpOnFailureBitmaps.entrySet()) {
96             String fileName = getFilename(description, entry.getKey(), "png");
97             Log.i(TAG, "Dumping " + root + "/" + fileName);
98             BitmapUtils.saveBitmap(entry.getValue(), root.toString(), fileName);
99         }
100     }
101 
102     private String getFilename(Description description, String name, String extension) {
103         return description.getTestClass().getSimpleName() + "_" + description.getMethodName()
104                 + "__" + name + "." + extension;
105     }
106 
107     private Path getDumpRoot(Description description) {
108         return Paths.get("/sdcard/DumpOnFailure/", description.getClassName()
109                 + "_" + description.getMethodName());
110     }
111 
112     private void cleanDir(File dir) {
113         final File[] files = dir.listFiles();
114         if (files == null) {
115             return;
116         }
117         for (File file : files) {
118             if (!file.isDirectory()) {
119                 if (!file.delete()) {
120                     throw new RuntimeException("Unable to delete " + file);
121                 }
122             }
123         }
124     }
125 
126     /**
127      * Dumps the Bitmap if the test fails.
128      */
129     public void dumpOnFailure(String name, Bitmap bitmap) {
130         if (mDumpOnFailureBitmaps.containsKey(name)) {
131             int i = 1;
132             while (mDumpOnFailureBitmaps.containsKey(name + "_" + i)) {
133                 ++i;
134             }
135             name += "_" + i;
136         }
137         mDumpOnFailureBitmaps.put(name, bitmap);
138     }
139 }
140