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