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 package com.android.tradefed.device.metric; 17 18 import com.android.annotations.VisibleForTesting; 19 import com.android.tradefed.device.CollectingByteOutputReceiver; 20 import com.android.tradefed.device.DeviceNotAvailableException; 21 import com.android.tradefed.device.ILogcatReceiver; 22 import com.android.tradefed.device.ITestDevice; 23 import com.android.tradefed.device.LogcatReceiver; 24 import com.android.tradefed.device.TestDeviceState; 25 import com.android.tradefed.log.LogUtil.CLog; 26 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 27 import com.android.tradefed.result.ByteArrayInputStreamSource; 28 import com.android.tradefed.result.FailureDescription; 29 import com.android.tradefed.result.InputStreamSource; 30 import com.android.tradefed.result.LogDataType; 31 import com.android.tradefed.result.TestDescription; 32 import com.android.tradefed.util.IRunUtil; 33 import com.android.tradefed.util.RunUtil; 34 35 import java.util.HashMap; 36 import java.util.Map; 37 38 /** Collector that will capture and log a logcat when a test case fails. */ 39 public class LogcatOnFailureCollector extends BaseDeviceMetricCollector { 40 41 private static final int MAX_LOGAT_SIZE_BYTES = 4 * 1024 * 1024; 42 /** Always include a bit of prior data to capture what happened before */ 43 private static final int OFFSET_CORRECTION = 10000; 44 45 private static final String NAME_FORMAT = "%s-%s-logcat-on-failure"; 46 47 private static final String LOGCAT_COLLECT_CMD = "logcat -b all -T 150"; 48 // -t implies -d (dump) so it's a one time collection 49 private static final String LOGCAT_COLLECT_CMD_LEGACY = "logcat -b all -t 5000"; 50 private static final int API_LIMIT = 20; 51 52 private static final int THROTTLE_LIMIT_PER_RUN = 10; 53 54 private Map<ITestDevice, ILogcatReceiver> mLogcatReceivers = new HashMap<>(); 55 private Map<ITestDevice, Integer> mOffset = new HashMap<>(); 56 private int mCurrentCount = 0; 57 private boolean mFirstThrottle = true; 58 59 @Override onTestRunStart(DeviceMetricData runData)60 public void onTestRunStart(DeviceMetricData runData) { 61 mCurrentCount = 0; 62 mFirstThrottle = true; 63 for (ITestDevice device : getRealDevices()) { 64 if (getApiLevelNoThrow(device) < API_LIMIT) { 65 continue; 66 } 67 // In case of multiple runs for the same test runner, re-init the receiver. 68 initReceiver(device); 69 // Get the current offset of the buffer to be able to query later 70 try (InputStreamSource data = mLogcatReceivers.get(device).getLogcatData()) { 71 int offset = (int) data.size(); 72 if (offset > OFFSET_CORRECTION) { 73 offset -= OFFSET_CORRECTION; 74 } 75 mOffset.put(device, offset); 76 } 77 } 78 } 79 80 @Override onTestStart(DeviceMetricData testData)81 public void onTestStart(DeviceMetricData testData) { 82 // TODO: Handle the buffer to reset it at the test start 83 } 84 85 @Override onTestFail(DeviceMetricData testData, TestDescription test)86 public void onTestFail(DeviceMetricData testData, TestDescription test) 87 throws DeviceNotAvailableException { 88 if (mCurrentCount > THROTTLE_LIMIT_PER_RUN) { 89 if (mFirstThrottle) { 90 CLog.w("Throttle capture of logcat-on-failure due to too many failures."); 91 mFirstThrottle = false; 92 } 93 return; 94 } 95 // Delay slightly for the error to get in the logcat 96 getRunUtil().sleep(100); 97 collectAndLog(test.toString(), MAX_LOGAT_SIZE_BYTES); 98 mCurrentCount++; 99 } 100 101 @Override onTestRunFailed(DeviceMetricData testData, FailureDescription failure)102 public void onTestRunFailed(DeviceMetricData testData, FailureDescription failure) 103 throws DeviceNotAvailableException { 104 // Delay slightly for the error to get in the logcat 105 getRunUtil().sleep(100); 106 // TODO: Improve the name 107 collectAndLog("run-failure", MAX_LOGAT_SIZE_BYTES); 108 } 109 110 @Override onTestRunEnd(DeviceMetricData runData, Map<String, Metric> currentRunMetrics)111 public void onTestRunEnd(DeviceMetricData runData, Map<String, Metric> currentRunMetrics) { 112 clearReceivers(); 113 } 114 115 @VisibleForTesting createLogcatReceiver(ITestDevice device)116 ILogcatReceiver createLogcatReceiver(ITestDevice device) { 117 // Use logcat -T 'count' to only print a few line before we start and not the full buffer 118 return new LogcatReceiver( 119 device, LOGCAT_COLLECT_CMD, device.getOptions().getMaxLogcatDataSize(), 0); 120 } 121 122 @VisibleForTesting createLegacyCollectingReceiver()123 CollectingByteOutputReceiver createLegacyCollectingReceiver() { 124 return new CollectingByteOutputReceiver(); 125 } 126 127 @VisibleForTesting getRunUtil()128 IRunUtil getRunUtil() { 129 return RunUtil.getDefault(); 130 } 131 collectAndLog(String testName, int size)132 protected void collectAndLog(String testName, int size) throws DeviceNotAvailableException { 133 for (ITestDevice device : getRealDevices()) { 134 boolean isDeviceOnline = isDeviceOnline(device); 135 ILogcatReceiver receiver = mLogcatReceivers.get(device); 136 // Receiver is only initialized above API 19, if not supported, we use a legacy command 137 if (receiver == null) { 138 if (isDeviceOnline) { 139 legacyCollection(device, testName); 140 } else { 141 CLog.w("Skip legacy LogcatOnFailureCollector device is offline."); 142 } 143 continue; 144 } 145 // If supported get the logcat buffer, even if device is offline to get the buffer 146 saveLogcatSource( 147 testName, 148 receiver.getLogcatData(size, mOffset.get(device)), 149 device.getSerialNumber()); 150 } 151 } 152 initReceiver(ITestDevice device)153 private void initReceiver(ITestDevice device) { 154 if (mLogcatReceivers.get(device) == null) { 155 ILogcatReceiver receiver = createLogcatReceiver(device); 156 mLogcatReceivers.put(device, receiver); 157 receiver.start(); 158 } 159 } 160 clearReceivers()161 private void clearReceivers() { 162 for (ILogcatReceiver receiver : mLogcatReceivers.values()) { 163 receiver.stop(); 164 receiver.clear(); 165 } 166 mLogcatReceivers.clear(); 167 mOffset.clear(); 168 } 169 getApiLevelNoThrow(ITestDevice device)170 private int getApiLevelNoThrow(ITestDevice device) { 171 try { 172 return device.getApiLevel(); 173 } catch (DeviceNotAvailableException e) { 174 return 1; 175 } 176 } 177 legacyCollection(ITestDevice device, String testName)178 private void legacyCollection(ITestDevice device, String testName) 179 throws DeviceNotAvailableException { 180 CollectingByteOutputReceiver outputReceiver = createLegacyCollectingReceiver(); 181 device.executeShellCommand(LOGCAT_COLLECT_CMD_LEGACY, outputReceiver); 182 saveLogcatSource( 183 testName, 184 new ByteArrayInputStreamSource(outputReceiver.getOutput()), 185 device.getSerialNumber()); 186 outputReceiver.cancel(); 187 } 188 saveLogcatSource(String testName, InputStreamSource source, String serial)189 private void saveLogcatSource(String testName, InputStreamSource source, String serial) { 190 if (source == null) { 191 return; 192 } 193 try (InputStreamSource logcatSource = source) { 194 // If the resulting logcat looks wrong or empty, discard it 195 if (logcatSource.size() < 75L) { 196 CLog.e( 197 "Discarding logcat on failure (size=%s): it failed to collect something" 198 + " relevant likely due to timings.", 199 logcatSource.size()); 200 return; 201 } 202 String name = String.format(NAME_FORMAT, testName, serial); 203 super.testLog(name, LogDataType.LOGCAT, logcatSource); 204 } 205 } 206 isDeviceOnline(ITestDevice device)207 private boolean isDeviceOnline(ITestDevice device) { 208 TestDeviceState state = device.getDeviceState(); 209 if (!TestDeviceState.ONLINE.equals(state)) { 210 return false; 211 } 212 return true; 213 } 214 } 215