• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 = Collections.synchronizedMap(new
39       HashMap<String, Queue<LogItem>>());
40   private static final Queue<LogItem> logs = new ConcurrentLinkedQueue<>();
41   private static final Map<String, Integer> tagToLevel = Collections.synchronizedMap(new
42       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    * @param tag A log tag
164    * @param level A log level, from {@link android.util.Log}
165    */
setLoggable(String tag, int level)166   public static void setLoggable(String tag, int level) {
167     tagToLevel.put(tag, level);
168   }
169 
addLog(int level, String tag, String msg, Throwable throwable)170   private static int addLog(int level, String tag, String msg, Throwable throwable) {
171     String timeString = null;
172     if (timeSupplier != null) {
173       timeString = timeSupplier.get();
174     }
175 
176     if (stream != null) {
177       logToStream(stream, timeString, level, tag, msg, throwable);
178     }
179 
180     LogItem item = new LogItem(timeString, level, tag, msg, throwable);
181     Queue<LogItem> itemList;
182 
183     synchronized (logsByTag) {
184       if (!logsByTag.containsKey(tag)) {
185         itemList = new ConcurrentLinkedQueue<>();
186         logsByTag.put(tag, itemList);
187       } else {
188         itemList = logsByTag.get(tag);
189       }
190     }
191 
192     itemList.add(item);
193     logs.add(item);
194 
195     return 0;
196   }
197 
levelToChar(int level)198   protected static char levelToChar(int level) {
199     final char c;
200     switch (level) {
201       case Log.ASSERT: c = 'A'; break;
202       case Log.DEBUG:  c = 'D'; break;
203       case Log.ERROR:  c = 'E'; break;
204       case Log.WARN:   c = 'W'; break;
205       case Log.INFO:   c = 'I'; break;
206       case Log.VERBOSE:c = 'V'; break;
207       default:         c = '?';
208     }
209     return c;
210   }
211 
logToStream( PrintStream ps, String timeString, int level, String tag, String msg, Throwable throwable)212   private static void logToStream(
213       PrintStream ps, String timeString, int level, String tag, String msg, Throwable throwable) {
214 
215     String outputString;
216     if (timeString != null && timeString.length() > 0) {
217       outputString = timeString + " " + levelToChar(level) + "/" + tag + ": " + msg;
218     } else {
219       outputString = levelToChar(level) + "/" + tag + ": " + msg;
220     }
221 
222     ps.println(outputString);
223     if (throwable != null) {
224       throwable.printStackTrace(ps);
225     }
226   }
227 
228   /**
229    * Returns ordered list of all log entries.
230    *
231    * @return List of log items
232    */
getLogs()233   public static ImmutableList<LogItem> getLogs() {
234     return ImmutableList.copyOf(logs);
235   }
236 
237   /**
238    * Returns ordered list of all log items for a specific tag.
239    *
240    * @param tag The tag to get logs for
241    * @return The list of log items for the tag or an empty list if no logs for that tag exist.
242    */
getLogsForTag(String tag)243   public static ImmutableList<LogItem> getLogsForTag(String tag) {
244     Queue<LogItem> logs = logsByTag.get(tag);
245     return logs == null ? ImmutableList.of() : ImmutableList.copyOf(logs);
246   }
247 
248   /** Clear all accumulated logs. */
clear()249   public static void clear() {
250     reset();
251   }
252 
253   @Resetter
reset()254   public static void reset() {
255     logs.clear();
256     logsByTag.clear();
257     tagToLevel.clear();
258     wtfIsFatal = false;
259     timeSupplier = null;
260   }
261 
262   @SuppressWarnings("CatchAndPrintStackTrace")
setupLogging()263   public static void setupLogging() {
264     String logging = System.getProperty("robolectric.logging");
265     if (logging != null && stream == null) {
266       PrintStream stream = null;
267       if (Ascii.equalsIgnoreCase("stdout", logging)) {
268         stream = System.out;
269       } else if (Ascii.equalsIgnoreCase("stderr", logging)) {
270         stream = System.err;
271       } else {
272         try {
273           final PrintStream file = new PrintStream(new FileOutputStream(logging), true);
274           stream = file;
275           Runtime.getRuntime()
276               .addShutdownHook(
277                   new Thread() {
278                     @Override
279                     public void run() {
280                       file.close();
281                     }
282                   });
283         } catch (IOException e) {
284           e.printStackTrace();
285         }
286       }
287       ShadowLog.stream = stream;
288     }
289   }
290 
291   /** A single log item. */
292   public static final class LogItem {
293     public final String timeString;
294     public final int type;
295     public final String tag;
296     public final String msg;
297     public final Throwable throwable;
298 
LogItem(int type, String tag, String msg, Throwable throwable)299     public LogItem(int type, String tag, String msg, Throwable throwable) {
300       this.timeString = null;
301       this.type = type;
302       this.tag = tag;
303       this.msg = msg;
304       this.throwable = throwable;
305     }
306 
LogItem(String timeString, int type, String tag, String msg, Throwable throwable)307     public LogItem(String timeString, int type, String tag, String msg, Throwable throwable) {
308       this.timeString = timeString;
309       this.type = type;
310       this.tag = tag;
311       this.msg = msg;
312       this.throwable = throwable;
313     }
314 
315     @Override
equals(Object o)316     public boolean equals(Object o) {
317       if (this == o) {
318         return true;
319       }
320       if (!(o instanceof LogItem)) {
321         return false;
322       }
323 
324       LogItem log = (LogItem) o;
325       return type == log.type
326           && !(timeString != null ? !timeString.equals(log.timeString) : log.timeString != null)
327           && !(msg != null ? !msg.equals(log.msg) : log.msg != null)
328           && !(tag != null ? !tag.equals(log.tag) : log.tag != null)
329           && !(throwable != null ? !throwable.equals(log.throwable) : log.throwable != null);
330     }
331 
332     @Override
hashCode()333     public int hashCode() {
334       int result = 0;
335       result = 31 * result + (timeString != null ? timeString.hashCode() : 0);
336       result = 31 * result + type;
337       result = 31 * result + (tag != null ? tag.hashCode() : 0);
338       result = 31 * result + (msg != null ? msg.hashCode() : 0);
339       result = 31 * result + (throwable != null ? throwable.hashCode() : 0);
340       return result;
341     }
342 
343     @Override
toString()344     public String toString() {
345       return "LogItem{"
346           + "\n  timeString='"
347           + timeString
348           + '\''
349           + "\n  type="
350           + type
351           + "\n  tag='"
352           + tag
353           + '\''
354           + "\n  msg='"
355           + msg
356           + '\''
357           + "\n  throwable="
358           + (throwable == null ? null : Throwables.getStackTraceAsString(throwable))
359           + "\n}";
360     }
361   }
362 
363   @ForType(Log.class)
364   interface LogReflector {
365     @Static
366     @Accessor("sWtfHandler")
getWtfHandler()367     TerribleFailureHandler getWtfHandler();
368   }
369 
370   @ForType(TerribleFailureHandler.class)
371   interface TerribleFailureHandlerReflector {
onTerribleFailure(String tag, TerribleFailure what)372     void onTerribleFailure(String tag, TerribleFailure what);
373   }
374 
375   @ForType(TerribleFailure.class)
376   interface TerribleFailureReflector {
377     // The return value should be generic because TerribleFailure is a hidden class.
378     @Constructor
newTerribleFailure(String msg, Throwable cause)379     Throwable newTerribleFailure(String msg, Throwable cause);
380   }
381 }
382