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