This patch adds "batch" execution of GWTTestCases, significantly reducing the
synchronization and network overhead.

Patch by: amitmanjhi
Review (and simplifications) by: scottb



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@5766 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/common.ant.xml b/common.ant.xml
index 37cfcce..e6ccd23 100755
--- a/common.ant.xml
+++ b/common.ant.xml
@@ -160,7 +160,7 @@
 
   <macrodef name="gwt.junit">
     <!-- TODO: make this more generic / refactor so it can be used from dev/core -->
-    <attribute name="test.args" default="" />
+    <attribute name="test.args" default="-batch module" />
     <attribute name="test.out" default="" />
     <attribute name="test.reports" default="@{test.out}/reports" />
     <attribute name="test.cases" default="" />
diff --git a/user/src/com/google/gwt/junit/BatchingStrategy.java b/user/src/com/google/gwt/junit/BatchingStrategy.java
new file mode 100644
index 0000000..c8b102d
--- /dev/null
+++ b/user/src/com/google/gwt/junit/BatchingStrategy.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2009 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;
+
+import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
+
+import java.util.Set;
+
+/**
+ * An interface that specifies how tests should be batched.
+ */
+public interface BatchingStrategy {
+
+  /**
+   * Returns the list of tests that should be executed along with this test.
+   */
+  TestInfo[] getTestBlock(TestInfo currentTest);
+}
+
+/**
+ * 
+ * Strategy that does not batch tests.
+ */
+class NoBatchingStrategy implements BatchingStrategy {
+  public TestInfo[] getTestBlock(TestInfo currentTest) {
+    return new TestInfo[] {currentTest};
+  }
+}
+
+/**
+ * Strategy that batches all tests belonging to one module.
+ */
+class ModuleBatchingStrategy implements BatchingStrategy {
+
+  /**
+   * Returns the list of all tests belonging to the module of
+   * <code>currentTest</code>.
+   */
+  public TestInfo[] getTestBlock(TestInfo currentTest) {
+    String moduleName = currentTest.getTestModule();
+    if (moduleName.endsWith(".JUnit")) {
+      moduleName = moduleName.substring(0, moduleName.length()
+          - ".JUnit".length());
+    }
+    Set<TestInfo> allTestsInModule = GWTTestCase.ALL_GWT_TESTS.get(moduleName);
+    if (allTestsInModule != null) {
+      assert allTestsInModule.size() > 0;
+      return allTestsInModule.toArray(new TestInfo[allTestsInModule.size()]);
+    }
+    // No data, default to just this test.
+    return new TestInfo[] {currentTest};
+  }
+}
diff --git a/user/src/com/google/gwt/junit/JUnitMessageQueue.java b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
index 169391c..4cec301 100644
--- a/user/src/com/google/gwt/junit/JUnitMessageQueue.java
+++ b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
@@ -23,10 +23,11 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 
 /**
- * A message queue to pass data between {@link JUnitShell} and {@link
- * com.google.gwt.junit.server.JUnitHostImpl} in a thread-safe manner.
+ * A message queue to pass data between {@link JUnitShell} and
+ * {@link com.google.gwt.junit.server.JUnitHostImpl} in a thread-safe manner.
  * 
  * <p>
  * The public methods are called by the servlet to find out what test to execute
@@ -45,8 +46,10 @@
    */
   public static class ClientStatus {
     public final String clientId;
-
-    public JUnitResult currentTestResults = null;
+    /**
+     * Stores the testResults for the current block of tests.
+     */
+    public Map<TestInfo, JUnitResult> currentTestBlockResults = null;
     public boolean hasRequestedCurrentTest = false;
     public boolean isNew = true;
 
@@ -68,7 +71,7 @@
   /**
    * The current test to execute.
    */
-  private TestInfo currentTest;
+  private TestInfo[] currentBlock;
 
   /**
    * The number of TestCase clients executing in parallel.
@@ -88,14 +91,14 @@
   }
 
   /**
-   * Called by the servlet to query for for the next method to test.
+   * Called by the servlet to query for for the next block to test.
    * 
    * @param timeout how long to wait for an answer
-   * @return the next test to run, or <code>null</code> if
-   *         <code>timeout</code> is exceeded or the next test does not match
+   * @return the next test to run, or <code>null</code> if <code>timeout</code>
+   *         is exceeded or the next test does not match
    *         <code>testClassName</code>
    */
-  public TestInfo getNextTestInfo(String clientId, long timeout)
+  public TestInfo[] getNextTestBlock(String clientId, long timeout)
       throws TimeoutException {
     synchronized (clientStatusesLock) {
       ClientStatus clientStatus = clientStatuses.get(clientId);
@@ -106,7 +109,7 @@
 
       long startTime = System.currentTimeMillis();
       long stopTime = startTime + timeout;
-      while (clientStatus.currentTestResults != null) {
+      while (clientStatus.currentTestBlockResults != null) {
         long timeToWait = stopTime - System.currentTimeMillis();
         if (timeToWait < 1) {
           double elapsed = (System.currentTimeMillis() - startTime) / 1000.0;
@@ -130,20 +133,27 @@
 
       // Record that this client has retrieved the current test.
       clientStatus.hasRequestedCurrentTest = true;
-      return currentTest;
+      return currentBlock;
     }
   }
 
+  public void reportFatalLaunch(String clientId, JUnitResult result) {
+    // Fatal launch error, cause this client to fail the whole block.
+    Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
+    for (TestInfo testInfo : currentBlock) {
+      results.put(testInfo, result);
+    }
+    reportResults(clientId, results);
+  }
+
   /**
    * Called by the servlet to report the results of the last test to run.
    * 
-   * @param testInfo the testInfo the result is for
-   * @param results the result of running the test
+   * @param results the result of running the test block
    */
-  public void reportResults(String clientId, TestInfo testInfo,
-      JUnitResult results) {
+  public void reportResults(String clientId, Map<TestInfo, JUnitResult> results) {
     synchronized (clientStatusesLock) {
-      if (testInfo != null && !testInfo.equals(currentTest)) {
+      if (results != null && !resultsMatchCurrentBlock(results)) {
         // A client is reporting results for the wrong test.
         return;
       }
@@ -157,7 +167,7 @@
         clientStatus = new ClientStatus(clientId);
         clientStatuses.put(clientId, clientStatus);
       }
-      clientStatus.currentTestResults = results;
+      clientStatus.currentTestBlockResults = results;
       clientStatusesLock.notifyAll();
     }
   }
@@ -168,10 +178,10 @@
    * @return Fetches a human-readable representation of the current test object
    */
   String getCurrentTestName() {
-    if (currentTest == null) {
+    if (currentBlock == null) {
       return "(no test)";
     }
-    return currentTest.toString();
+    return currentBlock[0].toString();
   }
 
   /**
@@ -212,11 +222,24 @@
    * 
    * @return A map of results from all clients.
    */
-  Map<String, JUnitResult> getResults() {
+  Map<TestInfo, Map<String, JUnitResult>> getResults() {
     synchronized (clientStatusesLock) {
-      Map<String, JUnitResult> result = new HashMap<String, JUnitResult>();
+      /*
+       * All this overly complicated piece of code does is transform mappings
+       * keyed by clientId into mappings keyed by TestInfo.
+       */
+      Map<TestInfo, Map<String, JUnitResult>> result = new HashMap<TestInfo, Map<String, JUnitResult>>();
       for (ClientStatus clientStatus : clientStatuses.values()) {
-        result.put(clientStatus.clientId, clientStatus.currentTestResults);
+        for (Entry<TestInfo, JUnitResult> entry : clientStatus.currentTestBlockResults.entrySet()) {
+          TestInfo testInfo = entry.getKey();
+          JUnitResult clientResultForThisTest = entry.getValue();
+          Map<String, JUnitResult> targetMap = result.get(testInfo);
+          if (targetMap == null) {
+            targetMap = new HashMap<String, JUnitResult>();
+            result.put(testInfo, targetMap);
+          }
+          targetMap.put(clientStatus.clientId, clientResultForThisTest);
+        }
       }
       return result;
     }
@@ -272,7 +295,7 @@
       int itemCount = 0;
       for (ClientStatus clientStatus : clientStatuses.values()) {
         if (clientStatus.hasRequestedCurrentTest
-            && clientStatus.currentTestResults == null) {
+            && clientStatus.currentTestBlockResults == null) {
           if (itemCount > 0) {
             buf.append(", ");
           }
@@ -304,7 +327,7 @@
         return false;
       }
       for (ClientStatus clientStatus : clientStatuses.values()) {
-        if (clientStatus.currentTestResults == null) {
+        if (clientStatus.currentTestBlockResults == null) {
           return false;
         }
       }
@@ -315,12 +338,12 @@
   /**
    * Called by the shell to set the next test to run.
    */
-  void setNextTest(TestInfo testInfo) {
+  void setNextTestBlock(TestInfo[] testBlock) {
     synchronized (clientStatusesLock) {
-      this.currentTest = testInfo;
+      this.currentBlock = testBlock;
       for (ClientStatus clientStatus : clientStatuses.values()) {
         clientStatus.hasRequestedCurrentTest = false;
-        clientStatus.currentTestResults = null;
+        clientStatus.currentTestBlockResults = null;
       }
       clientStatusesLock.notifyAll();
     }
@@ -334,4 +357,14 @@
       }
     }
   }
+
+  private boolean resultsMatchCurrentBlock(Map<TestInfo, JUnitResult> results) {
+    assert results.size() == currentBlock.length;
+    for (TestInfo testInfo : currentBlock) {
+      if (!results.containsKey(testInfo)) {
+        return false;
+      }
+    }
+    return true;
+  }
 }
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index 2a11a2b..052ba3f 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -44,6 +44,7 @@
 import junit.framework.TestResult;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.regex.Matcher;
@@ -66,10 +67,10 @@
  * </p>
  * 
  * <p>
- * The client classes consist of the translatable version of {@link
- * com.google.gwt.junit.client.GWTTestCase}, translatable JUnit classes, and the
- * user's own {@link com.google.gwt.junit.client.GWTTestCase}-derived class.
- * The client communicates to the server via RPC.
+ * The client classes consist of the translatable version of
+ * {@link com.google.gwt.junit.client.GWTTestCase}, translatable JUnit classes,
+ * and the user's own {@link com.google.gwt.junit.client.GWTTestCase}-derived
+ * class. The client communicates to the server via RPC.
  * </p>
  * 
  * <p>
@@ -238,6 +239,37 @@
         }
       });
 
+      // TODO: currently, only two values but soon may have multiple values.
+      registerHandler(new ArgHandlerString() {
+        @Override
+        public String getPurpose() {
+          return "Configure batch execution of tests";
+        }
+
+        @Override
+        public String getTag() {
+          return "-batch";
+        }
+
+        @Override
+        public String[] getTagArgs() {
+          return new String[] {"module"};
+        }
+
+        @Override
+        public boolean isUndocumented() {
+          return true;
+        }
+
+        @Override
+        public boolean setString(String str) {
+          if (str.equals("module")) {
+            batchingStrategy = new ModuleBatchingStrategy();
+          }
+          return true;
+        }
+      });
+
       registerHandler(new ArgHandler() {
         @Override
         public String[] getDefaultArgs() {
@@ -337,8 +369,8 @@
 
   /**
    * The amount of time to wait for all clients to complete a single test
-   * method, in milliseconds, measured from when the <i>last</i> client 
-   * connects (and thus starts the test).  5 minutes.
+   * method, in milliseconds, measured from when the <i>last</i> client connects
+   * (and thus starts the test). 5 minutes.
    */
   private static final long TEST_METHOD_TIMEOUT_MILLIS = 300000;
 
@@ -365,8 +397,8 @@
 
   /**
    * Entry point for {@link com.google.gwt.junit.client.GWTTestCase}. Gets or
-   * creates the singleton {@link JUnitShell} and invokes its {@link
-   * #runTestImpl(String, TestCase, TestResult, Strategy)}.
+   * creates the singleton {@link JUnitShell} and invokes its
+   * {@link #runTestImpl(String, TestCase, TestResult, Strategy)}.
    */
   public static void runTest(String moduleName, TestCase testCase,
       TestResult testResult) throws UnableToCompleteException {
@@ -445,6 +477,11 @@
   }
 
   /**
+   * Determines how to batch up tests for execution.
+   */
+  private BatchingStrategy batchingStrategy = new NoBatchingStrategy();
+
+  /**
    * When headless, all logging goes to the console.
    */
   private PrintWriterTreeLogger consoleLogger;
@@ -510,6 +547,8 @@
    */
   private long testMethodTimeout;
 
+  private Map<TestInfo, Map<String, JUnitResult>> cachedResults = new HashMap<TestInfo, Map<String, JUnitResult>>();
+
   /**
    * Enforce the singleton pattern. The call to {@link GWTShell}'s ctor forces
    * server mode and disables processing extra arguments as URLs to be shown.
@@ -637,6 +676,49 @@
     super.compile(getTopLogger(), module);
   }
 
+  private void processTestResult(TestInfo testInfo, TestCase testCase,
+      TestResult testResult, Strategy strategy) {
+
+    Map<String, JUnitResult> results = cachedResults.get(testInfo);
+    assert results != null;
+
+    boolean parallelTesting = numClients > 1;
+
+    for (Entry<String, JUnitResult> entry : results.entrySet()) {
+      String clientId = entry.getKey();
+      JUnitResult result = entry.getValue();
+      assert (result != null);
+      Throwable exception = result.getException();
+      // In the case that we're running multiple clients at once, we need to
+      // let the user know the browser in which the failure happened
+      if (parallelTesting && exception != null) {
+        String msg = "Remote test failed at " + clientId;
+        if (exception instanceof AssertionFailedError) {
+          AssertionFailedError newException = new AssertionFailedError(msg
+              + "\n" + exception.getMessage());
+          newException.setStackTrace(exception.getStackTrace());
+          newException.initCause(exception.getCause());
+          exception = newException;
+        } else {
+          exception = new RuntimeException(msg, exception);
+        }
+      }
+
+      // 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);
+      }
+
+      strategy.processResult(testCase, result);
+    }
+  }
+
   /**
    * Runs a particular test case.
    */
@@ -682,8 +764,19 @@
       return;
     }
 
-    messageQueue.setNextTest(new TestInfo(currentModule.getName(),
-        testCase.getClass().getName(), testCase.getName()));
+    TestInfo testInfo = new TestInfo(currentModule.getName(),
+        testCase.getClass().getName(), testCase.getName());
+    if (cachedResults.containsKey(testInfo)) {
+      // Already have a result.
+      processTestResult(testInfo, testCase, testResult, strategy);
+      return;
+    }
+
+    /*
+     * Need to process test. Set up synchronization.
+     */
+    TestInfo[] testBlock = batchingStrategy.getTestBlock(testInfo);
+    messageQueue.setNextTestBlock(testBlock);
 
     try {
       if (firstLaunch) {
@@ -710,43 +803,9 @@
     }
 
     assert (messageQueue.hasResult());
-    Map<String, JUnitResult> results = messageQueue.getResults();
-
-    boolean parallelTesting = numClients > 1;
-
-    for (Entry<String, JUnitResult> entry : results.entrySet()) {
-      String clientId = entry.getKey();
-      JUnitResult result = entry.getValue();
-      assert (result != null);
-      Throwable exception = result.getException();
-      // In the case that we're running multiple clients at once, we need to
-      // let the user know the browser in which the failure happened
-      if (parallelTesting && exception != null) {
-        String msg = "Remote test failed at " + clientId;
-        if (exception instanceof AssertionFailedError) {
-          AssertionFailedError newException = new AssertionFailedError(msg
-              + "\n" + exception.getMessage());
-          newException.setStackTrace(exception.getStackTrace());
-          newException.initCause(exception.getCause());
-          exception = newException;
-        } else {
-          exception = new RuntimeException(msg, exception);
-        }
-      }
-
-      // 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);
-      }
-
-      strategy.processResult(testCase, result);
-    }
+    cachedResults = messageQueue.getResults();
+    assert cachedResults.containsKey(testInfo);
+    processTestResult(testInfo, testCase, testResult, strategy);
   }
 
   /**
diff --git a/user/src/com/google/gwt/junit/client/GWTTestCase.java b/user/src/com/google/gwt/junit/client/GWTTestCase.java
index 256e153..d2fe917 100644
--- a/user/src/com/google/gwt/junit/client/GWTTestCase.java
+++ b/user/src/com/google/gwt/junit/client/GWTTestCase.java
@@ -16,10 +16,16 @@
 package com.google.gwt.junit.client;
 
 import com.google.gwt.junit.JUnitShell;
+import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 
 import junit.framework.TestCase;
 import junit.framework.TestResult;
 
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
 /**
  * Acts as a bridge between the JUnit environment and the GWT environment. We
  * hook the run method and stash the TestResult object for later communication
@@ -36,6 +42,12 @@
  */
 public abstract class GWTTestCase extends TestCase {
 
+  /**
+   * Records all live GWTTestCases by module name so we can optimize run they
+   * are compiled and run.
+   */
+  public static final Map<String, Set<TestInfo>> ALL_GWT_TESTS = new HashMap<String, Set<TestInfo>>();
+
   /*
    * Object that collects the results of this test case execution.
    */
@@ -133,6 +145,22 @@
     super.run(result);
   }
 
+  @Override
+  public void setName(String name) {
+    super.setName(name);
+
+    // Once the name is set, we can add ourselves to the global set.
+    String moduleName = getModuleName();
+    Set<TestInfo> testsInThisModule = ALL_GWT_TESTS.get(moduleName);
+    if (testsInThisModule == null) {
+      // Preserve the order.
+      testsInThisModule = new LinkedHashSet<TestInfo>();
+      ALL_GWT_TESTS.put(moduleName, testsInThisModule);
+    }
+    testsInThisModule.add(new TestInfo(moduleName + ".JUnit",
+        getClass().getName(), getName()));
+  }
+
   /**
    * Put the current test in asynchronous mode. If the test method completes
    * normally, this test will not immediately succeed. Instead, a <i>delay
@@ -218,8 +246,10 @@
   @Override
   protected void runTest() throws Throwable {
     if (this.getName() == null) {
-      throw new IllegalArgumentException("GWTTestCases require a name; \"" + this.toString() 
-          + "\" has none.  Perhaps you used TestSuite.addTest() instead of addTestClass()?");
+      throw new IllegalArgumentException(
+          "GWTTestCases require a name; \""
+              + this.toString()
+              + "\" has none.  Perhaps you used TestSuite.addTest() instead of addTestClass()?");
     }
 
     String moduleName = getModuleName();
diff --git a/user/src/com/google/gwt/junit/client/impl/JUnitHost.java b/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
index a968fd1..18bf8cf 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
@@ -19,6 +19,8 @@
 import com.google.gwt.user.client.rpc.IsSerializable;
 import com.google.gwt.user.client.rpc.RemoteService;
 
+import java.util.HashMap;
+
 /**
  * An interface for {@link com.google.gwt.junit.client.GWTTestCase} to
  * communicate with the test process through RPC.
@@ -85,17 +87,16 @@
    * @return the next test to run
    * @throws TimeoutException if the wait for the next method times out.
    */
-  TestInfo getFirstMethod() throws TimeoutException;
+  TestInfo[] getFirstMethod() throws TimeoutException;
 
   /**
    * Reports results for the last method run and gets the name of next method to
    * run.
    * 
-   * @param testInfo the testInfo the result is for
-   * @param result the results of executing the test
+   * @param results the results of executing the test
    * @return the next test to run
    * @throws TimeoutException if the wait for the next method times out.
    */
-  TestInfo reportResultsAndGetNextMethod(TestInfo testInfo, JUnitResult result)
-      throws TimeoutException;
+  TestInfo[] reportResultsAndGetNextMethod(
+      HashMap<TestInfo, JUnitResult> results) throws TimeoutException;
 }
diff --git a/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java b/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
index b26a588..7c25d06 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
@@ -18,6 +18,8 @@
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 
+import java.util.HashMap;
+
 /**
  * The asynchronous version of {@link JUnitHost}.
  */
@@ -29,17 +31,16 @@
    * @param callBack the object that will receive the name of the next method to
    *          run
    */
-  void getFirstMethod(AsyncCallback<TestInfo> callBack);
+  void getFirstMethod(AsyncCallback<TestInfo[]> callBack);
 
   /**
    * Reports results for the last method run and gets the name of next method to
    * run.
    * 
-   * @param testInfo the testInfo the result is for
-   * @param result the result of the test
+   * @param results the results of the tests
    * @param callBack the object that will receive the name of the next method to
    *          run
    */
-  void reportResultsAndGetNextMethod(TestInfo testInfo, JUnitResult result,
-      AsyncCallback<TestInfo> callBack);
+  void reportResultsAndGetNextMethod(HashMap<TestInfo, JUnitResult> results,
+      AsyncCallback<TestInfo[]> callBack);
 }
diff --git a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
index 9e34312..f9c16e8 100644
--- a/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
+++ b/user/src/com/google/gwt/junit/rebind/GWTRunnerGenerator.java
@@ -27,6 +27,7 @@
 import com.google.gwt.core.ext.typeinfo.TypeOracle;
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.junit.client.impl.GWTRunner;
+import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
 import com.google.gwt.user.rebind.SourceWriter;
 
@@ -38,7 +39,6 @@
  * This class generates a stub class for classes that derive from GWTTestCase.
  * This stub class provides the necessary bridge between our Hosted or Hybrid
  * mode classes and the JUnit system.
- * 
  */
 public class GWTRunnerGenerator extends Generator {
 
@@ -77,9 +77,8 @@
 
     String moduleName;
     try {
-      ConfigurationProperty prop
-          = context.getPropertyOracle().getConfigurationProperty(
-              "junit.moduleName");
+      ConfigurationProperty prop = context.getPropertyOracle().getConfigurationProperty(
+          "junit.moduleName");
       moduleName = prop.getValues().get(0);
     } catch (BadPropertyValueException e) {
       logger.log(TreeLogger.ERROR,
@@ -97,9 +96,20 @@
         generatedClass, GWT_RUNNER_NAME);
 
     if (sourceWriter != null) {
-      JClassType[] allTestTypes = getAllPossibleTestTypes(context.getTypeOracle());
-      Set<String> testClasses = getTestTypesForModule(logger, moduleName,
-          allTestTypes);
+      // Check the global set of active tests for this module.
+      Set<TestInfo> moduleTests = GWTTestCase.ALL_GWT_TESTS.get(moduleName);
+      Set<String> testClasses;
+      if (moduleTests == null || moduleTests.isEmpty()) {
+        // Fall back to pulling in all types in the module.
+        JClassType[] allTestTypes = getAllPossibleTestTypes(context.getTypeOracle());
+        testClasses = getTestTypesForModule(logger, moduleName, allTestTypes);
+      } else {
+        // Must use sorted set to prevent nondeterminism.
+        testClasses = new TreeSet<String>();
+        for (TestInfo testInfo : moduleTests) {
+          testClasses.add(testInfo.getTestClass());
+        }
+      }
       writeCreateNewTestCaseMethod(testClasses, sourceWriter);
       sourceWriter.commit(logger);
     }
diff --git a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
index 5da715e..a249549 100644
--- a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
+++ b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
@@ -30,6 +30,7 @@
 import java.io.IOException;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
+import java.util.HashMap;
 
 import javax.servlet.ServletException;
 import javax.servlet.http.HttpServletRequest;
@@ -80,20 +81,22 @@
     fld.set(obj, value);
   }
 
-  public TestInfo getFirstMethod() throws TimeoutException {
-    return getHost().getNextTestInfo(getClientId(getThreadLocalRequest()),
+  public TestInfo[] getFirstMethod() throws TimeoutException {
+    return getHost().getNextTestBlock(getClientId(getThreadLocalRequest()),
         TIME_TO_WAIT_FOR_TESTNAME);
   }
 
-  public TestInfo reportResultsAndGetNextMethod(TestInfo testInfo,
-      JUnitResult result) throws TimeoutException {
-    initResult(getThreadLocalRequest(), result);
-    ExceptionWrapper ew = result.getExceptionWrapper();
-    result.setException(deserialize(ew));
+  public TestInfo[] reportResultsAndGetNextMethod(
+      HashMap<TestInfo, JUnitResult> results) throws TimeoutException {
+    for (JUnitResult result : results.values()) {
+      initResult(getThreadLocalRequest(), result);
+      ExceptionWrapper ew = result.getExceptionWrapper();
+      result.setException(deserialize(ew));
+    }
     JUnitMessageQueue host = getHost();
     String clientId = getClientId(getThreadLocalRequest());
-    host.reportResults(clientId, testInfo, result);
-    return host.getNextTestInfo(clientId, TIME_TO_WAIT_FOR_TESTNAME);
+    host.reportResults(clientId, results);
+    return host.getNextTestBlock(clientId, TIME_TO_WAIT_FOR_TESTNAME);
   }
 
   @Override
@@ -105,7 +108,7 @@
       JUnitResult result = new JUnitResult();
       initResult(request, result);
       result.setException(new JUnitFatalLaunchException(requestPayload));
-      getHost().reportResults(getClientId(request), null, result);
+      getHost().reportFatalLaunch(getClientId(request), result);
     } else {
       super.service(request, response);
     }
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 2a679a9..363da9c 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
@@ -19,11 +19,15 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.junit.client.GWTTestCase;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
+import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.DeferredCommand;
 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.ServiceDefTarget;
 
+import java.util.HashMap;
+
 /**
  * The entry point class for GWTTestCases.
  * 
@@ -37,7 +41,7 @@
    * The RPC callback object for {@link GWTRunner#junitHost}. When
    * {@link #onSuccess(Object)} is called, it's time to run the next test case.
    */
-  private final class JUnitHostListener implements AsyncCallback<TestInfo> {
+  private final class JUnitHostListener implements AsyncCallback<TestInfo[]> {
 
     /**
      * A call to junitHost failed.
@@ -55,9 +59,11 @@
     /**
      * A call to junitHost succeeded; run the next test case.
      */
-    public void onSuccess(TestInfo nextTest) {
-      currentTest = nextTest;
-      if (currentTest != null) {
+    public void onSuccess(TestInfo[] nextTestBlock) {
+      currentBlock = nextTestBlock;
+      currentBlockIndex = 0;
+      currentResults.clear();
+      if (currentBlock != null && currentBlock.length > 0) {
         doRunTest();
       }
     }
@@ -82,9 +88,20 @@
     return sInstance;
   }
 
-  private JUnitResult currentResult;
+  /**
+   * The current block of tests to execute.
+   */
+  private TestInfo currentBlock[];
 
-  private TestInfo currentTest;
+  /**
+   * Active test within current block of tests.
+   */
+  private int currentBlockIndex = 0;
+
+  /**
+   * Results for all test cases in the current block.
+   */
+  private HashMap<TestInfo, JUnitResult> currentResults = new HashMap<TestInfo, JUnitResult>();
 
   /**
    * The remote service to communicate with.
@@ -116,8 +133,8 @@
   }
 
   public void onModuleLoad() {
-    currentTest = checkForQueryParamTestToRun();
-    if (currentTest != null) {
+    currentBlock = checkForQueryParamTestToRun();
+    if (currentBlock != null) {
       /*
        * Just run a single test with no server-side interaction.
        */
@@ -137,8 +154,19 @@
       // That's it, we're done
       return;
     }
-    currentResult = result;
-    syncToServer();
+    TestInfo currentTest = getCurrentTest();
+    currentResults.put(currentTest, result);
+    ++currentBlockIndex;
+    if (currentBlockIndex < currentBlock.length) {
+      // Run the next test after a short delay.
+      DeferredCommand.addCommand(new Command() {
+        public void execute() {
+          doRunTest();
+        }
+      });
+    } else {
+      syncToServer();
+    }
   }
 
   /**
@@ -147,19 +175,21 @@
    */
   protected abstract GWTTestCase createNewTestCase(String testClass);
 
-  private TestInfo checkForQueryParamTestToRun() {
+  private TestInfo[] checkForQueryParamTestToRun() {
     String testClass = Window.Location.getParameter(TESTCLASS_QUERY_PARAM);
     String testMethod = Window.Location.getParameter(TESTFUNC_QUERY_PARAM);
     if (testClass == null || testMethod == null) {
       return null;
     }
-    return new TestInfo(GWT.getModuleName(), testClass, testMethod);
+    // TODO: support blocks of tests?
+    return new TestInfo[] {new TestInfo(GWT.getModuleName(), testClass,
+        testMethod)};
   }
 
   private void doRunTest() {
     // Make sure the module matches.
     String currentModule = GWT.getModuleName();
-    String newModule = currentTest.getTestModule();
+    String newModule = getCurrentTest().getTestModule();
     if (currentModule.equals(newModule)) {
       // The module is correct.
       runTest();
@@ -171,11 +201,18 @@
       String href = Window.Location.getHref();
       String newHref = href.replace(currentModule, newModule);
       Window.Location.replace(newHref);
+      currentBlock = null;
+      currentBlockIndex = 0;
     }
   }
 
+  private TestInfo getCurrentTest() {
+    return currentBlock[currentBlockIndex];
+  }
+
   private void runTest() {
     // Dynamically create a new test case.
+    TestInfo currentTest = getCurrentTest();
     GWTTestCase testCase = null;
     Throwable caught = null;
     try {
@@ -197,11 +234,10 @@
   }
 
   private void syncToServer() {
-    if (currentTest == null) {
+    if (currentBlock == null) {
       junitHost.getFirstMethod(junitHostListener);
     } else {
-      junitHost.reportResultsAndGetNextMethod(currentTest, currentResult,
-          junitHostListener);
+      junitHost.reportResultsAndGetNextMethod(currentResults, junitHostListener);
     }
   }