Refactors c.g.gwt.junit to use common SerializableThrowable & StacktraceDeobfuscator.

- Gets rid of duplicate code in junit for deobfuscation of stack traces
- Improves the messaging of unserializable exceptions
- Minor improvements to the reporting of test infra failures

Change-Id: I1e1021bc99ac88ea6d9d47c3d23c83e79a896213
Review-Link: https://gwt-review.googlesource.com/#/c/2290/

Review by: skybrian@google.com

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@11581 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/core/client/JavaScriptException.java b/user/src/com/google/gwt/core/client/JavaScriptException.java
index 5c44f48..8f15edd 100644
--- a/user/src/com/google/gwt/core/client/JavaScriptException.java
+++ b/user/src/com/google/gwt/core/client/JavaScriptException.java
@@ -128,7 +128,7 @@
       StackTraceCreator.createStackTrace(this);
     }
   }
-  
+
   public JavaScriptException(String name, String description) {
     this.message = "JavaScript " + name + " exception: " + description;
     this.name = name;
@@ -137,10 +137,8 @@
   }
 
   /**
-   * Used for server-side instantiation during JUnit runs. Exceptions are
-   * manually marshaled through
-   * <code>com.google.gwt.junit.client.impl.ExceptionWrapper</code> objects.
-   * 
+   * Used for testing instantiations.
+   *
    * @param message the detail message
    */
   protected JavaScriptException(String message) {
diff --git a/user/src/com/google/gwt/junit/JUnitMessageQueue.java b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
index e2c4bf7..044d697 100644
--- a/user/src/com/google/gwt/junit/JUnitMessageQueue.java
+++ b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
@@ -16,10 +16,10 @@
 package com.google.gwt.junit;
 
 import com.google.gwt.junit.client.TimeoutException;
-import com.google.gwt.junit.client.impl.JUnitResult;
 import com.google.gwt.junit.client.impl.JUnitHost.ClientInfo;
 import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
+import com.google.gwt.junit.client.impl.JUnitResult;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -27,8 +27,8 @@
 import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.Map.Entry;
+import java.util.Set;
 
 /**
  * A message queue to pass data between {@link JUnitShell} and
@@ -95,15 +95,6 @@
     }
   }
 
-  private static final Set<Class<? extends Throwable>> THROWABLES_NOT_RETRIED = createThrowablesNotRetried();
-
-  private static Set<Class<? extends Throwable>> createThrowablesNotRetried() {
-    Set<Class<? extends Throwable>> throwableSet = new HashSet<Class<? extends Throwable>>();
-    throwableSet.add(com.google.gwt.junit.JUnitFatalLaunchException.class);
-    throwableSet.add(java.lang.Error.class);
-    return throwableSet;
-  }
-
   /**
    * Records results for each client; must lock before accessing.
    */
@@ -463,14 +454,20 @@
       if (result == null) {
         return true;
       }
-      Throwable exception = result.getException();
-      if (exception != null && !isMember(exception, THROWABLES_NOT_RETRIED)) {
+
+      if (isNonFatalFailure(result)) {
         return true;
       }
     }
     return false;
   }
 
+  private boolean isNonFatalFailure(JUnitResult result) {
+    return result.isAnyException()
+        && !result.isExceptionOf(Error.class)
+        && !result.isExceptionOf(JUnitFatalLaunchException.class);
+  }
+
   void removeResults(TestInfo testInfo) {
     synchronized (clientStatusesLock) {
       testResults.remove(testInfo);
@@ -520,14 +517,4 @@
     }
     return results;
   }
-
-  private boolean isMember(Throwable exception,
-      Set<Class<? extends Throwable>> throwableSet) {
-    for (Class<? extends Throwable> throwable : throwableSet) {
-      if (throwable.isInstance(exception)) {
-        return true;
-      }
-    }
-    return false;
-  }
 }
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index 3ae26a9..2555991 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -22,6 +22,7 @@
 import com.google.gwt.core.ext.linker.impl.StandardLinkerContext;
 import com.google.gwt.core.ext.typeinfo.JClassType;
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.core.shared.SerializableThrowable;
 import com.google.gwt.dev.ArgProcessorBase;
 import com.google.gwt.dev.Compiler;
 import com.google.gwt.dev.DevMode;
@@ -1224,41 +1225,28 @@
         + " != " + messageQueue.getNumClients();
 
     for (Entry<ClientStatus, JUnitResult> entry : results.entrySet()) {
-      ClientStatus client = entry.getKey();
       JUnitResult result = entry.getValue();
       assert (result != null);
-      Throwable exception = result.getException();
 
-      // Let the user know the browser in which the failure happened.
-      if (exception != null) {
-        String msg = "Remote test failed at " + client.getDesc();
-        if (exception instanceof AssertionFailedError) {
-          String oldMessage = exception.getMessage();
-          if (oldMessage != null) {
-            msg += "\n" + exception.getMessage();
-          }
-          AssertionFailedError newException = new AssertionFailedError(msg);
-          newException.setStackTrace(exception.getStackTrace());
-          newException.initCause(exception.getCause());
-          exception = newException;
+      if (result.isAnyException()) {
+        if (result.isExceptionOf(AssertionFailedError.class)) {
+          testResult.addFailure(testCase, toAssertionFailedError(result.getException()));
         } else {
-          exception = new RuntimeException(msg, exception);
+          testResult.addError(testCase, result.getException());
         }
       }
-
-      // A "successful" failure.
-      if (exception instanceof AssertionFailedError) {
-        testResult.addFailure(testCase, (AssertionFailedError) exception);
-      } else if (exception != null) {
-        // A real failure
-        if (exception instanceof JUnitFatalLaunchException) {
-          lastLaunchFailed = true;
-        }
-        testResult.addError(testCase, exception);
-      }
     }
   }
 
+  private AssertionFailedError toAssertionFailedError(SerializableThrowable thrown) {
+    AssertionFailedError error = new AssertionFailedError(thrown.getMessage());
+    error.setStackTrace(thrown.getStackTrace());
+    if (thrown.getCause() != null) {
+      error.initCause(thrown.getCause());
+    }
+    return error;
+  }
+
   private void runTestImpl(GWTTestCase testCase, TestResult testResult)
       throws UnableToCompleteException {
     runTestImpl(testCase, testResult, 0);
diff --git a/user/src/com/google/gwt/junit/client/impl/ExceptionWrapper.java b/user/src/com/google/gwt/junit/client/impl/ExceptionWrapper.java
deleted file mode 100644
index 8a46f3f..0000000
--- a/user/src/com/google/gwt/junit/client/impl/ExceptionWrapper.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright 2006 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.junit.client.impl;
-
-import java.io.Serializable;
-
-/**
- * Wraps a {@link Throwable}, and explicitly serializes cause and stack trace.
- */
-final class ExceptionWrapper implements Serializable {
-
-  /**
-   * Stand-in for the transient {@link Throwable#getCause()} in GWT JRE.
-   */
-  ExceptionWrapper causeWrapper;
-
-  /**
-   * The wrapped exception.
-   */
-  Throwable exception;
-
-  /**
-   * Stand-in for the transient {@link Throwable#getStackTrace()} in GWT JRE.
-   */
-  StackTraceElement[] stackTrace;
-
-  /**
-   * If true, the exception's inner stack trace and cause have been initialized.
-   * Defaults to false immediate after deserialization.
-   */
-  private transient boolean isExceptionInitialized;
-
-  /**
-   * Creates an {@link ExceptionWrapper} around an existing {@link Throwable}.
-   * 
-   * @param exception the {@link Throwable} to wrap.
-   */
-  public ExceptionWrapper(Throwable exception) {
-    this.exception = exception;
-    this.stackTrace = exception.getStackTrace();
-    Throwable cause = exception.getCause();
-    if (cause != null) {
-      this.causeWrapper = new ExceptionWrapper(cause);
-    }
-    this.isExceptionInitialized = true;
-  }
-
-  /**
-   * Deserialization constructor.
-   */
-  ExceptionWrapper() {
-    this.isExceptionInitialized = false;
-  }
-
-  public Throwable getException() {
-    if (!isExceptionInitialized) {
-      exception.setStackTrace(stackTrace);
-      if (causeWrapper != null) {
-        exception.initCause(causeWrapper.getException());
-      }
-      isExceptionInitialized = true;
-    }
-    return exception;
-  }
-}
-
diff --git a/user/src/com/google/gwt/junit/client/impl/JUnitResult.java b/user/src/com/google/gwt/junit/client/impl/JUnitResult.java
index 8d777d9..9d6476e 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitResult.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitResult.java
@@ -15,6 +15,12 @@
  */
 package com.google.gwt.junit.client.impl;
 
+import com.google.gwt.core.shared.GwtIncompatible;
+import com.google.gwt.core.shared.SerializableThrowable;
+import com.google.gwt.junit.client.TimeoutException;
+
+import junit.framework.AssertionFailedError;
+
 import java.io.Serializable;
 
 /**
@@ -33,7 +39,7 @@
   /**
    * If non-null, an exception that occurred during the run.
    */
-  ExceptionWrapper exceptionWrapper;
+  SerializableThrowable thrown;
 
   // Computed at the server, via HTTP header.
   private transient String agent;
@@ -45,8 +51,22 @@
     return agent;
   }
 
-  public Throwable getException() {
-    return (exceptionWrapper == null) ? null : exceptionWrapper.getException();
+  public SerializableThrowable getException() {
+    return thrown;
+  }
+
+  public boolean isAnyException() {
+    return thrown != null;
+  }
+
+  @GwtIncompatible
+  public boolean isExceptionOf(Class<?> expectedException) {
+    try {
+      return thrown == null ? false
+          : expectedException.isAssignableFrom(Class.forName(thrown.getDesignatedType()));
+    } catch (Exception e) {
+      return false;
+    }
   }
 
   public String getHost() {
@@ -58,7 +78,11 @@
   }
 
   public void setException(Throwable exception) {
-    this.exceptionWrapper = new ExceptionWrapper(exception);
+    thrown = SerializableThrowable.fromThrowable(exception);
+    // Try to improve exception message if there is no class metadata available
+    if (!thrown.isExactDesignatedTypeKnown()) {
+      improveDesignatedType(thrown, exception);
+    }
   }
 
   public void setHost(String host) {
@@ -67,11 +91,19 @@
 
   @Override
   public String toString() {
-    return "TestResult {" + toStringInner() + "}";
+    return "TestResult {thrown: " + thrown + ", agent: " + agent + ", host: " + host + "}";
   }
 
-  protected String toStringInner() {
-    return "exceptionWrapper: " + exceptionWrapper + ", agent: " + agent
-        + ", host: " + host;
+  /**
+   * Returns best effort type info by checking against some common exceptions for unit tests.
+   */
+  private static void improveDesignatedType(SerializableThrowable t, Throwable designatedType) {
+    if (designatedType instanceof AssertionFailedError) {
+      String className = "junit.framework.AssertionFailedError";
+      t.setDesignatedType(className, AssertionFailedError.class == designatedType.getClass());
+    } else if (designatedType instanceof TimeoutException) {
+      String className = "com.google.gwt.junit.client.TimeoutException";
+      t.setDesignatedType(className, TimeoutException.class == designatedType.getClass());
+    }
   }
 }
\ No newline at end of file
diff --git a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
index 4708d35..e7e0f9b 100644
--- a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
+++ b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
@@ -15,8 +15,7 @@
  */
 package com.google.gwt.junit.server;
 
-import com.google.gwt.dev.util.JsniRef;
-import com.google.gwt.dev.util.StringKey;
+import com.google.gwt.core.server.impl.StackTraceDeobfuscator;
 import com.google.gwt.junit.JUnitFatalLaunchException;
 import com.google.gwt.junit.JUnitMessageQueue;
 import com.google.gwt.junit.JUnitMessageQueue.ClientInfoExt;
@@ -24,16 +23,13 @@
 import com.google.gwt.junit.client.TimeoutException;
 import com.google.gwt.junit.client.impl.JUnitHost;
 import com.google.gwt.junit.client.impl.JUnitResult;
+import com.google.gwt.junit.linker.JUnitSymbolMapsLinker;
 import com.google.gwt.user.client.rpc.InvocationException;
 import com.google.gwt.user.server.rpc.HybridServiceServlet;
 import com.google.gwt.user.server.rpc.RPCServletUtils;
 
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
 import java.util.HashMap;
-import java.util.Map;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.servlet.ServletException;
@@ -47,18 +43,6 @@
  */
 public class JUnitHostImpl extends HybridServiceServlet implements JUnitHost {
 
-  private static class StrongName extends StringKey {
-    protected StrongName(String value) {
-      super(value);
-    }
-  }
-
-  private static class SymbolName extends StringKey {
-    protected SymbolName(String value) {
-      super(value);
-    }
-  }
-
   /**
    * A hook into GWTUnitTestShell, the underlying unit test process.
    */
@@ -91,7 +75,7 @@
     return sHost;
   }
 
-  private Map<StrongName, Map<SymbolName, String>> symbolMaps = new HashMap<StrongName, Map<SymbolName, String>>();
+  private StackTraceDeobfuscator deobfuscator;
 
   public InitialResponse getTestBlock(int blockIndex, ClientInfo clientInfo)
       throws TimeoutException {
@@ -113,7 +97,6 @@
       ClientInfo clientInfo) throws TimeoutException {
     for (JUnitResult result : results.values()) {
       initResult(getThreadLocalRequest(), result);
-      resymbolize(result.getException());
     }
     JUnitMessageQueue host = getHost();
     ClientInfoExt clientInfoExt = createClientInfo(clientInfo,
@@ -172,83 +155,28 @@
   }
 
   private void initResult(HttpServletRequest request, JUnitResult result) {
-    String agent = request.getHeader("User-Agent");
-    result.setAgent(agent);
-    String machine = request.getRemoteHost();
-    result.setHost(machine);
+    result.setAgent(request.getHeader("User-Agent"));
+    result.setHost(request.getRemoteHost());
+    Throwable throwable = result.getException();
+    if (throwable != null) {
+      deobfuscateStackTrace(throwable);
+    }
   }
 
-  private synchronized Map<SymbolName, String> loadSymbolMap(
-      StrongName strongName) {
-    Map<SymbolName, String> toReturn = symbolMaps.get(strongName);
-    if (toReturn != null) {
-      return toReturn;
-    }
-    toReturn = new HashMap<SymbolName, String>();
-
-    /*
-     * Collaborate with SymbolMapsLinker for the location of the symbol data
-     * because the -aux directory isn't accessible via the servlet context.
-     */
-    String path = getRequestModuleBasePath() + "/.junit_symbolMaps/"
-        + strongName.get() + ".symbolMap";
-    InputStream in = getServletContext().getResourceAsStream(path);
-    if (in == null) {
-      symbolMaps.put(strongName, null);
-      return null;
-    }
-
-    BufferedReader bin = new BufferedReader(new InputStreamReader(in));
-    String line;
+  private void deobfuscateStackTrace(Throwable throwable) {
     try {
-      try {
-        while ((line = bin.readLine()) != null) {
-          if (line.charAt(0) == '#') {
-            continue;
-          }
-          int idx = line.indexOf(',');
-          toReturn.put(new SymbolName(line.substring(0, idx)),
-                       line.substring(idx + 1));
-        }
-      } finally {
-        bin.close();
-      }
+      getDeobfuscator().deobfuscateStackTrace(throwable, getPermutationStrongName());
     } catch (IOException e) {
-      toReturn = null;
+      System.err.println("Unable to deobfuscate a stack trace due to an error:");
+      e.printStackTrace();
     }
-
-    symbolMaps.put(strongName, toReturn);
-    return toReturn;
   }
 
-  /**
-   * Resymbolizes a trace from obfuscated symbols to Java names.
-   */
-  private void resymbolize(Throwable exception) {
-    if (exception == null) {
-      return;
+  private StackTraceDeobfuscator getDeobfuscator() throws IOException {
+    if (deobfuscator == null) {
+      String path = getRequestModuleBasePath() + "/" + JUnitSymbolMapsLinker.SYMBOL_MAP_DIR;
+      deobfuscator = StackTraceDeobfuscator.fromUrl(getServletContext().getResource(path));
     }
-    StackTraceElement[] stackTrace = exception.getStackTrace();
-    StrongName strongName = new StrongName(getPermutationStrongName());
-    Map<SymbolName, String> map = loadSymbolMap(strongName);
-    if (map == null) {
-      return;
-    }
-    for (int i = 0; i < stackTrace.length; ++i) {
-      StackTraceElement ste = stackTrace[i];
-      String symbolData = map.get(new SymbolName(ste.getMethodName()));
-      if (symbolData != null) {
-        // jsniIdent, className, memberName, sourceUri, sourceLine
-        String[] parts = symbolData.split(",");
-        assert parts.length == 6 : "Expected 6, have " + parts.length;
-
-        JsniRef ref = JsniRef.parse(parts[0].substring(0,
-            parts[0].lastIndexOf(')') + 1));
-        stackTrace[i] = new StackTraceElement(ref.className(),
-            ref.memberName(), ste.getFileName(), ste.getLineNumber());
-      }
-    }
-    exception.setStackTrace(stackTrace);
-    resymbolize(exception.getCause());
+    return deobfuscator;
   }
 }
diff --git a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
index 9d9dd8a..fac71a8 100644
--- a/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
+++ b/user/super/com/google/gwt/junit/translatable/com/google/gwt/junit/client/impl/GWTRunner.java
@@ -28,9 +28,6 @@
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.rpc.SerializationException;
-import com.google.gwt.user.client.rpc.SerializationStreamFactory;
-import com.google.gwt.user.client.rpc.SerializationStreamWriter;
 import com.google.gwt.user.client.rpc.ServiceDefTarget;
 
 import java.util.HashMap;
@@ -243,8 +240,6 @@
     if (failureMessage != null) {
       RuntimeException ex = new RuntimeException(failureMessage);
       result.setException(ex);
-    } else if (result.exceptionWrapper != null) {
-      ensureSerializable(result.exceptionWrapper);
     }
     TestInfo currentTest = getCurrentTest();
     currentResults.put(currentTest, result);
@@ -262,25 +257,6 @@
   }
 
   /**
-   * Convert unserializable exceptions into generic serializable ones.
-   */
-  private void ensureSerializable(ExceptionWrapper wrapper) {
-    if (wrapper == null) {
-      return;
-    }
-
-    ensureSerializable(wrapper.causeWrapper);
-    try {
-      SerializationStreamFactory fac = (SerializationStreamFactory) junitHost;
-      SerializationStreamWriter dummyWriter = fac.createStreamWriter();
-      dummyWriter.writeObject(wrapper.exception);
-    } catch (SerializationException e) {
-      wrapper.exception = new Exception(wrapper.exception.toString() +
-          " (unserializable exception)");
-    }
-  }
-
-  /**
    * Executes a test on provided test class instance.
    */
   public void executeTestMethod(GWTTestCase testCase, String className, String methodName)
diff --git a/user/test/com/google/gwt/junit/GwtTestSuiteWithExpectedFailures.java b/user/test/com/google/gwt/junit/GwtTestSuiteWithExpectedFailures.java
index 2b3ebf4..7c72b82 100644
--- a/user/test/com/google/gwt/junit/GwtTestSuiteWithExpectedFailures.java
+++ b/user/test/com/google/gwt/junit/GwtTestSuiteWithExpectedFailures.java
@@ -20,7 +20,8 @@
 import junit.framework.TestResult;
 
 /**
- * A {@link GWTTestSuite} that can interpret {@link ExpectedFailure} on test methods.
+ * A {@link GWTTestSuite} that can interpret {@link com.google.gwt.junit.client.ExpectedFailure} on
+ * test methods.
  */
 class GwtTestSuiteWithExpectedFailures extends GWTTestSuite {
 
diff --git a/user/test/com/google/gwt/junit/JUnitSuite.java b/user/test/com/google/gwt/junit/JUnitSuite.java
index fb684c0..9d2a7b4 100644
--- a/user/test/com/google/gwt/junit/JUnitSuite.java
+++ b/user/test/com/google/gwt/junit/JUnitSuite.java
@@ -18,6 +18,7 @@
 import com.google.gwt.junit.client.DevModeOnCompiledScriptTest;
 import com.google.gwt.junit.client.GWTTestCaseAsyncTest;
 import com.google.gwt.junit.client.GWTTestCaseSetupTearDownTest;
+import com.google.gwt.junit.client.GWTTestCaseStackTraceTest;
 import com.google.gwt.junit.client.GWTTestCaseTest;
 import com.google.gwt.junit.client.GWTTestCaseUncaughtExceptionHandlerTest;
 import com.google.gwt.junit.client.PropertyDefiningGWTTest;
@@ -33,6 +34,7 @@
     TestSuite suite = new GwtTestSuiteWithExpectedFailures("Test suite for com.google.gwt.junit");
 
     suite.addTestSuite(GWTTestCaseTest.class);
+    suite.addTestSuite(GWTTestCaseStackTraceTest.class);
     suite.addTestSuite(GWTTestCaseUncaughtExceptionHandlerTest.class);
     suite.addTest(new TestSuiteWithOrder(GWTTestCaseAsyncTest.class));
     suite.addTest(new TestSuiteWithOrder(GWTTestCaseSetupTearDownTest.class));
diff --git a/user/test/com/google/gwt/junit/TestResultWithExpectedFailures.java b/user/test/com/google/gwt/junit/TestResultWithExpectedFailures.java
index c97846a..4e84625 100644
--- a/user/test/com/google/gwt/junit/TestResultWithExpectedFailures.java
+++ b/user/test/com/google/gwt/junit/TestResultWithExpectedFailures.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.junit;
 
+import com.google.gwt.junit.client.ExpectedFailure;
+
 import junit.framework.AssertionFailedError;
 import junit.framework.Test;
 import junit.framework.TestCase;
@@ -42,19 +44,21 @@
   @Override
   public void addFailure(Test test, AssertionFailedError t) {
     failed = true;
-    if (isAnExpectedException(test, t)) {
-      return; // This is a good kind of failure, do not report it.
+    if (isTestExpectedToFail(test)) {
+      processException(test, t);
+    } else {
+      super.addFailure(test, t);
     }
-    super.addFailure(test, t);
   }
 
   @Override
   public void addError(Test test, Throwable t) {
     failed = true;
-    if (isAnExpectedException(test, t)) {
-      return; // This is a good kind of failure, do not report it.
+    if (isTestExpectedToFail(test)) {
+      processException(test, t);
+    } else {
+      super.addError(test, t);
     }
-    super.addError(test, t);
   }
 
   @Override
@@ -71,27 +75,20 @@
     return getExpectedFailureAnnotation(test) != null;
   }
 
-  private boolean isAnExpectedException(Test test, Throwable t) {
+  private void processException(Test test, Throwable t) {
     ExpectedFailure annotation = getExpectedFailureAnnotation(test);
-    if (annotation != null) {
-      t = normalizeGwtTestException(t);
-      return annotation.withType().isAssignableFrom(t.getClass())
-          && getExceptionMessage(t).contains(annotation.withMessage());
+    try {
+      annotation.withAsserter().newInstance().assertException(annotation, t);
+    } catch (AssertionFailedError e) {
+      String msg = "Assertion failed for thrown exception: " + e.getMessage()
+          + "\n(Actual thrown exception is reported below via 'caused by')";
+      AssertionFailedError errorToReport = new AssertionFailedError(msg);
+      errorToReport.initCause(t);
+      errorToReport.setStackTrace(e.getStackTrace());
+      super.addFailure(test, errorToReport);
+    } catch (Exception e) {
+      super.addError(test, e);
     }
-    return false;
-  }
-
-  /**
-   * Extracts the real exception from the {@code RuntimeException} thrown by GwtTestCase.
-   */
-  private Throwable normalizeGwtTestException(Throwable t) {
-    // GWTTestCase replaces AssertionFailedError with RuntimeException and for all other exceptions
-    // it puts them into 'cause' property.
-    return t.getCause() == null ? new AssertionFailedError(t.getMessage()) : t.getCause();
-  }
-
-  private String getExceptionMessage(Throwable t) {
-    return t.getMessage() == null ? "" : t.getMessage();
   }
 
   private ExpectedFailure getExpectedFailureAnnotation(Test test) {
diff --git a/user/test/com/google/gwt/junit/TestSuiteWithOrder.java b/user/test/com/google/gwt/junit/TestSuiteWithOrder.java
index 459b477..86c77fc 100644
--- a/user/test/com/google/gwt/junit/TestSuiteWithOrder.java
+++ b/user/test/com/google/gwt/junit/TestSuiteWithOrder.java
@@ -37,6 +37,7 @@
    */
 
   public TestSuiteWithOrder(Class<? extends TestCase> clazz) {
+    super(clazz.getName());
     for (Class<?> c = clazz; Test.class.isAssignableFrom(c); c = c.getSuperclass()) {
       for (Method each : getDeclaredMethods(c)) {
         if (isTestMethod(each)) {
diff --git a/user/test/com/google/gwt/junit/client/DefaultExceptionAsserter.java b/user/test/com/google/gwt/junit/client/DefaultExceptionAsserter.java
new file mode 100644
index 0000000..38b176a
--- /dev/null
+++ b/user/test/com/google/gwt/junit/client/DefaultExceptionAsserter.java
@@ -0,0 +1,64 @@
+/*
+ * 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.junit.client;
+
+import com.google.gwt.core.shared.GwtIncompatible;
+import com.google.gwt.core.shared.SerializableThrowable;
+
+import junit.framework.Assert;
+
+/**
+ * A default {@link ExceptionAsserter} that checks exception type and message.
+ */
+public class DefaultExceptionAsserter extends Assert implements ExceptionAsserter {
+
+  @GwtIncompatible
+  @Override
+  public void assertException(ExpectedFailure annotation, Throwable actual) {
+    assertAssignable(annotation.withType(), getExceptionClass(actual));
+    assertMessageContains(annotation.withMessage(), getExceptionMessage(actual));
+  }
+
+  private static void assertMessageContains(String expected, String actual) {
+    if (!actual.contains(expected)) {
+      fail("expected message: " + expected + " in: " + actual);
+    }
+  }
+
+  @GwtIncompatible
+  private static void assertAssignable(Class<?> expected, Class<?> exceptionClass) {
+    if (!expected.isAssignableFrom(exceptionClass)) {
+      fail("expected subclass of: " + expected + " found: " + exceptionClass);
+    }
+  }
+
+  @GwtIncompatible
+  private Class<?> getExceptionClass(Throwable t) {
+    if (t instanceof SerializableThrowable) {
+      try {
+        SerializableThrowable throwableWithClassName = (SerializableThrowable) t;
+        return Class.forName(throwableWithClassName.getDesignatedType());
+      } catch (Exception e) {
+        // Nothing to do here, just fallback to #getClass
+      }
+    }
+    return t.getClass();
+  }
+
+  private String getExceptionMessage(Throwable t) {
+    return t.getMessage() == null ? "" : t.getMessage();
+  }
+}
\ No newline at end of file
diff --git a/user/test/com/google/gwt/junit/client/ExceptionAsserter.java b/user/test/com/google/gwt/junit/client/ExceptionAsserter.java
new file mode 100644
index 0000000..1ee2587
--- /dev/null
+++ b/user/test/com/google/gwt/junit/client/ExceptionAsserter.java
@@ -0,0 +1,27 @@
+/*
+ * 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.junit.client;
+
+import com.google.gwt.core.shared.GwtIncompatible;
+
+/**
+ * An abstraction to define assertion of exceptions to be used with {@link ExpectedFailure}.
+ * Note: Exception asserters are only executed in JRE so they don't need to be GWT compatible.
+ */
+public interface ExceptionAsserter {
+  @GwtIncompatible
+  void assertException(ExpectedFailure annotation, Throwable actual);
+}
diff --git a/user/test/com/google/gwt/junit/ExpectedFailure.java b/user/test/com/google/gwt/junit/client/ExpectedFailure.java
similarity index 89%
rename from user/test/com/google/gwt/junit/ExpectedFailure.java
rename to user/test/com/google/gwt/junit/client/ExpectedFailure.java
index dc76900..d4c2b8d 100644
--- a/user/test/com/google/gwt/junit/ExpectedFailure.java
+++ b/user/test/com/google/gwt/junit/client/ExpectedFailure.java
@@ -13,7 +13,7 @@
  * License for the specific language governing permissions and limitations under
  * the License.
  */
-package com.google.gwt.junit;
+package com.google.gwt.junit.client;
 
 import junit.framework.AssertionFailedError;
 
@@ -31,4 +31,6 @@
   String withMessage() default "";
 
   Class<? extends Throwable> withType() default AssertionFailedError.class;
+
+  Class<? extends ExceptionAsserter> withAsserter() default DefaultExceptionAsserter.class;
 }
diff --git a/user/test/com/google/gwt/junit/client/GWTTestCaseAsyncTest.java b/user/test/com/google/gwt/junit/client/GWTTestCaseAsyncTest.java
index 3984236..9bdf3a5 100644
--- a/user/test/com/google/gwt/junit/client/GWTTestCaseAsyncTest.java
+++ b/user/test/com/google/gwt/junit/client/GWTTestCaseAsyncTest.java
@@ -15,7 +15,6 @@
  */
 package com.google.gwt.junit.client;
 
-import com.google.gwt.junit.ExpectedFailure;
 import com.google.gwt.user.client.Timer;
 
 /**
diff --git a/user/test/com/google/gwt/junit/client/GWTTestCaseSetupTearDownTest.java b/user/test/com/google/gwt/junit/client/GWTTestCaseSetupTearDownTest.java
index d93cef2..07df732 100644
--- a/user/test/com/google/gwt/junit/client/GWTTestCaseSetupTearDownTest.java
+++ b/user/test/com/google/gwt/junit/client/GWTTestCaseSetupTearDownTest.java
@@ -19,7 +19,6 @@
 import static com.google.gwt.junit.client.GWTTestCaseSetupTearDownTest.SetUpTearDownState.TEARDOWN;
 import static com.google.gwt.junit.client.GWTTestCaseSetupTearDownTest.SetUpTearDownState.TESTCASE;
 
-import com.google.gwt.junit.ExpectedFailure;
 import com.google.gwt.user.client.Timer;
 
 import java.util.ArrayList;
@@ -31,16 +30,12 @@
  *
  * Note: This test requires some test methods to be executed in a specific order.
  */
-public class GWTTestCaseSetupTearDownTest extends GWTTestCase {
-
-  public String getModuleName() {
-    return "com.google.gwt.junit.JUnit";
-  }
+public class GWTTestCaseSetupTearDownTest extends GWTTestCaseTestBase {
 
   /**
    * Tracks setup, teardown and testcase runs.
    */
-  protected enum SetUpTearDownState {
+  enum SetUpTearDownState {
     SETUP, TEARDOWN, TESTCASE
   }
 
diff --git a/user/test/com/google/gwt/junit/client/GWTTestCaseStackTraceTest.java b/user/test/com/google/gwt/junit/client/GWTTestCaseStackTraceTest.java
new file mode 100644
index 0000000..59822c3
--- /dev/null
+++ b/user/test/com/google/gwt/junit/client/GWTTestCaseStackTraceTest.java
@@ -0,0 +1,128 @@
+/*
+ * 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.junit.client;
+
+import com.google.gwt.core.shared.SerializableThrowable;
+import com.google.gwt.junit.client.WithProperties.Property;
+
+import junit.framework.AssertionFailedError;
+
+/**
+ * This class tests stack traces generated by GWTTestCase.
+ */
+public class GWTTestCaseStackTraceTest extends GWTTestCaseTestBase {
+
+  private static final int LINE_NUMBER_1 = 39;
+  private static final int LINE_NUMBER_2 = LINE_NUMBER_1 + 2;
+
+  private static final String FILE_NAME = "GWTTestCaseStackTraceTest.java";
+  private static final String CLASS_NAME = GWTTestCaseStackTraceTest.class.getName();
+
+  private static void throwException(boolean withCause) {
+    if (Math.abs(Math.random()) < 0) return; // Dummy code to prevent inlining
+
+    // the next line should be LINE_NUMBER_1
+    AssertionFailedError exception = new AssertionFailedError("stack_trace_msg");
+    if (withCause) {
+      exception.initCause(new RuntimeException("the_cause"));
+    }
+    throw exception;
+  }
+
+  private static void assertStackTrace(
+      Throwable t, String methodName, int lineNumber, boolean hasCause) {
+    assertSame(AssertionFailedError.class, t.getClass());
+    assertTrue(t.getMessage().startsWith("stack_trace_msg"));
+    StackTraceElement[] trace = t.getStackTrace();
+    assertStackTrace(trace, CLASS_NAME, "throwException", FILE_NAME, LINE_NUMBER_1);
+    assertStackTrace(trace, CLASS_NAME, methodName, FILE_NAME, lineNumber);
+    assertCause(t, hasCause);
+  }
+
+  private static void assertCause(Throwable t, boolean hasCause) {
+    Throwable cause = t.getCause();
+    if (hasCause) {
+      assertNotNull(cause);
+      assertCauseDetails(cause);
+    } else {
+      assertNull(cause);
+    }
+  }
+
+  private static void assertCauseDetails(Throwable t) {
+    assertSame(SerializableThrowable.class, t.getClass());
+    String type = ((SerializableThrowable) t).getDesignatedType();
+    assertEquals(RuntimeException.class.getName(), type);
+    assertTrue(t.getMessage().startsWith("the_cause"));
+    StackTraceElement[] trace = t.getStackTrace();
+    assertStackTrace(trace, CLASS_NAME, "throwException", FILE_NAME, LINE_NUMBER_2);
+  }
+
+  private static void assertStackTrace(StackTraceElement[] stackTrace, String className,
+      String methodName, String fileName, int lineNumber) {
+    for (StackTraceElement stackTraceElement : stackTrace) {
+      if (stackTraceElement.getClassName().equals(className)
+          && stackTraceElement.getMethodName().equals(methodName)) {
+        assertEquals(fileName, stackTraceElement.getFileName());
+        assertEquals(lineNumber, stackTraceElement.getLineNumber());
+        return; // Found!!!
+      }
+    }
+    fail("Stack trace element not found " + className + "#" + methodName);
+  }
+
+  /** Asserts stack trace generated by {@link #testStackTrace} */
+  public static class StackTraceAsserter implements ExceptionAsserter {
+    public void assertException(ExpectedFailure annotation, Throwable actual) {
+      final int lineNumber = 98;
+      assertStackTrace(actual, "testStackTrace", lineNumber, false);
+    }
+  }
+
+  @ExpectedFailure(withAsserter = StackTraceAsserter.class)
+  public void testStackTrace() {
+    throwException(false);
+  }
+
+  /** Asserts stack trace generated by {@link #testStackTrace_withCause} */
+  public static class StackTraceAsserterWithCause implements ExceptionAsserter {
+    public void assertException(ExpectedFailure annotation, Throwable actual) {
+      final int lineNumber = 111;
+      assertStackTrace(actual, "testStackTrace_withCause", lineNumber, true);
+    }
+  }
+
+  @ExpectedFailure(withAsserter = StackTraceAsserterWithCause.class)
+  public void testStackTrace_withCause() {
+    throwException(true);
+  }
+
+  /** Asserts stack trace generated by {@link #testStackTrace_fromDifferentModule} */
+  public static class StackTraceAsserterFromDifferentModule implements ExceptionAsserter {
+    public void assertException(ExpectedFailure annotation, Throwable actual) {
+      final int lineNumber = 126;
+      assertStackTrace(actual, "testStackTrace_fromDifferentModule", lineNumber, false);
+    }
+  }
+
+  // @Propery added just to introduce a different module name for the test
+  @WithProperties(@Property(name = "locale", value = "tr"))
+  @ExpectedFailure(withAsserter = StackTraceAsserterFromDifferentModule.class)
+  public void testStackTrace_fromDifferentModule() {
+    throwException(false);
+  }
+}
diff --git a/user/test/com/google/gwt/junit/client/GWTTestCaseTest.java b/user/test/com/google/gwt/junit/client/GWTTestCaseTest.java
index 340951b..54eed8f 100644
--- a/user/test/com/google/gwt/junit/client/GWTTestCaseTest.java
+++ b/user/test/com/google/gwt/junit/client/GWTTestCaseTest.java
@@ -23,7 +23,6 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptException;
 import com.google.gwt.junit.DoNotRunWith;
-import com.google.gwt.junit.ExpectedFailure;
 import com.google.gwt.junit.Platform;
 
 import junit.framework.AssertionFailedError;
@@ -78,11 +77,32 @@
     throw new Exception();
   }
 
-  @ExpectedFailure(withType = Exception.class)
-  public void testThrowsNonSerializableException() {
+  @ExpectedFailure(withType = JavaScriptException.class)
+  public void testThrowsJavaScriptException() {
     throw new JavaScriptException("name", "desc");
   }
 
+  @ExpectedFailure(withType = NullPointerException.class)
+  public void testThrowsNullPointerException() {
+    throw new NullPointerException();
+  }
+
+  static class SomeNonSerializableException extends RuntimeException {
+    public SomeNonSerializableException(String msg) {
+      super(msg);
+    }
+    // no default constructor
+    // public SomeNonSerializableException() {}
+  }
+
+  // We lose some type information if class meta data is not available, setting expected failure
+  // to RuntimeException will ensure this test case passes for no metadata.
+  @ExpectedFailure(withType = RuntimeException.class,
+      withMessage = "testThrowsNonSerializableException")
+  public void testThrowsNonSerializableException() {
+    throw new SomeNonSerializableException("testThrowsNonSerializableException");
+  }
+
   public void testAssertEqualsDouble() {
     assertEquals(0.0, 0.0, 0.0);
     assertEquals(1.1, 1.1, 0.0);
diff --git a/user/test/com/google/gwt/junit/client/GWTTestCaseUncaughtExceptionHandlerTest.java b/user/test/com/google/gwt/junit/client/GWTTestCaseUncaughtExceptionHandlerTest.java
index 0c7e575..e018477 100644
--- a/user/test/com/google/gwt/junit/client/GWTTestCaseUncaughtExceptionHandlerTest.java
+++ b/user/test/com/google/gwt/junit/client/GWTTestCaseUncaughtExceptionHandlerTest.java
@@ -17,7 +17,6 @@
 
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.GWT.UncaughtExceptionHandler;
-import com.google.gwt.junit.ExpectedFailure;
 
 import junit.framework.AssertionFailedError;