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