1 /*
2  * Copyright (C) 2022 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.platform.test.rule;
18 
19 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
20 
21 import android.os.ParcelFileDescriptor;
22 import android.os.Trace;
23 
24 import androidx.annotation.NonNull;
25 import androidx.test.InstrumentationRegistry;
26 import androidx.test.uiautomator.UiDevice;
27 
28 import org.junit.runner.Description;
29 
30 import java.io.BufferedOutputStream;
31 import java.io.File;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.OutputStream;
35 import java.util.zip.ZipEntry;
36 import java.util.zip.ZipOutputStream;
37 
38 /** Utilities for producing test artifacts. */
39 public class ArtifactSaver {
40     private static final String TAG = ArtifactSaver.class.getSimpleName();
41 
42     // Presubmit tests have a time limit. We are not taking expensive bugreports from presubmits.
43     private static boolean sShouldTakeBugreport = !PresubmitRule.runningInPresubmit();
44 
artifactFile(String fileName)45     public static File artifactFile(String fileName) {
46         return new File(
47                 InstrumentationRegistry.getInstrumentation().getTargetContext().getFilesDir(),
48                 fileName);
49     }
50 
51     /**
52      * @return a file to store an artifact for test described by description. Providing the same
53      *     prefix and ext will overwrite the same file.
54      */
artifactFile(Description description, String prefix, String ext)55     public static File artifactFile(Description description, String prefix, String ext) {
56         return artifactFile(
57                 "TestScreenshot-" + prefix + "-" + getClassAndMethodName(description) + "." + ext);
58     }
59 
getClassAndMethodName(Description description)60     private static String getClassAndMethodName(Description description) {
61         String suffix = description.getMethodName();
62         if (suffix == null) {
63             // Can happen when the description is from a ClassRule
64             suffix = "EntireClassExecution";
65         }
66         Class<?> testClass = description.getTestClass();
67 
68         // Can have null class if this is a synthetic suite
69         String className = testClass != null ? testClass.getSimpleName() : "SUITE";
70         return className + "." + suffix;
71     }
72 
onError(Description description, Throwable e)73     public static void onError(Description description, Throwable e) {
74         Trace.beginSection("ArtifactSaver.onError");
75         final UiDevice device = getUiDevice();
76         final File hierarchy = artifactFile(description, "Hierarchy", "zip");
77 
78         final File screenshot = takeDebugScreenshot(description, device, "OnFailure");
79 
80         // Dump accessibility hierarchy
81         try {
82             device.dumpWindowHierarchy(artifactFile(description, "AccessibilityHierarchy", "uix"));
83         } catch (Exception ex) {
84             android.util.Log.e(TAG, "Failed to save accessibility hierarchy", ex);
85         }
86 
87         // Dump window hierarchy
88         try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(hierarchy))) {
89             out.putNextEntry(new ZipEntry("bugreport.txt"));
90             dumpCommandAndOutput("dumpsys window windows", out);
91             dumpCommandAndOutput("dumpsys package", out);
92             out.closeEntry();
93 
94             out.putNextEntry(new ZipEntry("visible_windows.zip"));
95             dumpCommandOutput("cmd window dump-visible-window-views", out);
96             out.closeEntry();
97         } catch (IOException ex) {
98         }
99 
100         android.util.Log.e(
101                 TAG,
102                 "Failed test "
103                         + description.getMethodName()
104                         + ",\nscreenshot will be saved to "
105                         + screenshot
106                         + ",\nUI dump at: "
107                         + hierarchy
108                         + " (use go/web-hv to open the dump file)",
109                 e);
110 
111         // Dump bugreport
112         if (sShouldTakeBugreport && FailureWatcher.getSystemAnomalyMessage(device) != null) {
113             // Taking bugreport is expensive, we should do this only once.
114             sShouldTakeBugreport = false;
115             dumpCommandOutput("bugreportz -s", artifactFile(description, "Bugreport", "zip"));
116         }
117 
118         dumpCommandOutput(
119                 "dumpsys meminfo",
120                 artifactFile("MemInfo-OnFailure-" + getClassAndMethodName(description) + ".txt"));
121 
122         dumpCommandOutput(
123                 "cmd statusbar flag | tail +11", // Flags info starts at line 11
124                 artifactFile("Flags-OnFailure-" + getClassAndMethodName(description) + ".txt"));
125 
126         dumpCommandOutput(
127                 "dumpsys activity service SystemUI",
128                 artifactFile("SystemUI-OnFailure-" + getClassAndMethodName(description) + ".txt"));
129 
130         Trace.endSection();
131     }
132 
getUiDevice()133     private static UiDevice getUiDevice() {
134         return UiDevice.getInstance(getInstrumentation());
135     }
136 
137     @NonNull
takeDebugScreenshot(Description description, UiDevice device, String prefix)138     private static File takeDebugScreenshot(Description description, UiDevice device,
139             String prefix) {
140         final File screenshot = artifactFile(description, prefix, "png");
141         device.takeScreenshot(screenshot);
142         return screenshot;
143     }
144 
takeDebugScreenshot(Description description, String prefix)145     public static void takeDebugScreenshot(Description description, String prefix) {
146         File screenshotFile = takeDebugScreenshot(description, getUiDevice(), prefix);
147         android.util.Log.e(
148                 TAG,
149                 "Screenshot taken in test: "
150                         + description.getMethodName()
151                         + ",\nscreenshot will be saved to "
152                         + screenshotFile);
153     }
154 
dumpCommandAndOutput(String cmd, OutputStream out)155     private static void dumpCommandAndOutput(String cmd, OutputStream out) throws IOException {
156         out.write(("\n\n" + cmd + "\n").getBytes());
157         dumpCommandOutput(cmd, out);
158     }
159 
dumpCommandOutput(String cmd, File out)160     public static void dumpCommandOutput(String cmd, File out) {
161         try (BufferedOutputStream buffered = new BufferedOutputStream(new FileOutputStream(out))) {
162             dumpCommandOutput(cmd, buffered);
163         } catch (IOException ex) {
164         }
165     }
166 
dumpCommandOutput(String cmd, OutputStream out)167     private static void dumpCommandOutput(String cmd, OutputStream out) throws IOException {
168         try (ParcelFileDescriptor.AutoCloseInputStream in =
169                 new ParcelFileDescriptor.AutoCloseInputStream(
170                         InstrumentationRegistry.getInstrumentation()
171                                 .getUiAutomation()
172                                 .executeShellCommand(cmd))) {
173             android.os.FileUtils.copy(in, out);
174         }
175     }
176 }
177