1 package org.robolectric.shadows; 2 3 import android.util.Log; 4 import java.io.FileOutputStream; 5 import java.io.IOException; 6 import java.io.PrintStream; 7 import java.util.ArrayList; 8 import java.util.Collections; 9 import java.util.HashMap; 10 import java.util.List; 11 import java.util.Map; 12 import java.util.Queue; 13 import java.util.concurrent.ConcurrentLinkedQueue; 14 import org.robolectric.annotation.Implementation; 15 import org.robolectric.annotation.Implements; 16 import org.robolectric.annotation.Resetter; 17 18 @Implements(Log.class) 19 public class ShadowLog { 20 private static final int extraLogLength = "l/: \n".length(); 21 private static final Map<String, Queue<LogItem>> logsByTag = Collections.synchronizedMap(new 22 HashMap<String, Queue<LogItem>>()); 23 private static final Queue<LogItem> logs = new ConcurrentLinkedQueue<>(); 24 public static PrintStream stream; 25 private static final Map<String, Integer> tagToLevel = Collections.synchronizedMap(new 26 HashMap<String, Integer>()); 27 28 /** 29 * Whether calling {@link Log#wtf} will throw {@link TerribleFailure}. This is analogous to 30 * Android's {@link android.provider.Settings.Global#WTF_IS_FATAL}. The default value is false to 31 * preserve existing behavior. 32 */ 33 private static boolean wtfIsFatal = false; 34 35 @Implementation e(String tag, String msg)36 protected static int e(String tag, String msg) { 37 return e(tag, msg, null); 38 } 39 40 @Implementation e(String tag, String msg, Throwable throwable)41 protected static int e(String tag, String msg, Throwable throwable) { 42 return addLog(Log.ERROR, tag, msg, throwable); 43 } 44 45 @Implementation d(String tag, String msg)46 protected static int d(String tag, String msg) { 47 return d(tag, msg, null); 48 } 49 50 @Implementation d(String tag, String msg, Throwable throwable)51 protected static int d(String tag, String msg, Throwable throwable) { 52 return addLog(Log.DEBUG, tag, msg, throwable); 53 } 54 55 @Implementation i(String tag, String msg)56 protected static int i(String tag, String msg) { 57 return i(tag, msg, null); 58 } 59 60 @Implementation i(String tag, String msg, Throwable throwable)61 protected static int i(String tag, String msg, Throwable throwable) { 62 return addLog(Log.INFO, tag, msg, throwable); 63 } 64 65 @Implementation v(String tag, String msg)66 protected static int v(String tag, String msg) { 67 return v(tag, msg, null); 68 } 69 70 @Implementation v(String tag, String msg, Throwable throwable)71 protected static int v(String tag, String msg, Throwable throwable) { 72 return addLog(Log.VERBOSE, tag, msg, throwable); 73 } 74 75 @Implementation w(String tag, String msg)76 protected static int w(String tag, String msg) { 77 return w(tag, msg, null); 78 } 79 80 @Implementation w(String tag, Throwable throwable)81 protected static int w(String tag, Throwable throwable) { 82 return w(tag, null, throwable); 83 } 84 85 @Implementation w(String tag, String msg, Throwable throwable)86 protected static int w(String tag, String msg, Throwable throwable) { 87 return addLog(Log.WARN, tag, msg, throwable); 88 } 89 90 @Implementation wtf(String tag, String msg)91 protected static int wtf(String tag, String msg) { 92 return wtf(tag, msg, null); 93 } 94 95 @Implementation wtf(String tag, String msg, Throwable throwable)96 protected static int wtf(String tag, String msg, Throwable throwable) { 97 addLog(Log.ASSERT, tag, msg, throwable); 98 if (wtfIsFatal) { 99 throw new TerribleFailure(msg, throwable); 100 } 101 return 0; 102 } 103 104 /** Sets whether calling {@link Log#wtf} will throw {@link TerribleFailure}. */ setWtfIsFatal(boolean fatal)105 public static void setWtfIsFatal(boolean fatal) { 106 wtfIsFatal = fatal; 107 } 108 109 @Implementation isLoggable(String tag, int level)110 protected static boolean isLoggable(String tag, int level) { 111 synchronized (tagToLevel) { 112 if (tagToLevel.containsKey(tag)) { 113 return level >= tagToLevel.get(tag); 114 } 115 } 116 return stream != null || level >= Log.INFO; 117 } 118 119 @Implementation println_native(int bufID, int priority, String tag, String msg)120 protected static int println_native(int bufID, int priority, String tag, String msg) { 121 addLog(priority, tag, msg, null); 122 int tagLength = tag == null ? 0 : tag.length(); 123 int msgLength = msg == null ? 0 : msg.length(); 124 return extraLogLength + tagLength + msgLength; 125 } 126 127 /** 128 * Sets the log level of a given tag, that {@link #isLoggable} will follow. 129 * @param tag A log tag 130 * @param level A log level, from {@link android.util.Log} 131 */ setLoggable(String tag, int level)132 public static void setLoggable(String tag, int level) { 133 tagToLevel.put(tag, level); 134 } 135 addLog(int level, String tag, String msg, Throwable throwable)136 private static int addLog(int level, String tag, String msg, Throwable throwable) { 137 if (stream != null) { 138 logToStream(stream, level, tag, msg, throwable); 139 } 140 141 LogItem item = new LogItem(level, tag, msg, throwable); 142 Queue<LogItem> itemList; 143 144 synchronized (logsByTag) { 145 if (!logsByTag.containsKey(tag)) { 146 itemList = new ConcurrentLinkedQueue<>(); 147 logsByTag.put(tag, itemList); 148 } else { 149 itemList = logsByTag.get(tag); 150 } 151 } 152 153 itemList.add(item); 154 logs.add(item); 155 156 return 0; 157 } 158 logToStream(PrintStream ps, int level, String tag, String msg, Throwable throwable)159 private static void logToStream(PrintStream ps, int level, String tag, String msg, Throwable throwable) { 160 final char c; 161 switch (level) { 162 case Log.ASSERT: c = 'A'; break; 163 case Log.DEBUG: c = 'D'; break; 164 case Log.ERROR: c = 'E'; break; 165 case Log.WARN: c = 'W'; break; 166 case Log.INFO: c = 'I'; break; 167 case Log.VERBOSE:c = 'V'; break; 168 default: c = '?'; 169 } 170 ps.println(c + "/" + tag + ": " + msg); 171 if (throwable != null) { 172 throwable.printStackTrace(ps); 173 } 174 } 175 176 /** 177 * Returns ordered list of all log entries. 178 * @return List of log items 179 */ getLogs()180 public static List<LogItem> getLogs() { 181 return new ArrayList<>(logs); 182 } 183 184 /** 185 * Returns ordered list of all log items for a specific tag. 186 * 187 * @param tag The tag to get logs for 188 * @return The list of log items for the tag or an empty list if no logs for that tag exist. 189 */ getLogsForTag(String tag)190 public static List<LogItem> getLogsForTag(String tag) { 191 Queue<LogItem> logs = logsByTag.get(tag); 192 return logs == null ? Collections.emptyList() : new ArrayList<>(logs); 193 } 194 195 /** Clear all accumulated logs. */ clear()196 public static void clear() { 197 reset(); 198 } 199 200 @Resetter reset()201 public static void reset() { 202 logs.clear(); 203 logsByTag.clear(); 204 tagToLevel.clear(); 205 wtfIsFatal = false; 206 } 207 208 @SuppressWarnings("CatchAndPrintStackTrace") setupLogging()209 public static void setupLogging() { 210 String logging = System.getProperty("robolectric.logging"); 211 if (logging != null && stream == null) { 212 PrintStream stream = null; 213 if ("stdout".equalsIgnoreCase(logging)) { 214 stream = System.out; 215 } else if ("stderr".equalsIgnoreCase(logging)) { 216 stream = System.err; 217 } else { 218 try { 219 final PrintStream file = new PrintStream(new FileOutputStream(logging), true); 220 stream = file; 221 Runtime.getRuntime().addShutdownHook(new Thread() { 222 @Override public void run() { 223 try { 224 file.close(); 225 } catch (Exception ignored) { 226 } 227 } 228 }); 229 } catch (IOException e) { 230 e.printStackTrace(); 231 } 232 } 233 ShadowLog.stream = stream; 234 } 235 } 236 237 public static class LogItem { 238 public final int type; 239 public final String tag; 240 public final String msg; 241 public final Throwable throwable; 242 LogItem(int type, String tag, String msg, Throwable throwable)243 public LogItem(int type, String tag, String msg, Throwable throwable) { 244 this.type = type; 245 this.tag = tag; 246 this.msg = msg; 247 this.throwable = throwable; 248 } 249 250 @Override equals(Object o)251 public boolean equals(Object o) { 252 if (this == o) return true; 253 if (o == null || getClass() != o.getClass()) return false; 254 255 LogItem log = (LogItem) o; 256 return type == log.type 257 && !(msg != null ? !msg.equals(log.msg) : log.msg != null) 258 && !(tag != null ? !tag.equals(log.tag) : log.tag != null) 259 && !(throwable != null ? !throwable.equals(log.throwable) : log.throwable != null); 260 } 261 262 @Override hashCode()263 public int hashCode() { 264 int result = type; 265 result = 31 * result + (tag != null ? tag.hashCode() : 0); 266 result = 31 * result + (msg != null ? msg.hashCode() : 0); 267 result = 31 * result + (throwable != null ? throwable.hashCode() : 0); 268 return result; 269 } 270 271 @Override toString()272 public String toString() { 273 return "LogItem{" + 274 "type=" + type + 275 ", tag='" + tag + '\'' + 276 ", msg='" + msg + '\'' + 277 ", throwable=" + throwable + 278 '}'; 279 } 280 } 281 282 /** 283 * Failure thrown when wtf_is_fatal is true and Log.wtf is called. This is a parallel 284 * implementation of framework's hidden API {@link android.util.Log#TerribleFailure}, to allow 285 * tests to catch / expect these exceptions. 286 */ 287 public static class TerribleFailure extends RuntimeException { TerribleFailure(String msg, Throwable cause)288 public TerribleFailure(String msg, Throwable cause) { 289 super(msg, cause); 290 } 291 } 292 } 293