xref: /aosp_15_r20/external/robolectric/shadows/framework/src/main/java/org/robolectric/shadows/ShadowLog.java (revision e6ba16074e6af37d123cb567d575f496bf0a58ee)
1 package org.robolectric.shadows;
2 
3 import static org.robolectric.util.reflector.Reflector.reflector;
4 
5 import android.util.Log;
6 import android.util.Log.TerribleFailure;
7 import android.util.Log.TerribleFailureHandler;
8 import com.google.common.base.Ascii;
9 import com.google.common.base.Throwables;
10 import com.google.common.collect.ImmutableList;
11 import java.io.FileOutputStream;
12 import java.io.IOException;
13 import java.io.PrintStream;
14 import java.util.Collections;
15 import java.util.HashMap;
16 import java.util.Map;
17 import java.util.Queue;
18 import java.util.concurrent.ConcurrentLinkedQueue;
19 import java.util.function.Supplier;
20 import org.robolectric.RuntimeEnvironment;
21 import org.robolectric.annotation.Implementation;
22 import org.robolectric.annotation.Implements;
23 import org.robolectric.annotation.Resetter;
24 import org.robolectric.util.Util;
25 import org.robolectric.util.reflector.Accessor;
26 import org.robolectric.util.reflector.Constructor;
27 import org.robolectric.util.reflector.ForType;
28 import org.robolectric.util.reflector.Static;
29 import org.robolectric.versioning.AndroidVersions.L;
30 
31 /** Controls the behavior of {@link android.util.Log} and provides access to log messages. */
32 @Implements(Log.class)
33 public class ShadowLog {
34   public static PrintStream stream;
35 
36   private static final int EXTRA_LOG_LENGTH = "l/: \n".length();
37 
38   private static final Map<String, Queue<LogItem>> logsByTag =
39       Collections.synchronizedMap(new HashMap<String, Queue<LogItem>>());
40   private static final Queue<LogItem> logs = new ConcurrentLinkedQueue<>();
41   private static final Map<String, Integer> tagToLevel =
42       Collections.synchronizedMap(new HashMap<String, Integer>());
43 
44   /**
45    * Whether calling {@link Log#wtf} will throw {@link TerribleFailure}. This is analogous to
46    * Android's {@link android.provider.Settings.Global#WTF_IS_FATAL}. The default value is false to
47    * preserve existing behavior.
48    */
49   private static boolean wtfIsFatal = false;
50 
51   /** Provides string that will be used as time in logs. */
52   private static Supplier<String> timeSupplier;
53 
54   @Implementation
e(String tag, String msg)55   protected static int e(String tag, String msg) {
56     return e(tag, msg, null);
57   }
58 
59   @Implementation
e(String tag, String msg, Throwable throwable)60   protected static int e(String tag, String msg, Throwable throwable) {
61     return addLog(Log.ERROR, tag, msg, throwable);
62   }
63 
64   @Implementation
d(String tag, String msg)65   protected static int d(String tag, String msg) {
66     return d(tag, msg, null);
67   }
68 
69   @Implementation
d(String tag, String msg, Throwable throwable)70   protected static int d(String tag, String msg, Throwable throwable) {
71     return addLog(Log.DEBUG, tag, msg, throwable);
72   }
73 
74   @Implementation
i(String tag, String msg)75   protected static int i(String tag, String msg) {
76     return i(tag, msg, null);
77   }
78 
79   @Implementation
i(String tag, String msg, Throwable throwable)80   protected static int i(String tag, String msg, Throwable throwable) {
81     return addLog(Log.INFO, tag, msg, throwable);
82   }
83 
84   @Implementation
v(String tag, String msg)85   protected static int v(String tag, String msg) {
86     return v(tag, msg, null);
87   }
88 
89   @Implementation
v(String tag, String msg, Throwable throwable)90   protected static int v(String tag, String msg, Throwable throwable) {
91     return addLog(Log.VERBOSE, tag, msg, throwable);
92   }
93 
94   @Implementation
w(String tag, String msg)95   protected static int w(String tag, String msg) {
96     return w(tag, msg, null);
97   }
98 
99   @Implementation
w(String tag, Throwable throwable)100   protected static int w(String tag, Throwable throwable) {
101     return w(tag, null, throwable);
102   }
103 
104   @Implementation
w(String tag, String msg, Throwable throwable)105   protected static int w(String tag, String msg, Throwable throwable) {
106     return addLog(Log.WARN, tag, msg, throwable);
107   }
108 
109   @Implementation
wtf(String tag, String msg)110   protected static int wtf(String tag, String msg) {
111     return wtf(tag, msg, null);
112   }
113 
114   @Implementation
wtf(String tag, String msg, Throwable throwable)115   protected static int wtf(String tag, String msg, Throwable throwable) {
116     addLog(Log.ASSERT, tag, msg, throwable);
117     // invoking the wtfHandler
118     Throwable terribleFailure =
119         reflector(TerribleFailureReflector.class).newTerribleFailure(msg, throwable);
120     if (wtfIsFatal) {
121       Util.sneakyThrow(terribleFailure);
122     }
123     TerribleFailureHandler terribleFailureHandler = reflector(LogReflector.class).getWtfHandler();
124     if (RuntimeEnvironment.getApiLevel() >= L.SDK_INT) {
125       terribleFailureHandler.onTerribleFailure(tag, (TerribleFailure) terribleFailure, false);
126     } else {
127       reflector(TerribleFailureHandlerReflector.class, terribleFailureHandler)
128           .onTerribleFailure(tag, (TerribleFailure) terribleFailure);
129     }
130     return 0;
131   }
132 
133   /** Sets whether calling {@link Log#wtf} will throw {@link TerribleFailure}. */
setWtfIsFatal(boolean fatal)134   public static void setWtfIsFatal(boolean fatal) {
135     wtfIsFatal = fatal;
136   }
137 
138   /** Sets supplier that can be used to get time to add to logs. */
setTimeSupplier(Supplier<String> supplier)139   public static void setTimeSupplier(Supplier<String> supplier) {
140     timeSupplier = supplier;
141   }
142 
143   @Implementation
isLoggable(String tag, int level)144   protected static boolean isLoggable(String tag, int level) {
145     synchronized (tagToLevel) {
146       if (tagToLevel.containsKey(tag)) {
147         return level >= tagToLevel.get(tag);
148       }
149     }
150     return level >= Log.INFO;
151   }
152 
153   @Implementation
println_native(int bufID, int priority, String tag, String msg)154   protected static int println_native(int bufID, int priority, String tag, String msg) {
155     addLog(priority, tag, msg, null);
156     int tagLength = tag == null ? 0 : tag.length();
157     int msgLength = msg == null ? 0 : msg.length();
158     return EXTRA_LOG_LENGTH + tagLength + msgLength;
159   }
160 
161   /**
162    * Sets the log level of a given tag, that {@link #isLoggable} will follow.
163    *
164    * @param tag A log tag
165    * @param level A log level, from {@link android.util.Log}
166    */
setLoggable(String tag, int level)167   public static void setLoggable(String tag, int level) {
168     tagToLevel.put(tag, level);
169   }
170 
addLog(int level, String tag, String msg, Throwable throwable)171   private static int addLog(int level, String tag, String msg, Throwable throwable) {
172     String timeString = null;
173     if (timeSupplier != null) {
174       timeString = timeSupplier.get();
175     }
176 
177     if (stream != null) {
178       logToStream(stream, timeString, level, tag, msg, throwable);
179     }
180 
181     LogItem item = new LogItem(timeString, level, tag, msg, throwable);
182     Queue<LogItem> itemList;
183 
184     synchronized (logsByTag) {
185       if (!logsByTag.containsKey(tag)) {
186         itemList = new ConcurrentLinkedQueue<>();
187         logsByTag.put(tag, itemList);
188       } else {
189         itemList = logsByTag.get(tag);
190       }
191     }
192 
193     itemList.add(item);
194     logs.add(item);
195 
196     return 0;
197   }
198 
levelToChar(int level)199   protected static char levelToChar(int level) {
200     final char c;
201     switch (level) {
202       case Log.ASSERT:
203         c = 'A';
204         break;
205       case Log.DEBUG:
206         c = 'D';
207         break;
208       case Log.ERROR:
209         c = 'E';
210         break;
211       case Log.WARN:
212         c = 'W';
213         break;
214       case Log.INFO:
215         c = 'I';
216         break;
217       case Log.VERBOSE:
218         c = 'V';
219         break;
220       default:
221         c = '?';
222     }
223     return c;
224   }
225 
logToStream( PrintStream ps, String timeString, int level, String tag, String msg, Throwable throwable)226   private static void logToStream(
227       PrintStream ps, String timeString, int level, String tag, String msg, Throwable throwable) {
228 
229     String outputString;
230     if (timeString != null && timeString.length() > 0) {
231       outputString = timeString + " " + levelToChar(level) + "/" + tag + ": " + msg;
232     } else {
233       outputString = levelToChar(level) + "/" + tag + ": " + msg;
234     }
235 
236     ps.println(outputString);
237     if (throwable != null) {
238       throwable.printStackTrace(ps);
239     }
240   }
241 
242   /**
243    * Returns ordered list of all log entries.
244    *
245    * @return List of log items
246    */
getLogs()247   public static ImmutableList<LogItem> getLogs() {
248     return ImmutableList.copyOf(logs);
249   }
250 
251   /**
252    * Returns ordered list of all log items for a specific tag.
253    *
254    * @param tag The tag to get logs for
255    * @return The list of log items for the tag or an empty list if no logs for that tag exist.
256    */
getLogsForTag(String tag)257   public static ImmutableList<LogItem> getLogsForTag(String tag) {
258     Queue<LogItem> logs = logsByTag.get(tag);
259     return logs == null ? ImmutableList.of() : ImmutableList.copyOf(logs);
260   }
261 
262   /** Clear all accumulated logs. */
clear()263   public static void clear() {
264     reset();
265   }
266 
267   @Resetter
reset()268   public static void reset() {
269     logs.clear();
270     logsByTag.clear();
271     tagToLevel.clear();
272     wtfIsFatal = false;
273     timeSupplier = null;
274   }
275 
276   @SuppressWarnings("CatchAndPrintStackTrace")
setupLogging()277   public static void setupLogging() {
278     String logging = System.getProperty("robolectric.logging");
279     if (logging != null && stream == null) {
280       PrintStream stream = null;
281       if (Ascii.equalsIgnoreCase("stdout", logging)) {
282         stream = System.out;
283       } else if (Ascii.equalsIgnoreCase("stderr", logging)) {
284         stream = System.err;
285       } else {
286         try {
287           final PrintStream file = new PrintStream(new FileOutputStream(logging), true);
288           stream = file;
289           Runtime.getRuntime()
290               .addShutdownHook(
291                   new Thread() {
292                     @Override
293                     public void run() {
294                       file.close();
295                     }
296                   });
297         } catch (IOException e) {
298           e.printStackTrace();
299         }
300       }
301       ShadowLog.stream = stream;
302     }
303   }
304 
305   /** A single log item. */
306   public static final class LogItem {
307     public final String timeString;
308     public final int type;
309     public final String tag;
310     public final String msg;
311     public final Throwable throwable;
312 
LogItem(int type, String tag, String msg, Throwable throwable)313     public LogItem(int type, String tag, String msg, Throwable throwable) {
314       this.timeString = null;
315       this.type = type;
316       this.tag = tag;
317       this.msg = msg;
318       this.throwable = throwable;
319     }
320 
LogItem(String timeString, int type, String tag, String msg, Throwable throwable)321     public LogItem(String timeString, int type, String tag, String msg, Throwable throwable) {
322       this.timeString = timeString;
323       this.type = type;
324       this.tag = tag;
325       this.msg = msg;
326       this.throwable = throwable;
327     }
328 
329     @Override
equals(Object o)330     public boolean equals(Object o) {
331       if (this == o) {
332         return true;
333       }
334       if (!(o instanceof LogItem)) {
335         return false;
336       }
337 
338       LogItem log = (LogItem) o;
339       return type == log.type
340           && !(timeString != null ? !timeString.equals(log.timeString) : log.timeString != null)
341           && !(msg != null ? !msg.equals(log.msg) : log.msg != null)
342           && !(tag != null ? !tag.equals(log.tag) : log.tag != null)
343           && !(throwable != null ? !throwable.equals(log.throwable) : log.throwable != null);
344     }
345 
346     @Override
hashCode()347     public int hashCode() {
348       int result = 0;
349       result = 31 * result + (timeString != null ? timeString.hashCode() : 0);
350       result = 31 * result + type;
351       result = 31 * result + (tag != null ? tag.hashCode() : 0);
352       result = 31 * result + (msg != null ? msg.hashCode() : 0);
353       result = 31 * result + (throwable != null ? throwable.hashCode() : 0);
354       return result;
355     }
356 
357     @Override
toString()358     public String toString() {
359       return "LogItem{"
360           + "\n  timeString='"
361           + timeString
362           + '\''
363           + "\n  type="
364           + type
365           + "\n  tag='"
366           + tag
367           + '\''
368           + "\n  msg='"
369           + msg
370           + '\''
371           + "\n  throwable="
372           + (throwable == null ? null : Throwables.getStackTraceAsString(throwable))
373           + "\n}";
374     }
375   }
376 
377   @ForType(Log.class)
378   interface LogReflector {
379     @Static
380     @Accessor("sWtfHandler")
getWtfHandler()381     TerribleFailureHandler getWtfHandler();
382   }
383 
384   @ForType(TerribleFailureHandler.class)
385   interface TerribleFailureHandlerReflector {
onTerribleFailure(String tag, TerribleFailure what)386     void onTerribleFailure(String tag, TerribleFailure what);
387   }
388 
389   @ForType(TerribleFailure.class)
390   interface TerribleFailureReflector {
391     // The return value should be generic because TerribleFailure is a hidden class.
392     @Constructor
newTerribleFailure(String msg, Throwable cause)393     Throwable newTerribleFailure(String msg, Throwable cause);
394   }
395 }
396