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