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 android.device.collectors;
17 
18 import android.device.collectors.annotations.OptionClass;
19 import android.os.Bundle;
20 import android.util.Log;
21 
22 import androidx.annotation.VisibleForTesting;
23 
24 import org.junit.runner.Description;
25 import org.junit.runner.notification.Failure;
26 
27 import java.io.File;
28 import java.io.IOException;
29 import java.util.Arrays;
30 import java.util.Date;
31 import java.util.HashMap;
32 import java.text.SimpleDateFormat;
33 
34 /**
35  * A {@link BaseMetricListener} that captures logcat after each test case failure.
36  *
37  * This class needs external storage permission. See {@link BaseMetricListener} how to grant
38  * external storage permission, especially at install time.
39  *
40  */
41 @OptionClass(alias = "logcat-failure-collector")
42 public class LogcatOnFailureCollector extends BaseMetricListener {
43     @VisibleForTesting
44     static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
45 
46     @VisibleForTesting static final String METRIC_SEP = "-";
47     @VisibleForTesting static final String FILENAME_SUFFIX = "logcat";
48 
49     public static final String DEFAULT_DIR = "run_listeners/logcats";
50     private static final int BUFFER_SIZE = 16 * 1024;
51 
52     private File mDestDir;
53     private String mStartTime = null;
54     private boolean mTestFailed = false;
55 
56     // Map to keep track of test iterations for multiple test iterations.
57     private HashMap<Description, Integer> mTestIterations = new HashMap<>();
58 
LogcatOnFailureCollector()59     public LogcatOnFailureCollector() {
60         super();
61     }
62 
63     /**
64      * Constructor to simulate receiving the instrumentation arguments. Should not be used except
65      * for testing.
66      */
67     @VisibleForTesting
LogcatOnFailureCollector(Bundle args)68     LogcatOnFailureCollector(Bundle args) {
69         super(args);
70     }
71 
72     @Override
onTestRunStart(DataRecord runData, Description description)73     public void onTestRunStart(DataRecord runData, Description description) {
74         mDestDir = createAndEmptyDirectory(DEFAULT_DIR);
75         // Capture the start time in case onTestStart() is never called due to failure during
76         // @BeforeClass.
77         mStartTime = getCurrentDate();
78     }
79 
80     @Override
onTestStart(DataRecord testData, Description description)81     public void onTestStart(DataRecord testData, Description description) {
82         // Capture the start time for logcat purpose.
83         // Overwrites any start time set prior to the test.
84         mStartTime = getCurrentDate();
85         // Keep track of test iterations.
86         mTestIterations.computeIfPresent(description, (desc, iteration) -> iteration + 1);
87         mTestIterations.computeIfAbsent(description, desc -> 1);
88     }
89 
90     /**
91      * Mark the test as failed if this is called. The actual collection will be done in {@link
92      * onTestEnd} to ensure that all actions around a test failure end up in the logcat.
93      */
94     @Override
onTestFail(DataRecord testData, Description description, Failure failure)95     public void onTestFail(DataRecord testData, Description description, Failure failure) {
96         mTestFailed = true;
97     }
98 
99     /** If the test fails, collect logcat since test start time. */
100     @Override
onTestEnd(DataRecord testData, Description description)101     public void onTestEnd(DataRecord testData, Description description) {
102         if (mTestFailed) {
103             // Capture logcat from start time
104             if (mDestDir == null) {
105                 return;
106             }
107             try {
108                 int iteration = mTestIterations.get(description);
109                 final String fileName =
110                         String.format(
111                                 "%s.%s%s%s-logcat-on-failure.txt",
112                                 description.getClassName(),
113                                 description.getMethodName(),
114                                 iteration == 1 ? "" : (METRIC_SEP + String.valueOf(iteration)),
115                                 METRIC_SEP + FILENAME_SUFFIX);
116                 File logcat = new File(mDestDir, fileName);
117                 getLogcatSince(mStartTime, logcat);
118                 testData.addFileMetric(String.format("%s_%s", getTag(), logcat.getName()), logcat);
119             } catch (IOException | InterruptedException e) {
120                 Log.e(getTag(), "Error trying to retrieve logcat.", e);
121             }
122         }
123         // Reset the flag here, as onTestStart might not have been called if a @BeforeClass method
124         // fails.
125         mTestFailed = false;
126         // Update the start time here in case onTestStart() is not called for the next test. If it
127         // is called, the start time will be overwritten.
128         mStartTime = getCurrentDate();
129     }
130 
131     /** @hide */
132     @VisibleForTesting
getLogcatSince(String startTime, File saveTo)133     protected void getLogcatSince(String startTime, File saveTo)
134             throws IOException, InterruptedException {
135         // ProcessBuilder is used here in favor of UiAutomation.executeShellCommand() because the
136         // logcat command requires the timestamp to be quoted which in Java requires
137         // Runtime.exec(String[]) or ProcessBuilder to work properly, and UiAutomation does not
138         // support this for now.
139         ProcessBuilder pb = new ProcessBuilder(Arrays.asList("logcat", "-t", startTime));
140         pb.redirectOutput(saveTo);
141         Process proc = pb.start();
142         // Make the process blocking to ensure consistent behavior.
143         proc.waitFor();
144     }
145 
146     /** @hide */
147     @VisibleForTesting
getCurrentDate()148     protected String getCurrentDate() {
149         // Get time using system (wall clock) time since this is the time that logcat is based on.
150         Date date = new Date(System.currentTimeMillis());
151         return DATE_FORMATTER.format(date);
152     }
153 }
154