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