1 // Copyright 2023 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.net; 6 7 import org.chromium.base.Log; 8 9 import java.io.BufferedReader; 10 import java.io.Closeable; 11 import java.io.IOException; 12 import java.io.InputStreamReader; 13 import java.util.ArrayList; 14 import java.util.Arrays; 15 import java.util.List; 16 import java.util.UUID; 17 18 /** 19 * Test utility class for capturing logcat output. 20 * 21 * <p>This class is useful for testing code that logs to logcat. 22 * 23 * <p><b>Note:</b> when using this class, it is recommended to match the resulting log lines based 24 * on randomly generated markers (e.g. {@link UUID#randomUUID()}), if possible. This ensures the 25 * test cannot be confused by similar code running in parallel on the same device. 26 */ 27 public final class LogcatCapture implements Closeable { 28 private static final String TAG = "LogcatCapture"; 29 30 private Process mLogcat; 31 private final BufferedReader mLogcatOutput; 32 33 /** 34 * Starts capturing logcat. 35 * 36 * <p>By default, this only captures the {@code main} log and filters out all tags and levels. 37 * Use {@code additionalArgs} to customize this behavior. 38 * 39 * @param additionalArgs Additional arguments to pass to the logcat command. This can be used 40 * to select which tags and levels to capture, e.g. {@code cr_MyTag:I}. Refer to the 41 * documentation of the {@code logcat} command for details. 42 */ LogcatCapture(List<String> additionalArgs)43 LogcatCapture(List<String> additionalArgs) throws IOException { 44 List<String> args = 45 new ArrayList<String>( 46 Arrays.asList("logcat", "-s", "-b", "main", Log.normalizeTag(TAG) + ":I")); 47 args.addAll(additionalArgs); 48 mLogcat = new ProcessBuilder().command(args).start(); 49 mLogcat.getErrorStream().close(); 50 mLogcat.getOutputStream().close(); 51 mLogcatOutput = new BufferedReader(new InputStreamReader(mLogcat.getInputStream())); 52 53 // To ensure we only return logs that have been produced after this point, log a line with 54 // a marker then consume the logcat output until we find the marker. This also provides 55 // useful information to a human troubleshooting the logcat output. 56 String marker = UUID.randomUUID().toString(); 57 Log.i(TAG, "%s --- START OF LOGCAT CAPTURE --- (command: %s)", marker, args); 58 while (!readLine().contains(marker)) {} 59 } 60 61 /** 62 * Waits for a log line to arrive, then returns it. 63 * 64 * <p>Note that, contrary to the logcat command, this will only return log lines that have been 65 * produced <i>after</i> the capture started. This ensures the output is not polluted by logs 66 * from previous tests. 67 * 68 * @return The log line. Never null. 69 * @throws IllegalStateException if {@link #close()} was called 70 */ readLine()71 String readLine() throws IOException { 72 if (mLogcat == null) { 73 throw new IllegalStateException("Attempting to read a closed LogcatCapture"); 74 } 75 76 String line = mLogcatOutput.readLine(); 77 if (line == null) { 78 // We've reached end of stream, which means logcat unexpectedly closed its stdout 79 // (most likely an error/crash). Clean up the process. 80 close(/* reachedEndOfStream= */ true); // guaranteed to throw 81 } 82 return line; 83 } 84 85 /** Stops the capture. */ 86 @Override close()87 public void close() { 88 close(/* reachedEndOfStream= */ false); 89 } 90 close(boolean reachedEndOfStream)91 private void close(boolean reachedEndOfStream) { 92 if (mLogcat == null) return; 93 94 try { 95 // Announce the end of the capture as a courtesy to a human reading the logcat output. 96 String marker = UUID.randomUUID().toString(); 97 Log.i(TAG, "%s --- END OF LOGCAT CAPTURE ---", marker); 98 99 // As a self-check, make sure the end marker shows up in the capture. 100 while (!reachedEndOfStream) { 101 String line = mLogcatOutput.readLine(); 102 if (line == null) { 103 reachedEndOfStream = true; 104 } else if (line.contains(marker)) { 105 break; 106 } 107 } 108 109 mLogcat.destroy(); 110 mLogcat.waitFor(); 111 mLogcat = null; 112 if (reachedEndOfStream) { 113 throw new RuntimeException("unexpected end of stream from logcat command"); 114 } 115 } catch (InterruptedException exception) { 116 Thread.currentThread().interrupt(); 117 throw new RuntimeException("Interrupted while closing logcat", exception); 118 } catch (IOException exception) { 119 throw new RuntimeException("I/O exception while closing logcat", exception); 120 } 121 } 122 } 123