1 /* 2 * Copyright (C) 2018 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.result; 17 18 import com.android.loganalysis.item.JavaCrashItem; 19 import com.android.loganalysis.item.LogcatItem; 20 import com.android.loganalysis.parser.LogcatParser; 21 import com.android.tradefed.device.ITestDevice; 22 import com.android.tradefed.log.LogUtil.CLog; 23 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 24 import com.android.tradefed.result.error.DeviceErrorIdentifier; 25 import com.android.tradefed.result.proto.TestRecordProto.FailureStatus; 26 import com.android.tradefed.util.StreamUtil; 27 28 import java.io.IOException; 29 import java.util.ArrayList; 30 import java.util.Arrays; 31 import java.util.HashMap; 32 import java.util.LinkedHashSet; 33 import java.util.List; 34 35 /** 36 * Special listener: on failures (instrumentation process crashing) it will attempt to extract from 37 * the logcat the crash and adds it to the failure message associated with the test. 38 */ 39 public class LogcatCrashResultForwarder extends ResultForwarder { 40 41 /** Special error message from the instrumentation when something goes wrong on device side. */ 42 public static final String ERROR_MESSAGE = "Process crashed."; 43 public static final String SYSTEM_CRASH_MESSAGE = "System has crashed."; 44 45 public static final int MAX_NUMBER_CRASH = 3; 46 47 private Long mStartTime = null; 48 private Long mLastStartTime = null; 49 private ITestDevice mDevice; 50 private LogcatItem mLogcatItem = null; 51 LogcatCrashResultForwarder(ITestDevice device, ITestInvocationListener... listeners)52 public LogcatCrashResultForwarder(ITestDevice device, ITestInvocationListener... listeners) { 53 super(listeners); 54 mDevice = device; 55 } 56 getDevice()57 public ITestDevice getDevice() { 58 return mDevice; 59 } 60 61 @Override testStarted(TestDescription test, long startTime)62 public void testStarted(TestDescription test, long startTime) { 63 mStartTime = startTime; 64 super.testStarted(test, startTime); 65 } 66 67 @Override testFailed(TestDescription test, String trace)68 public void testFailed(TestDescription test, String trace) { 69 testFailed(test, FailureDescription.create(trace)); 70 } 71 72 @Override testFailed(TestDescription test, FailureDescription failure)73 public void testFailed(TestDescription test, FailureDescription failure) { 74 // If the test case was detected as crashing the instrumentation, we add the crash to it. 75 String trace = extractCrashAndAddToMessage(failure.getErrorMessage(), mStartTime); 76 if (trace.compareTo(failure.getErrorMessage()) != 0) { 77 // Crash stack trace found, consider this a test failure. 78 failure.setFailureStatus(FailureStatus.TEST_FAILURE); 79 } 80 failure.setErrorMessage(trace); 81 super.testFailed(test, failure); 82 } 83 84 @Override testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics)85 public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) { 86 super.testEnded(test, endTime, testMetrics); 87 mLastStartTime = mStartTime; 88 mStartTime = null; 89 } 90 91 @Override testRunFailed(String errorMessage)92 public void testRunFailed(String errorMessage) { 93 testRunFailed(FailureDescription.create(errorMessage, FailureStatus.TEST_FAILURE)); 94 } 95 96 @Override testRunFailed(FailureDescription error)97 public void testRunFailed(FailureDescription error) { 98 // Also add the failure to the run failure if the testFailed generated it. 99 // A Process crash would end the instrumentation, so a testRunFailed is probably going to 100 // be raised for the same reason. 101 String errorMessage = error.getErrorMessage(); 102 if (mLogcatItem != null) { 103 errorMessage = addJavaCrashToString(mLogcatItem, errorMessage); 104 mLogcatItem = null; 105 } else { 106 errorMessage = extractCrashAndAddToMessage(errorMessage, mLastStartTime); 107 } 108 error.setErrorMessage(errorMessage); 109 if (isCrash(errorMessage)) { 110 error.setErrorIdentifier(DeviceErrorIdentifier.INSTRUMENATION_CRASH); 111 } 112 super.testRunFailed(error); 113 } 114 115 @Override testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)116 public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) { 117 super.testRunEnded(elapsedTime, runMetrics); 118 mLastStartTime = null; 119 } 120 121 /** Attempt to extract the crash from the logcat if the test was seen as started. */ extractCrashAndAddToMessage(String errorMessage, Long startTime)122 private String extractCrashAndAddToMessage(String errorMessage, Long startTime) { 123 if (isCrash(errorMessage) && startTime != null) { 124 mLogcatItem = extractLogcat(mDevice, startTime); 125 errorMessage = addJavaCrashToString(mLogcatItem, errorMessage); 126 } 127 return errorMessage; 128 } 129 isCrash(String errorMessage)130 private boolean isCrash(String errorMessage) { 131 return errorMessage.contains(ERROR_MESSAGE) || errorMessage.contains(SYSTEM_CRASH_MESSAGE); 132 } 133 134 /** 135 * Extract a formatted object from the logcat snippet. 136 * 137 * @param device The device from which to pull the logcat. 138 * @param startTime The beginning time of the last tests. 139 * @return A {@link LogcatItem} that contains the information inside the logcat. 140 */ extractLogcat(ITestDevice device, long startTime)141 private LogcatItem extractLogcat(ITestDevice device, long startTime) { 142 try (InputStreamSource logSource = device.getLogcatSince(startTime)) { 143 if (logSource.size() == 0L) { 144 return null; 145 } 146 String message = StreamUtil.getStringFromStream(logSource.createInputStream()); 147 LogcatParser parser = new LogcatParser(); 148 List<String> lines = Arrays.asList(message.split("\n")); 149 return parser.parse(lines); 150 } catch (IOException e) { 151 CLog.e(e); 152 } 153 return null; 154 } 155 156 /** Append the Java crash information to the failure message. */ addJavaCrashToString(LogcatItem item, String errorMsg)157 private String addJavaCrashToString(LogcatItem item, String errorMsg) { 158 if (item == null) { 159 return errorMsg; 160 } 161 List<String> crashes = dedupCrash(item.getJavaCrashes()); 162 int displayed = Math.min(crashes.size(), MAX_NUMBER_CRASH); 163 for (int i = 0; i < displayed; i++) { 164 errorMsg = String.format("%s\nCrash Message:%s\n", errorMsg, crashes.get(i)); 165 } 166 return errorMsg; 167 } 168 169 /** Remove identical crash from the list of errors. */ dedupCrash(List<JavaCrashItem> origList)170 private List<String> dedupCrash(List<JavaCrashItem> origList) { 171 LinkedHashSet<String> dedupList = new LinkedHashSet<>(); 172 for (JavaCrashItem item : origList) { 173 dedupList.add(String.format("%s\n%s", item.getMessage(), item.getStack())); 174 } 175 return new ArrayList<>(dedupList); 176 } 177 } 178