Refactors c.g.gwt.logging to use SerializableThrowable.

Change-Id: Ifeee26a9b36cbe40bd1cf0942da8fae39179e985
Review-Link: https://gwt-review.googlesource.com/#/c/2310/

Review by: skybrian@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@11580 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/logging/client/HtmlLogFormatter.java b/user/src/com/google/gwt/logging/client/HtmlLogFormatter.java
index 74cc1c1..b56ae58 100644
--- a/user/src/com/google/gwt/logging/client/HtmlLogFormatter.java
+++ b/user/src/com/google/gwt/logging/client/HtmlLogFormatter.java
@@ -17,6 +17,7 @@
 package com.google.gwt.logging.client;
 
 import com.google.gwt.logging.impl.FormatterImpl;
+import com.google.gwt.logging.impl.StackTracePrintStream;
 
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
@@ -27,28 +28,35 @@
  * is properly escaped.
  */
 public class HtmlLogFormatter extends FormatterImpl {
-  private static String newline = "__GWT_LOG_FORMATTER_BR__";
   private boolean showStackTraces;
-  
+
   public HtmlLogFormatter(boolean showStackTraces) {
     this.showStackTraces = showStackTraces;
   }
 
-  // TODO(unnurg): Handle the outputting of Throwables.
   @Override
   public String format(LogRecord event) {
-    StringBuilder html = new StringBuilder();
+    final StringBuilder html = new StringBuilder();
     html.append(getHtmlPrefix(event));
     html.append(getRecordInfo(event, " "));
     html.append(getEscaped(event.getMessage()));
-    if (showStackTraces) {
-      html.append(getEscaped(getStackTraceAsString(
-          event.getThrown(), newline, "   ")));
+    if (showStackTraces && event.getThrown() != null) {
+      event.getThrown().printStackTrace(new StackTracePrintStream(html) {
+        @Override
+        public void append(String str) {
+          html.append(getEscaped(str));
+        }
+
+        @Override
+        public void newLine() {
+          html.append("<br>");
+        }
+      });
     }
     html.append(getHtmlSuffix(event));
     return html.toString();
   }
-  
+
   protected String getHtmlPrefix(LogRecord event) {
     StringBuilder prefix = new StringBuilder();
     prefix.append("<span style='color:");
@@ -57,14 +65,11 @@
     prefix.append("<code>");
     return prefix.toString();
   }
-  
-  /**
-   * @param event
-   */
+
   protected String getHtmlSuffix(LogRecord event) {
     return "</code></span>";
   }
-  
+
   private String getColor(int logLevel) {
     if (logLevel == Level.OFF.intValue()) {
       return "#000"; // black
@@ -96,8 +101,7 @@
   private String getEscaped(String text) {
     text = text.replaceAll("<", "&lt;");
     text = text.replaceAll(">", "&gt;");
-    // but allow the line breaks that we put in ourselves
-    text = text.replaceAll(newline, "<br>");
+    text = text.replaceAll("\t", "&nbsp;&nbsp;&nbsp;");
     return text;
   }
 
diff --git a/user/src/com/google/gwt/logging/client/TextLogFormatter.java b/user/src/com/google/gwt/logging/client/TextLogFormatter.java
index b001f7b..67cc4cb 100644
--- a/user/src/com/google/gwt/logging/client/TextLogFormatter.java
+++ b/user/src/com/google/gwt/logging/client/TextLogFormatter.java
@@ -17,6 +17,7 @@
 package com.google.gwt.logging.client;
 
 import com.google.gwt.logging.impl.FormatterImpl;
+import com.google.gwt.logging.impl.StackTracePrintStream;
 
 import java.util.logging.LogRecord;
 
@@ -35,8 +36,8 @@
     StringBuilder message = new StringBuilder();
     message.append(getRecordInfo(event, "\n"));
     message.append(event.getMessage());
-    if (showStackTraces) {
-      message.append(getStackTraceAsString(event.getThrown(), "\n", "\t"));
+    if (showStackTraces && event.getThrown() != null) {
+      event.getThrown().printStackTrace(new StackTracePrintStream(message));
     }
     return message.toString();
   }
diff --git a/user/src/com/google/gwt/logging/impl/FormatterImpl.java b/user/src/com/google/gwt/logging/impl/FormatterImpl.java
index 2af328c..ca02f22 100644
--- a/user/src/com/google/gwt/logging/impl/FormatterImpl.java
+++ b/user/src/com/google/gwt/logging/impl/FormatterImpl.java
@@ -16,10 +16,8 @@
 
 package com.google.gwt.logging.impl;
 
-import com.google.gwt.core.client.impl.SerializableThrowable;
-
+import java.io.PrintStream;
 import java.util.Date;
-import java.util.HashSet;
 import java.util.logging.Formatter;
 import java.util.logging.LogRecord;
 
@@ -45,43 +43,29 @@
     s.append(": ");
     return s.toString();
   }
-  
-  // This method is borrowed from AbstractTreeLogger. 
-  // TODO(unnurg): once there is a clear place where code used by gwt-dev and
-  // gwt-user can live, move this function there.
-  protected String getStackTraceAsString(Throwable e, String newline,
-      String indent) {
+
+  /**
+   * @deprecated Use {@link Throwable#printStackTrace(PrintStream)} with
+   *             {@link StackTracePrintStream} instead.
+   */
+  @Deprecated
+  protected String getStackTraceAsString(Throwable e, final String newline, final String indent) {
     if (e == null) {
       return "";
     }
-    // For each cause, print the requested number of entries of its stack
-    // trace, being careful to avoid getting stuck in an infinite loop.
-    //
-    StringBuffer s = new StringBuffer(newline);
-    Throwable currentCause = e;
-    String causedBy = "";
-    HashSet<Throwable> seenCauses = new HashSet<Throwable>();
-    while (currentCause != null && !seenCauses.contains(currentCause)) {
-      seenCauses.add(currentCause);
-      s.append(causedBy);
-      causedBy = newline + "Caused by: "; // after 1st, all say "caused by"
-      if (currentCause instanceof SerializableThrowable.ThrowableWithClassName) {
-        s.append(((SerializableThrowable.ThrowableWithClassName) currentCause).getExceptionClass());
-      } else {
-        s.append(currentCause.getClass().getName());
-      }
-      s.append(": " + currentCause.getMessage());
-      StackTraceElement[] stackElems = currentCause.getStackTrace();
-      if (stackElems != null) {
-        for (int i = 0; i < stackElems.length; ++i) {
-          s.append(newline + indent + "at ");
-          s.append(stackElems[i].toString());
-        }
+    final StringBuilder builder = new StringBuilder();
+    PrintStream stream = new StackTracePrintStream(builder) {
+      @Override
+      public void append(String str) {
+        builder.append(str.replaceAll("\t", indent));
       }
 
-      currentCause = currentCause.getCause();
-    }
-    return s.toString();
+      @Override
+      public void newLine() {
+        builder.append(newline);
+      }
+    };
+    e.printStackTrace(stream);
+    return builder.toString();
   }
-
 }
diff --git a/user/src/com/google/gwt/logging/impl/StackTracePrintStream.java b/user/src/com/google/gwt/logging/impl/StackTracePrintStream.java
new file mode 100644
index 0000000..2be2e27
--- /dev/null
+++ b/user/src/com/google/gwt/logging/impl/StackTracePrintStream.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.logging.impl;
+
+import java.io.FilterOutputStream;
+import java.io.PrintStream;
+
+/**
+ * A {@link PrintStream} implementation that implements only a subset of methods that is enough to
+ * be used with {@link Throwable#printStackTrace(PrintStream)}.
+ * <p>
+ * Note that all implemented methods marked as final except two methods that are safe to be
+ * overridden by the subclasses: {@link #append(String)} {@link #newLine()}.
+ */
+public class StackTracePrintStream extends PrintStream {
+
+  private final StringBuilder builder;
+
+  public StackTracePrintStream(StringBuilder builder) {
+    super(new FilterOutputStream(null));
+    this.builder = builder;
+  }
+
+  /**
+   * Appends some text to the output.
+   */
+  protected void append(String text) {
+    builder.append(text);
+  }
+
+  /**
+   * Appends a newline to the output.
+   */
+  protected void newLine() {
+    builder.append("\n");
+  }
+
+  @Override
+  public final void print(Object obj) {
+    append(String.valueOf(obj));
+  }
+
+  @Override
+  public final void println(Object obj) {
+    append(String.valueOf(obj));
+    newLine();
+  }
+
+  @Override
+  public final void print(String str) {
+    append(str);
+  }
+
+  @Override
+  public final void println() {
+    newLine();
+  }
+
+  @Override
+  public final void println(String str) {
+    append(str);
+    newLine();
+  }
+}
\ No newline at end of file
diff --git a/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java b/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java
index f3384a8..be1ed65 100644
--- a/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java
+++ b/user/src/com/google/gwt/logging/server/JsonLogRecordServerUtil.java
@@ -16,15 +16,12 @@
 
 package com.google.gwt.logging.server;
 
-import com.google.gwt.core.client.impl.SerializableThrowable;
-
 import org.json.JSONArray;
 import org.json.JSONException;
 import org.json.JSONObject;
 
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
-import java.util.logging.Logger;
 
 /**
  * A set of functions to convert standard JSON strings into
@@ -35,17 +32,14 @@
  * JsonLogRecordClientUtil.
  */
 public class JsonLogRecordServerUtil {
-  private static Logger logger =
-    Logger.getLogger(JsonLogRecordServerUtil.class.getName());
-  public static LogRecord logRecordFromJson(String jsonString)
-  throws JSONException {
+
+  public static LogRecord logRecordFromJson(String jsonString) throws JSONException {
     JSONObject lro = new JSONObject(jsonString);
     String level = lro.getString("level");
     String loggerName = lro.getString("loggerName");
     String msg = lro.getString("msg");
     long timestamp = Long.parseLong(lro.getString("timestamp"));
-    Throwable thrown =
-      throwableFromJson(lro.getString("thrown"));
+    Throwable thrown = JsonLogRecordThrowable.fromJsonString(lro.getString("thrown"));
     LogRecord lr = new LogRecord(Level.parse(level), msg);
     lr.setLoggerName(loggerName);
     lr.setThrown(thrown);
@@ -53,41 +47,48 @@
     return lr;
   }
 
-  private static StackTraceElement stackTraceElementFromJson(
-      String jsonString) throws JSONException {
-    JSONObject ste = new JSONObject(jsonString);
-    String className = ste.getString("className");
-    String fileName = ste.getString("fileName");
-    String methodName = ste.getString("methodName");
-    int lineNumber = Integer.parseInt(ste.getString("lineNumber"));
-    return new StackTraceElement(className, methodName, fileName, lineNumber);
-  }
+  private static class JsonLogRecordThrowable extends Throwable {
 
-  private static Throwable throwableFromJson(String jsonString)
-  throws JSONException {
-    if (jsonString.equals("{}")) {
-      return null;
-    }
-    JSONObject t = new JSONObject(jsonString);
-    String message = t.getString("message");
-    Throwable cause =
-      throwableFromJson(t.getString("cause"));
-    StackTraceElement[] stackTrace = null;
-    if (t.has("stackTrace")) {
-      JSONArray st = t.getJSONArray("stackTrace");
-      if (st.length() > 0) {
-        stackTrace = new StackTraceElement[st.length()];
-        for (int i = 0; i < st.length(); i++) {
-          stackTrace[i] = stackTraceElementFromJson(st.getString(i));
-        }
+    private static Throwable fromJsonString(String jsonString) throws JSONException {
+      if (jsonString.equals("{}")) {
+        return null;
       }
-    } else {
-      stackTrace = new StackTraceElement[0];
+      return new JsonLogRecordThrowable(new JSONObject(jsonString));
     }
-    String exceptionClass = t.getString("type");
-    SerializableThrowable.ThrowableWithClassName thrown = 
-        new SerializableThrowable.ThrowableWithClassName(message, cause, exceptionClass);
-    thrown.setStackTrace(stackTrace);
-    return thrown;
+
+    private String type;
+    private String msg;
+
+    public JsonLogRecordThrowable(JSONObject t) throws JSONException {
+      type = t.getString("type");
+      msg = t.getString("message");
+      setStackTrace(stackTraceFromJson(t.optJSONArray("stackTrace")));
+      initCause(JsonLogRecordThrowable.fromJsonString(t.getString("cause")));
+    }
+
+    private StackTraceElement[] stackTraceFromJson(JSONArray st) throws JSONException {
+      if (st == null || st.length() <= 0) {
+        return new StackTraceElement[0];
+      }
+      StackTraceElement[] stackTrace = new StackTraceElement[st.length()];
+      for (int i = 0; i < st.length(); i++) {
+        stackTrace[i] = stackTraceElementFromJson(st.getString(i));
+      }
+      return stackTrace;
+    }
+
+    private StackTraceElement stackTraceElementFromJson(String jsonString) throws JSONException {
+      JSONObject ste = new JSONObject(jsonString);
+      String className = ste.getString("className");
+      String fileName = ste.getString("fileName");
+      String methodName = ste.getString("methodName");
+      int lineNumber = Integer.parseInt(ste.getString("lineNumber"));
+      return new StackTraceElement(className, methodName, fileName, lineNumber);
+    }
+
+    @Override
+    public String toString() {
+      return msg != null ? type + ": " + msg : type;
+    }
   }
 }
diff --git a/user/src/com/google/gwt/user/client/rpc/core/java/util/logging/LogRecord_CustomFieldSerializer.java b/user/src/com/google/gwt/user/client/rpc/core/java/util/logging/LogRecord_CustomFieldSerializer.java
index 8aa785d..d807d6c 100644
--- a/user/src/com/google/gwt/user/client/rpc/core/java/util/logging/LogRecord_CustomFieldSerializer.java
+++ b/user/src/com/google/gwt/user/client/rpc/core/java/util/logging/LogRecord_CustomFieldSerializer.java
@@ -16,7 +16,7 @@
 
 package com.google.gwt.user.client.rpc.core.java.util.logging;
 
-import com.google.gwt.core.client.impl.SerializableThrowable;
+import com.google.gwt.core.shared.SerializableThrowable;
 import com.google.gwt.user.client.rpc.SerializationException;
 import com.google.gwt.user.client.rpc.SerializationStreamReader;
 import com.google.gwt.user.client.rpc.SerializationStreamWriter;
@@ -28,43 +28,37 @@
  * Custom serializer for LogRecord.
  */
 public class LogRecord_CustomFieldSerializer {
-  public static void deserialize(SerializationStreamReader reader,
-      LogRecord instance) throws SerializationException {
+
+  public static void deserialize(SerializationStreamReader reader, LogRecord instance)
+      throws SerializationException {
     String loggerName = reader.readString();
     Long millis = reader.readLong();
-    Object throwable = reader.readObject();
+    SerializableThrowable thrown = (SerializableThrowable) reader.readObject();
 
     instance.setLoggerName(loggerName);
     instance.setMillis(millis);
-    if (throwable != null && throwable instanceof SerializableThrowable) {
-      instance.setThrown(((SerializableThrowable) throwable).getThrowable());
-    }
+    instance.setThrown(thrown);
   }
 
   public static LogRecord instantiate(SerializationStreamReader reader)
-  throws SerializationException {
-    // Note: Fields should be read in the same order that they were written.
+      throws SerializationException {
     String levelString = reader.readString();
     String msg = reader.readString();
-    
+
     Level level = Level.parse(levelString);
     LogRecord toReturn = new LogRecord(level, msg);
     return toReturn;
   }
 
-  public static void serialize(SerializationStreamWriter writer,
-      LogRecord lr) throws SerializationException {
-    // Although Level is serializable, the Level in LogRecord is actually a
-    // LevelWithExposedConstructor, which serialization does not like, so we
+  public static void serialize(SerializationStreamWriter writer, LogRecord lr)
+      throws SerializationException {
+    // Although Level is serializable, the Level in LogRecord is actually
+    // extending Level, which serialization does not like, so we
     // manually just serialize the name.
     writer.writeString(lr.getLevel().getName());
     writer.writeString(lr.getMessage());
     writer.writeString(lr.getLoggerName());
     writer.writeLong(lr.getMillis());
-    if (lr.getThrown() != null) {
-      writer.writeObject(new SerializableThrowable(lr.getThrown()));
-    } else {
-      writer.writeObject(null);
-    }
+    writer.writeObject(SerializableThrowable.fromThrowable(lr.getThrown()));
   }
 }
diff --git a/user/src/com/google/gwt/user/server/rpc/core/java/util/logging/LogRecord_ServerCustomFieldSerializer.java b/user/src/com/google/gwt/user/server/rpc/core/java/util/logging/LogRecord_ServerCustomFieldSerializer.java
deleted file mode 100644
index 26d6113..0000000
--- a/user/src/com/google/gwt/user/server/rpc/core/java/util/logging/LogRecord_ServerCustomFieldSerializer.java
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright 2011 Google Inc.
- * 
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- * 
- * http://www.apache.org/licenses/LICENSE-2.0
- * 
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.google.gwt.user.server.rpc.core.java.util.logging;
-
-import com.google.gwt.core.client.impl.SerializableThrowable;
-import com.google.gwt.user.client.rpc.SerializationException;
-import com.google.gwt.user.client.rpc.SerializationStreamReader;
-import com.google.gwt.user.client.rpc.SerializationStreamWriter;
-import com.google.gwt.user.client.rpc.core.java.util.logging.LogRecord_CustomFieldSerializer;
-import com.google.gwt.user.server.rpc.ServerCustomFieldSerializer;
-import com.google.gwt.user.server.rpc.impl.DequeMap;
-import com.google.gwt.user.server.rpc.impl.ServerSerializationStreamReader;
-
-import java.lang.reflect.Type;
-import java.lang.reflect.TypeVariable;
-import java.util.logging.LogRecord;
-
-/**
- * Custom serializer for LogRecord.
- */
-public class LogRecord_ServerCustomFieldSerializer extends ServerCustomFieldSerializer<LogRecord> {
-  @SuppressWarnings("unused")
-  public static void deserialize(ServerSerializationStreamReader streamReader, LogRecord instance,
-      Type[] expectedParameterTypes, DequeMap<TypeVariable<?>, Type> resolvedTypes) throws
-      SerializationException {
-    String loggerName = streamReader.readString();
-    Long millis = streamReader.readLong();
-    Object throwable = streamReader.readObject(SerializableThrowable.class, resolvedTypes);
-
-    instance.setLoggerName(loggerName);
-    instance.setMillis(millis);
-    if (throwable != null && throwable instanceof SerializableThrowable) {
-      instance.setThrown(((SerializableThrowable) throwable).getThrowable());
-    }
-  }
-
-  @Override
-  public void deserializeInstance(SerializationStreamReader streamReader, LogRecord instance)
-      throws SerializationException {
-    LogRecord_CustomFieldSerializer.deserialize(streamReader, instance);
-  }
-
-  @Override
-  public void deserializeInstance(ServerSerializationStreamReader streamReader, LogRecord instance,
-      Type[] expectedParameterTypes, DequeMap<TypeVariable<?>, Type> resolvedTypes) throws
-      SerializationException {
-    deserialize(streamReader, instance, expectedParameterTypes, resolvedTypes);
-  }
-
-  @Override
-  public boolean hasCustomInstantiateInstance() {
-    return true;
-  }
-
-  @Override
-  public LogRecord instantiateInstance(ServerSerializationStreamReader reader,
-      Type[] expectedParameterTypes, DequeMap<TypeVariable<?>, Type> resolvedTypes) throws
-      SerializationException {
-    return LogRecord_CustomFieldSerializer.instantiate(reader);
-  }
-
-  @Override
-  public void serializeInstance(SerializationStreamWriter writer, LogRecord lr)
-      throws SerializationException {
-    LogRecord_CustomFieldSerializer.serialize(writer, lr);
-  }
-}
diff --git a/user/super/com/google/gwt/emul/java/lang/Throwable.java b/user/super/com/google/gwt/emul/java/lang/Throwable.java
index 3350c81..dd9ecb9 100644
--- a/user/super/com/google/gwt/emul/java/lang/Throwable.java
+++ b/user/super/com/google/gwt/emul/java/lang/Throwable.java
@@ -119,7 +119,7 @@
       }
       out.println(t);
       for (StackTraceElement element : t.getStackTrace()) {
-        out.println("\tat" + element);
+        out.println("\tat " + element);
       }
     }
   }
diff --git a/user/super/com/google/gwt/emul/java/util/logging/Level.java b/user/super/com/google/gwt/emul/java/util/logging/Level.java
index ea0781e..23c67ed 100644
--- a/user/super/com/google/gwt/emul/java/util/logging/Level.java
+++ b/user/super/com/google/gwt/emul/java/util/logging/Level.java
@@ -74,11 +74,6 @@
     @Override public int intValue() { return Integer.MAX_VALUE; }
   }
 
-  private static class LevelNull extends Level {
-    @Override public String getName() { return null; }
-    @Override public int intValue() { return -1; }
-  }
-  
   private static class LevelSevere extends Level {
     @Override public String getName() { return "SEVERE"; }
     @Override public int intValue() { return 1000; }
diff --git a/user/super/com/google/gwt/emul/java/util/logging/LogRecord.java b/user/super/com/google/gwt/emul/java/util/logging/LogRecord.java
index eea80d7..88ff814 100644
--- a/user/super/com/google/gwt/emul/java/util/logging/LogRecord.java
+++ b/user/super/com/google/gwt/emul/java/util/logging/LogRecord.java
@@ -16,26 +16,23 @@
 
 package java.util.logging;
 
-import com.google.gwt.core.client.impl.SerializableThrowable;
+import com.google.gwt.core.shared.SerializableThrowable;
 
 import java.io.Serializable;
 import java.util.Date;
 
 /**
- *  An emulation of the java.util.logging.LogRecord class. See 
- *  <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/logging/LogRecord.html"> 
+ *  An emulation of the java.util.logging.LogRecord class. See
+ *  <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/util/logging/LogRecord.html">
  *  The Java API doc for details</a>
  */
 public class LogRecord implements Serializable {
   private Level level;
   private String loggerName = "";
   private String msg;
-  private Throwable thrown = null;
+  private SerializableThrowable thrown = null;
   private long millis;
   
-  // DUMMY - Used to trigger serialization
-  private SerializableThrowable dummy = null;
-  
   public LogRecord(Level level, String msg) {
     this.level = level;
     this.msg = msg;
@@ -83,7 +80,7 @@
   }
 
   public void setThrown(Throwable newThrown) {
-    thrown = newThrown;
+    thrown = SerializableThrowable.fromThrowable(newThrown);
   }
 
   /* Not Implemented */
diff --git a/user/test/com/google/gwt/logging/LoggingSuite.java b/user/test/com/google/gwt/logging/LoggingSuite.java
new file mode 100644
index 0000000..b58011d
--- /dev/null
+++ b/user/test/com/google/gwt/logging/LoggingSuite.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.logging;
+
+import com.google.gwt.junit.tools.GWTTestSuite;
+import com.google.gwt.logging.client.StackTracePrintStreamTest;
+
+import junit.framework.Test;
+
+/**
+ * Logging tests.
+ */
+public class LoggingSuite {
+
+  public static Test suite() {
+    GWTTestSuite suite = new GWTTestSuite("Test suite for logging GWTTestCases");
+    suite.addTestSuite(StackTracePrintStreamTest.class);
+    return suite;
+  }
+}
diff --git a/user/test/com/google/gwt/logging/client/StackTracePrintStreamTest.java b/user/test/com/google/gwt/logging/client/StackTracePrintStreamTest.java
new file mode 100644
index 0000000..9501813
--- /dev/null
+++ b/user/test/com/google/gwt/logging/client/StackTracePrintStreamTest.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2013 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+package com.google.gwt.logging.client;
+
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.logging.impl.StackTracePrintStream;
+
+/**
+ * Tests {@link StackTracePrintStream}.
+ */
+public class StackTracePrintStreamTest extends GWTTestCase {
+  @Override
+  public String getModuleName() {
+    return "com.google.gwt.logging.Logging";
+  }
+
+  public void testPrintStackTrace() throws Exception {
+    StringBuilder actual = new StringBuilder();
+    createTestException().printStackTrace(new StackTracePrintStream(actual));
+
+    StringBuilder expected = new StringBuilder();
+    expected.append("custom msg\n");
+    expected.append("\tat c1.m1(f1:1)\n");
+    expected.append("\tat c2.m2(f2:2)\n");
+    expected.append("Caused by: custom msg cause\n");
+    expected.append("\tat c3.m3(f3:3)\n");
+    expected.append("\tat c4.m4(f4:4)\n");
+
+    assertEquals(expected.toString(), actual.toString());
+  }
+
+  private Throwable createTestException() {
+    Exception exception = new Exception() {
+      @Override
+      public String toString() {
+        return "custom msg";
+      }
+    };
+    exception.setStackTrace(new StackTraceElement[] {
+        new StackTraceElement("c1", "m1", "f1", 1), new StackTraceElement("c2", "m2", "f2", 2)});
+
+    Exception cause = new Exception() {
+      @Override
+      public String toString() {
+        return "custom msg cause";
+      }
+    };
+    cause.setStackTrace(new StackTraceElement[] {
+        new StackTraceElement("c3", "m3", "f3", 3), new StackTraceElement("c4", "m4", "f4", 4)});
+
+    return exception.initCause(cause);
+  }
+}