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 @Implementation 29 public static void e(String tag, String msg) { 30 e(tag, msg, null); 31 } 32 33 @Implementation 34 public static void e(String tag, String msg, Throwable throwable) { 35 addLog(Log.ERROR, tag, msg, throwable); 36 } 37 38 @Implementation 39 public static void d(String tag, String msg) { 40 d(tag, msg, null); 41 } 42 43 @Implementation 44 public static void d(String tag, String msg, Throwable throwable) { 45 addLog(Log.DEBUG, tag, msg, throwable); 46 } 47 48 @Implementation 49 public static void i(String tag, String msg) { 50 i(tag, msg, null); 51 } 52 53 @Implementation 54 public static void i(String tag, String msg, Throwable throwable) { 55 addLog(Log.INFO, tag, msg, throwable); 56 } 57 58 @Implementation 59 public static void v(String tag, String msg) { 60 v(tag, msg, null); 61 } 62 63 @Implementation 64 public static void v(String tag, String msg, Throwable throwable) { 65 addLog(Log.VERBOSE, tag, msg, throwable); 66 } 67 68 @Implementation 69 public static void w(String tag, String msg) { 70 w(tag, msg, null); 71 } 72 73 @Implementation 74 public static void w(String tag, Throwable throwable) { 75 w(tag, null, throwable); 76 } 77 78 79 @Implementation 80 public static void w(String tag, String msg, Throwable throwable) { 81 addLog(Log.WARN, tag, msg, throwable); 82 } 83 84 @Implementation 85 public static void wtf(String tag, String msg) { 86 wtf(tag, msg, null); 87 } 88 89 @Implementation 90 public static void wtf(String tag, String msg, Throwable throwable) { 91 addLog(Log.ASSERT, tag, msg, throwable); 92 } 93 94 @Implementation 95 public static boolean isLoggable(String tag, int level) { 96 synchronized (tagToLevel) { 97 if (tagToLevel.containsKey(tag)) { 98 return level >= tagToLevel.get(tag); 99 } 100 } 101 return stream != null || level >= Log.INFO; 102 } 103 104 @Implementation 105 public static int println(int priority, String tag, String msg) { 106 addLog(priority, tag, msg, null); 107 int tagLength = tag == null ? 0 : tag.length(); 108 int msgLength = msg == null ? 0 : msg.length(); 109 return extraLogLength + tagLength + msgLength; 110 } 111 112 /** 113 * Sets the log level of a given tag, that {@link #isLoggable} will follow. 114 * @param tag A log tag 115 * @param level A log level, from {@link android.util.Log} 116 */ 117 public static void setLoggable(String tag, int level) { 118 tagToLevel.put(tag, level); 119 } 120 121 private static void addLog(int level, String tag, String msg, Throwable throwable) { 122 if (stream != null) { 123 logToStream(stream, level, tag, msg, throwable); 124 } 125 126 LogItem item = new LogItem(level, tag, msg, throwable); 127 Queue<LogItem> itemList; 128 129 synchronized (logsByTag) { 130 if (!logsByTag.containsKey(tag)) { 131 itemList = new ConcurrentLinkedQueue<>(); 132 logsByTag.put(tag, itemList); 133 } else { 134 itemList = logsByTag.get(tag); 135 } 136 } 137 138 itemList.add(item); 139 logs.add(item); 140 } 141 142 private static void logToStream(PrintStream ps, int level, String tag, String msg, Throwable throwable) { 143 final char c; 144 switch (level) { 145 case Log.ASSERT: c = 'A'; break; 146 case Log.DEBUG: c = 'D'; break; 147 case Log.ERROR: c = 'E'; break; 148 case Log.WARN: c = 'W'; break; 149 case Log.INFO: c = 'I'; break; 150 case Log.VERBOSE:c = 'V'; break; 151 default: c = '?'; 152 } 153 ps.println(c + "/" + tag + ": " + msg); 154 if (throwable != null) { 155 throwable.printStackTrace(ps); 156 } 157 } 158 159 /** 160 * Returns ordered list of all log entries. 161 * @return List of log items 162 */ 163 public static List<LogItem> getLogs() { 164 return new ArrayList<>(logs); 165 } 166 167 /** 168 * Returns ordered list of all log items for a specific tag. 169 * 170 * @param tag The tag to get logs for 171 * @return The list of log items for the tag or an empty list if no logs for that tag exist. 172 */ 173 public static List<LogItem> getLogsForTag(String tag) { 174 Queue<LogItem> logs = logsByTag.get(tag); 175 return logs == null ? Collections.emptyList() : new ArrayList<>(logs); 176 } 177 178 /** 179 * Clear all accummulated logs. 180 */ 181 public static void clear() { 182 reset(); 183 } 184 185 @Resetter 186 public static void reset() { 187 logs.clear(); 188 logsByTag.clear(); 189 tagToLevel.clear(); 190 } 191 192 public static void setupLogging() { 193 String logging = System.getProperty("robolectric.logging"); 194 if (logging != null && stream == null) { 195 PrintStream stream = null; 196 if ("stdout".equalsIgnoreCase(logging)) { 197 stream = System.out; 198 } else if ("stderr".equalsIgnoreCase(logging)) { 199 stream = System.err; 200 } else { 201 try { 202 final PrintStream file = new PrintStream(new FileOutputStream(logging), true); 203 stream = file; 204 Runtime.getRuntime().addShutdownHook(new Thread() { 205 @Override public void run() { 206 try { 207 file.close(); 208 } catch (Exception ignored) { 209 } 210 } 211 }); 212 } catch (IOException e) { 213 e.printStackTrace(); 214 } 215 } 216 ShadowLog.stream = stream; 217 } 218 } 219 220 public static class LogItem { 221 public final int type; 222 public final String tag; 223 public final String msg; 224 public final Throwable throwable; 225 226 public LogItem(int type, String tag, String msg, Throwable throwable) { 227 this.type = type; 228 this.tag = tag; 229 this.msg = msg; 230 this.throwable = throwable; 231 } 232 233 @Override 234 public boolean equals(Object o) { 235 if (this == o) return true; 236 if (o == null || getClass() != o.getClass()) return false; 237 238 LogItem log = (LogItem) o; 239 return type == log.type 240 && !(msg != null ? !msg.equals(log.msg) : log.msg != null) 241 && !(tag != null ? !tag.equals(log.tag) : log.tag != null) 242 && !(throwable != null ? !throwable.equals(log.throwable) : log.throwable != null); 243 } 244 245 @Override 246 public int hashCode() { 247 int result = type; 248 result = 31 * result + (tag != null ? tag.hashCode() : 0); 249 result = 31 * result + (msg != null ? msg.hashCode() : 0); 250 result = 31 * result + (throwable != null ? throwable.hashCode() : 0); 251 return result; 252 } 253 254 @Override 255 public String toString() { 256 return "LogItem{" + 257 "type=" + type + 258 ", tag='" + tag + '\'' + 259 ", msg='" + msg + '\'' + 260 ", throwable=" + throwable + 261 '}'; 262 } 263 } 264 } 265