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