1 /* 2 * Copyright (C) 2016 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 package com.android.tradefed.testtype.suite; 17 18 import com.android.ddmlib.testrunner.TestIdentifier; 19 import com.android.tradefed.device.DeviceNotAvailableException; 20 import com.android.tradefed.device.ITestDevice; 21 import com.android.tradefed.log.LogUtil.CLog; 22 import com.android.tradefed.result.ITestInvocationListener; 23 import com.android.tradefed.result.InputStreamSource; 24 import com.android.tradefed.result.LogDataType; 25 import com.android.tradefed.util.IRunUtil; 26 import com.android.tradefed.util.RunUtil; 27 28 import com.google.common.annotations.VisibleForTesting; 29 30 import java.util.ArrayList; 31 import java.util.HashMap; 32 import java.util.List; 33 import java.util.Map; 34 35 /** 36 * Listener used to take action such as screenshot, bugreport, logcat collection upon a test failure 37 * when requested. 38 */ 39 public class TestFailureListener implements ITestInvocationListener { 40 41 private static final int DEFAULT_MAX_LOGCAT_BYTES = 500 * 1024; // 500K 42 /* Arbitrary upper limit for mMaxLogcatBytes to avoid un-reasonably high limit */ 43 private static final int LOGCAT_BYTE_LIMIT = 20 * 1024 * 1024; // 20 MB 44 private static final String LOGCAT_ON_FAILURE_SIZE_OPTION = "logcat-on-failure-size"; 45 private static final long LOGCAT_CAPTURE_TIMEOUT = 2 * 60 * 1000; 46 47 private List<ITestDevice> mListDevice; 48 private ITestInvocationListener mListener; 49 private boolean mBugReportOnFailure; 50 private boolean mLogcatOnFailure; 51 private boolean mScreenshotOnFailure; 52 private boolean mRebootOnFailure; 53 private int mMaxLogcatBytes; 54 private Map<TestIdentifier, Long> mTrackStartTime = new HashMap<>(); 55 private List<Thread> mLogcatThreads = new ArrayList<>(); 56 57 public TestFailureListener( 58 ITestInvocationListener listener, 59 List<ITestDevice> devices, 60 boolean bugReportOnFailure, 61 boolean logcatOnFailure, 62 boolean screenshotOnFailure, 63 boolean rebootOnFailure, 64 int maxLogcatBytes) { 65 mListener = listener; 66 mListDevice = devices; 67 mBugReportOnFailure = bugReportOnFailure; 68 mLogcatOnFailure = logcatOnFailure; 69 mScreenshotOnFailure = screenshotOnFailure; 70 mRebootOnFailure = rebootOnFailure; 71 if (maxLogcatBytes < 0 ) { 72 CLog.w("FailureListener could not set %s to '%d', using default value %d", 73 LOGCAT_ON_FAILURE_SIZE_OPTION, maxLogcatBytes, 74 DEFAULT_MAX_LOGCAT_BYTES); 75 mMaxLogcatBytes = DEFAULT_MAX_LOGCAT_BYTES; 76 } else if (maxLogcatBytes > LOGCAT_BYTE_LIMIT) { 77 CLog.w("Value %d for %s exceeds limit %d, using limit value", maxLogcatBytes, 78 LOGCAT_ON_FAILURE_SIZE_OPTION, LOGCAT_BYTE_LIMIT); 79 mMaxLogcatBytes = LOGCAT_BYTE_LIMIT; 80 } else { 81 mMaxLogcatBytes = maxLogcatBytes; 82 } 83 } 84 85 /** 86 * We override testStarted in order to track the start time. 87 */ 88 @Override 89 public void testStarted(TestIdentifier test) { 90 if (mLogcatOnFailure) { 91 try { 92 mTrackStartTime.put(test, mListDevice.get(0).getDeviceDate()); 93 } catch (DeviceNotAvailableException e) { 94 CLog.e(e); 95 // we fall back to logcat dump on null. 96 mTrackStartTime.put(test, null); 97 } 98 } 99 } 100 101 /** 102 * Make sure we clean the map when test end to avoid too much overhead. 103 */ 104 @Override 105 public void testEnded(TestIdentifier test, Map<String, String> testMetrics) { 106 if (mLogcatOnFailure) { 107 mTrackStartTime.remove(test); 108 } 109 } 110 111 /** 112 * {@inheritDoc} 113 */ 114 @Override 115 public void testFailed(TestIdentifier test, String trace) { 116 CLog.i("FailureListener.testFailed %s %b %b %b", test.toString(), mBugReportOnFailure, 117 mLogcatOnFailure, mScreenshotOnFailure); 118 for (ITestDevice device : mListDevice) { 119 captureFailure(device, test); 120 } 121 } 122 123 /** Capture the appropriate logs for one device for one test failure. */ 124 private void captureFailure(ITestDevice device, TestIdentifier test) { 125 String serial = device.getSerialNumber(); 126 if (mScreenshotOnFailure) { 127 try { 128 try (InputStreamSource screenSource = device.getScreenshot()) { 129 testLog( 130 String.format("%s-%s-screenshot", test.toString(), serial), 131 LogDataType.PNG, 132 screenSource); 133 } 134 } catch (DeviceNotAvailableException e) { 135 CLog.e(e); 136 CLog.e("Device %s became unavailable while capturing screenshot", serial); 137 } 138 } 139 if (mBugReportOnFailure) { 140 try (InputStreamSource bugSource = device.getBugreportz()) { 141 testLog( 142 String.format("%s-%s-bugreport", test.toString(), serial), 143 LogDataType.BUGREPORTZ, 144 bugSource); 145 } 146 } 147 if (mLogcatOnFailure) { 148 Runnable captureLogcat = 149 new Runnable() { 150 @Override 151 public void run() { 152 InputStreamSource logSource = null; 153 Long startTime = mTrackStartTime.remove(test); 154 if (startTime != null) { 155 logSource = device.getLogcatSince(startTime); 156 } else { 157 // sleep 2s to ensure test failure stack trace makes it into the 158 // logcat capture 159 getRunUtil().sleep(2 * 1000); 160 logSource = device.getLogcat(mMaxLogcatBytes); 161 } 162 testLog( 163 String.format("%s-%s-logcat", test.toString(), serial), 164 LogDataType.LOGCAT, 165 logSource); 166 logSource.close(); 167 } 168 }; 169 if (mRebootOnFailure) { 170 captureLogcat.run(); 171 } else { 172 // If no reboot will be done afterward capture asynchronously the logcat. 173 Thread captureThread = 174 new Thread(captureLogcat, String.format("Capture failure logcat %s", test)); 175 captureThread.setDaemon(true); 176 mLogcatThreads.add(captureThread); 177 captureThread.start(); 178 } 179 } 180 if (mRebootOnFailure) { 181 try { 182 // Rebooting on all failures can hide legitimate issues and platform instabilities, 183 // therefore only allowed on "user-debug" and "eng" builds. 184 if ("user".equals(device.getProperty("ro.build.type"))) { 185 CLog.e("Reboot-on-failure should only be used during development," + 186 " this is a\" user\" build device"); 187 } else { 188 device.reboot(); 189 } 190 } catch (DeviceNotAvailableException e) { 191 CLog.e(e); 192 CLog.e("Device %s became unavailable while rebooting", serial); 193 } 194 } 195 } 196 197 /** Join on all the logcat capturing threads to ensure they terminate. */ 198 public void join() { 199 synchronized (mLogcatThreads) { 200 for (Thread t : mLogcatThreads) { 201 if (!t.isAlive()) { 202 continue; 203 } 204 try { 205 t.join(LOGCAT_CAPTURE_TIMEOUT); 206 } catch (InterruptedException e) { 207 CLog.e(e); 208 } 209 } 210 mLogcatThreads.clear(); 211 } 212 } 213 214 @Override 215 public void testLog(String dataName, LogDataType dataType, InputStreamSource dataStream) { 216 mListener.testLog(dataName, dataType, dataStream); 217 } 218 219 /** 220 * Get the default {@link IRunUtil} instance 221 */ 222 @VisibleForTesting 223 IRunUtil getRunUtil() { 224 return RunUtil.getDefault(); 225 } 226 } 227