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("<", "<");
text = text.replaceAll(">", ">");
- // but allow the line breaks that we put in ourselves
- text = text.replaceAll(newline, "<br>");
+ text = text.replaceAll("\t", " ");
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);
+ }
+}