• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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