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