• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package com.android.launcher3.util.rule;
2 
3 import static androidx.test.InstrumentationRegistry.getInstrumentation;
4 
5 import android.os.FileUtils;
6 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
7 import android.util.Log;
8 
9 import androidx.annotation.NonNull;
10 import androidx.annotation.Nullable;
11 import androidx.test.uiautomator.UiDevice;
12 
13 import com.android.app.viewcapture.data.ExportedData;
14 import com.android.launcher3.tapl.LauncherInstrumentation;
15 import com.android.launcher3.ui.AbstractLauncherUiTest;
16 
17 import org.junit.rules.TestWatcher;
18 import org.junit.runner.Description;
19 import org.junit.runners.model.Statement;
20 
21 import java.io.BufferedOutputStream;
22 import java.io.File;
23 import java.io.FileOutputStream;
24 import java.io.IOException;
25 import java.io.OutputStream;
26 import java.util.function.Supplier;
27 import java.util.zip.ZipEntry;
28 import java.util.zip.ZipOutputStream;
29 
30 public class FailureWatcher extends TestWatcher {
31     private static final String TAG = "FailureWatcher";
32     private static boolean sSavedBugreport = false;
33     private static Description sDescriptionForLastSavedArtifacts;
34 
35     private final LauncherInstrumentation mLauncher;
36     @NonNull
37     private final Supplier<ExportedData> mViewCaptureDataSupplier;
38 
FailureWatcher(LauncherInstrumentation launcher, @NonNull Supplier<ExportedData> viewCaptureDataSupplier)39     public FailureWatcher(LauncherInstrumentation launcher,
40             @NonNull Supplier<ExportedData> viewCaptureDataSupplier) {
41         mLauncher = launcher;
42         mViewCaptureDataSupplier = viewCaptureDataSupplier;
43     }
44 
45     @Override
starting(Description description)46     protected void starting(Description description) {
47         mLauncher.setOnFailure(() -> onError(mLauncher, description, mViewCaptureDataSupplier));
48         super.starting(description);
49     }
50 
51     @Override
finished(Description description)52     protected void finished(Description description) {
53         super.finished(description);
54         mLauncher.setOnFailure(null);
55     }
56 
57     @Override
succeeded(Description description)58     protected void succeeded(Description description) {
59         super.succeeded(description);
60         AbstractLauncherUiTest.checkDetectedLeaks(mLauncher);
61     }
62 
63     @Override
apply(Statement base, Description description)64     public Statement apply(Statement base, Description description) {
65         return new Statement() {
66             @Override
67             public void evaluate() throws Throwable {
68                 try {
69                     FailureWatcher.super.apply(base, description).evaluate();
70                 } finally {
71                     // Detect touch events coming from physical screen.
72                     if (mLauncher.hadNontestEvents()) {
73                         throw new AssertionError(
74                                 "Launcher received events not sent by the test. This may mean "
75                                         + "that the touch screen of the lab device has sent false"
76                                         + " events. See the logcat for "
77                                         + "TaplEvents|LauncherEvents|TaplTarget tag and look for "
78                                         + "events with deviceId != -1");
79                     }
80                 }
81             }
82         };
83     }
84 
85     @Override
86     protected void failed(Throwable e, Description description) {
87         onError(mLauncher, description, mViewCaptureDataSupplier);
88     }
89 
90     static File diagFile(Description description, String prefix, String ext) {
91         return new File(getInstrumentation().getTargetContext().getFilesDir(),
92                 prefix + "-" + description.getTestClass().getSimpleName() + "."
93                         + description.getMethodName() + "." + ext);
94     }
95 
96     /** Action executed when an error condition is expected. Saves artifacts. */
97     public static void onError(LauncherInstrumentation launcher, Description description) {
98         onError(launcher, description, null);
99     }
100 
101     private static void onError(LauncherInstrumentation launcher, Description description,
102             @Nullable Supplier<ExportedData> viewCaptureDataSupplier) {
103         if (description.equals(sDescriptionForLastSavedArtifacts)) {
104             // This test has already saved its artifacts.
105             return;
106         }
107         sDescriptionForLastSavedArtifacts = description;
108 
109         final File sceenshot = diagFile(description, "TestScreenshot", "png");
110         final File hierarchy = diagFile(description, "Hierarchy", "zip");
111 
112         // Dump window hierarchy
113         try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(hierarchy))) {
114             out.putNextEntry(new ZipEntry("bugreport.txt"));
115             dumpStringCommand("dumpsys window windows", out);
116             dumpStringCommand("dumpsys package", out);
117             dumpStringCommand("dumpsys activity service TouchInteractionService", out);
118             out.closeEntry();
119 
120             out.putNextEntry(new ZipEntry("visible_windows.zip"));
121             dumpCommand("cmd window dump-visible-window-views", out);
122             out.closeEntry();
123 
124             if (viewCaptureDataSupplier != null) {
125                 out.putNextEntry(new ZipEntry("FS/data/misc/wmtrace/failed_test.vc"));
126                 final ExportedData exportedData = viewCaptureDataSupplier.get();
127                 if (exportedData != null) exportedData.writeTo(out);
128                 out.closeEntry();
129             }
130         } catch (Exception ignored) {
131         }
132 
133         Log.e(TAG, "Failed test " + description.getMethodName()
134                 + ",\nscreenshot will be saved to " + sceenshot
135                 + ",\nUI dump at: " + hierarchy
136                 + " (use go/web-hv to open the dump file)");
137         final UiDevice device = launcher.getDevice();
138         device.takeScreenshot(sceenshot);
139 
140         // Dump accessibility hierarchy
141         try {
142             device.dumpWindowHierarchy(diagFile(description, "AccessibilityHierarchy", "uix"));
143         } catch (IOException ex) {
144             Log.e(TAG, "Failed to save accessibility hierarchy", ex);
145         }
146 
147         // Dump bugreport
148         if (!sSavedBugreport) {
149             dumpCommand("bugreportz -s", diagFile(description, "Bugreport", "zip"));
150             // Not saving bugreport for each failure for time and space economy.
151             sSavedBugreport = true;
152         }
153     }
154 
155     private static void dumpStringCommand(String cmd, OutputStream out) throws IOException {
156         out.write(("\n\n" + cmd + "\n").getBytes());
157         dumpCommand(cmd, out);
158     }
159 
160     private static void dumpCommand(String cmd, File out) {
161         try (BufferedOutputStream buffered = new BufferedOutputStream(
162                 new FileOutputStream(out))) {
163             dumpCommand(cmd, buffered);
164         } catch (IOException ex) {
165         }
166     }
167 
168     private static void dumpCommand(String cmd, OutputStream out) throws IOException {
169         try (AutoCloseInputStream in = new AutoCloseInputStream(getInstrumentation()
170                 .getUiAutomation().executeShellCommand(cmd))) {
171             FileUtils.copy(in, out);
172         }
173     }
174 }
175