• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import org.junit.runner.Result;
27 
28 import java.io.File;
29 import java.io.IOException;
30 import java.util.Arrays;
31 import java.util.Date;
32 import java.util.HashMap;
33 import java.text.SimpleDateFormat;
34 
35 /**
36  * A {@link LogcatCollector} that captures logcat after each test.
37  *
38  * This class needs external storage permission. See {@link BaseMetricListener} how to grant
39  * external storage permission, especially at install time.
40  *
41  */
42 @OptionClass(alias = "logcat-collector")
43 public class LogcatCollector extends BaseMetricListener {
44     @VisibleForTesting
45     static final SimpleDateFormat DATE_FORMATTER = new SimpleDateFormat("MM-dd HH:mm:ss.SSS");
46 
47     @VisibleForTesting static final String METRIC_SEP = "-";
48     @VisibleForTesting static final String FILENAME_SUFFIX = "logcat";
49     @VisibleForTesting static final String BEFORE_LOGCAT_DURATION_SECS =
50             "before-logcat-duration-secs";
51     @VisibleForTesting static final String COLLECT_ON_FAILURE_ONLY = "collect-on-failure-only";
52     @VisibleForTesting static final String RETURN_LOGCAT_DIR = "return-logcat-directory";
53     @VisibleForTesting static final String DEFAULT_DIR = "run_listeners/logcats";
54 
55     private static final int BUFFER_SIZE = 16 * 1024;
56 
57 
58     private File mDestDir;
59     private String mStartTime = null;
60     private boolean mTestFailed = false;
61     // Logcat duration to include before the test starts.
62     private long mBeforeLogcatDurationInSecs = 0;
63     // Use this flag to enable logcat collection only when the test fails.
64     private boolean mCollectOnlyTestFailed = false;
65     // Use this flag to return the root directory of the logcat files in run metrics
66     // otherwise individual logcat file will be reported associated with the test.
67     // The final directory which contains all the logcat files will be <DEFAULT_DIR>_all.
68     private boolean mReturnLogcatDir = false;
69 
70     // Map to keep track of test iterations for multiple test iterations.
71     private HashMap<Description, Integer> mTestIterations = new HashMap<>();
72 
LogcatCollector()73     public LogcatCollector() {
74         super();
75     }
76 
77     /**
78      * Constructor to simulate receiving the instrumentation arguments. Should not be used except
79      * for testing.
80      */
81     @VisibleForTesting
LogcatCollector(Bundle args)82     LogcatCollector(Bundle args) {
83         super(args);
84     }
85 
86     @Override
onTestRunStart(DataRecord runData, Description description)87     public void onTestRunStart(DataRecord runData, Description description) {
88         setupAdditionalArgs();
89         mDestDir = createAndEmptyDirectory(DEFAULT_DIR);
90         // Capture the start time in case onTestStart() is never called due to failure during
91         // @BeforeClass.
92         mStartTime = getLogcatStartTime();
93     }
94 
95     @Override
onTestStart(DataRecord testData, Description description)96     public void onTestStart(DataRecord testData, Description description) {
97         // Capture the start time for logcat purpose.
98         // Overwrites any start time set prior to the test and adds custom
99         // duration to capture before current start time.
100         mStartTime = getLogcatStartTime();
101         // Keep track of test iterations.
102         mTestIterations.computeIfPresent(description, (desc, iteration) -> iteration + 1);
103         mTestIterations.computeIfAbsent(description, desc -> 1);
104     }
105 
106     /**
107      * Mark the test as failed if this is called. The actual collection will be done in {@link
108      * onTestEnd} to ensure that all actions around a test failure end up in the logcat.
109      */
110     @Override
onTestFail(DataRecord testData, Description description, Failure failure)111     public void onTestFail(DataRecord testData, Description description, Failure failure) {
112         mTestFailed = true;
113     }
114 
115     /**
116      * Collect the logcat at the end of each test or collect the logcat only on test
117      * failed if the flag is enabled.
118      */
119     @Override
onTestEnd(DataRecord testData, Description description)120     public void onTestEnd(DataRecord testData, Description description) {
121         if (!mCollectOnlyTestFailed || (mCollectOnlyTestFailed && mTestFailed)) {
122             // Capture logcat from start time
123             if (mDestDir == null) {
124                 return;
125             }
126             try {
127                 int iteration = mTestIterations.get(description);
128                 final String fileName =
129                         String.format(
130                                 "%s.%s%s%s-logcat.txt",
131                                 description.getClassName(),
132                                 description.getMethodName(),
133                                 iteration == 1 ? "" : (METRIC_SEP + String.valueOf(iteration)),
134                                 METRIC_SEP + FILENAME_SUFFIX);
135                 File logcat = new File(mDestDir, fileName);
136                 getLogcatSince(mStartTime, logcat);
137                 if (!mReturnLogcatDir) {
138                     // Do not return individual logcat file path if the logcat directory
139                     // option is enabled. Logcat root directory path will be returned in the
140                     // test run status.
141                     testData.addFileMetric(String.format("%s_%s", getTag(), logcat.getName()),
142                             logcat);
143                 }
144             } catch (IOException | InterruptedException e) {
145                 Log.e(getTag(), "Error trying to retrieve logcat.", e);
146             }
147         }
148         // Reset the flag here, as onTestStart might not have been called if a @BeforeClass method
149         // fails.
150         mTestFailed = false;
151         // Update the start time here in case onTestStart() is not called for the next test. If it
152         // is called, the start time will be overwritten.
153         mStartTime = getLogcatStartTime();
154     }
155 
156     @Override
onTestRunEnd(DataRecord runData, Result result)157     public void onTestRunEnd(DataRecord runData, Result result) {
158         if (mReturnLogcatDir) {
159             runData.addStringMetric(getTag(), mDestDir.getAbsolutePath().toString());
160         }
161     }
162 
163     /** @hide */
164     @VisibleForTesting
getLogcatSince(String startTime, File saveTo)165     protected void getLogcatSince(String startTime, File saveTo)
166             throws IOException, InterruptedException {
167         // ProcessBuilder is used here in favor of UiAutomation.executeShellCommand() because the
168         // logcat command requires the timestamp to be quoted which in Java requires
169         // Runtime.exec(String[]) or ProcessBuilder to work properly, and UiAutomation does not
170         // support this for now.
171         ProcessBuilder pb = new ProcessBuilder(Arrays.asList("logcat", "-t", startTime));
172         pb.redirectOutput(saveTo);
173         Process proc = pb.start();
174         // Make the process blocking to ensure consistent behavior.
175         proc.waitFor();
176     }
177 
178     @VisibleForTesting
getLogcatStartTime()179     protected String getLogcatStartTime() {
180         Date date = new Date(System.currentTimeMillis());
181         Log.i(getTag(), "Current Date:" + DATE_FORMATTER.format(date));
182         if (mBeforeLogcatDurationInSecs > 0) {
183             date = new Date(System.currentTimeMillis() - (mBeforeLogcatDurationInSecs * 1000));
184             Log.i(getTag(), "Date including the before duration:" + DATE_FORMATTER.format(date));
185         }
186         return DATE_FORMATTER.format(date);
187     }
188 
189     /**
190      * Add custom options if available.
191      */
setupAdditionalArgs()192     private void setupAdditionalArgs() {
193         Bundle args = getArgsBundle();
194 
195         if (args.getString(BEFORE_LOGCAT_DURATION_SECS) != null) {
196             mBeforeLogcatDurationInSecs = Long
197                     .parseLong(args.getString(BEFORE_LOGCAT_DURATION_SECS));
198         }
199 
200         if (args.getString(COLLECT_ON_FAILURE_ONLY) != null) {
201             mCollectOnlyTestFailed = Boolean.parseBoolean(args.getString(COLLECT_ON_FAILURE_ONLY));
202         }
203 
204         if (args.getString(RETURN_LOGCAT_DIR) != null) {
205             mReturnLogcatDir = Boolean
206                     .parseBoolean(args.getString(RETURN_LOGCAT_DIR));
207         }
208 
209     }
210 }
211