Home | History | Annotate | Download | only in shadows
      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
     36   protected static int e(String tag, String msg) {
     37     return e(tag, msg, null);
     38   }
     39 
     40   @Implementation
     41   protected static int e(String tag, String msg, Throwable throwable) {
     42     return addLog(Log.ERROR, tag, msg, throwable);
     43   }
     44 
     45   @Implementation
     46   protected static int d(String tag, String msg) {
     47     return d(tag, msg, null);
     48   }
     49 
     50   @Implementation
     51   protected static int d(String tag, String msg, Throwable throwable) {
     52     return addLog(Log.DEBUG, tag, msg, throwable);
     53   }
     54 
     55   @Implementation
     56   protected static int i(String tag, String msg) {
     57     return i(tag, msg, null);
     58   }
     59 
     60   @Implementation
     61   protected static int i(String tag, String msg, Throwable throwable) {
     62     return addLog(Log.INFO, tag, msg, throwable);
     63   }
     64 
     65   @Implementation
     66   protected static int v(String tag, String msg) {
     67     return v(tag, msg, null);
     68   }
     69 
     70   @Implementation
     71   protected static int v(String tag, String msg, Throwable throwable) {
     72     return addLog(Log.VERBOSE, tag, msg, throwable);
     73   }
     74 
     75   @Implementation
     76   protected static int w(String tag, String msg) {
     77     return w(tag, msg, null);
     78   }
     79 
     80   @Implementation
     81   protected static int w(String tag, Throwable throwable) {
     82     return w(tag, null, throwable);
     83   }
     84 
     85   @Implementation
     86   protected static int w(String tag, String msg, Throwable throwable) {
     87     return addLog(Log.WARN, tag, msg, throwable);
     88   }
     89 
     90   @Implementation
     91   protected static int wtf(String tag, String msg) {
     92     return wtf(tag, msg, null);
     93   }
     94 
     95   @Implementation
     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}. */
    105   public static void setWtfIsFatal(boolean fatal) {
    106     wtfIsFatal = fatal;
    107   }
    108 
    109   @Implementation
    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
    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    */
    132   public static void setLoggable(String tag, int level) {
    133     tagToLevel.put(tag, level);
    134   }
    135 
    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 
    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    */
    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    */
    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. */
    196   public static void clear() {
    197     reset();
    198   }
    199 
    200   @Resetter
    201   public static void reset() {
    202     logs.clear();
    203     logsByTag.clear();
    204     tagToLevel.clear();
    205     wtfIsFatal = false;
    206   }
    207 
    208   @SuppressWarnings("CatchAndPrintStackTrace")
    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 
    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
    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
    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
    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 {
    288     public TerribleFailure(String msg, Throwable cause) {
    289       super(msg, cause);
    290     }
    291   }
    292 }
    293