Fixes JUnit sessions to not use Cookies, which can be problematic.

- The session id is passed through RPC
- Session ids are now ints.

Suggested by: jlabanca
Review by: jlabanca

git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7155 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/junit/JUnitMessageQueue.java b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
index 8f91977..7222b93 100644
--- a/user/src/com/google/gwt/junit/JUnitMessageQueue.java
+++ b/user/src/com/google/gwt/junit/JUnitMessageQueue.java
@@ -15,15 +15,16 @@
  */
 package com.google.gwt.junit;
 
-import com.google.gwt.dev.util.collect.HashSet;
-import com.google.gwt.dev.util.collect.IdentityHashMap;
 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 java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -46,17 +47,51 @@
 public class JUnitMessageQueue {
 
   /**
+   * Server-side client info that includes a description.
+   */
+  public static class ClientInfoExt extends ClientInfo {
+    /**
+     * A description of this client.
+     */
+    private final String desc;
+
+    public ClientInfoExt(int sessionId, String userAgent, String desc) {
+      super(sessionId, userAgent);
+      this.desc = desc;
+    }
+
+    public String getDesc() {
+      return desc;
+    }
+  }
+
+  /**
    * Holds the state of an individual client.
    */
   public static class ClientStatus {
-    public int blockIndex = 0;
-    public final String clientId;
-    public String clientDesc;
-    public boolean isNew = true;
+    private int blockIndex = 0;
+    private ClientInfoExt clientInfo;
+    private boolean isNew = true;
 
-    public ClientStatus(String clientId, String clientDesc) {
-      this.clientId = clientId;
-      this.clientDesc = clientDesc;
+    public ClientStatus(ClientInfoExt clientInfo) {
+      this.clientInfo = clientInfo;
+    }
+
+    public String getDesc() {
+      return clientInfo.getDesc();
+    }
+
+    public int getId() {
+      return clientInfo.getSessionId();
+    }
+
+    @Override
+    public String toString() {
+      return clientInfo.getDesc();
+    }
+
+    public void updateClientInfo(ClientInfoExt clientInfo) {
+      this.clientInfo = clientInfo;
     }
   }
 
@@ -72,7 +107,7 @@
   /**
    * Records results for each client; must lock before accessing.
    */
-  private final Map<String, ClientStatus> clientStatuses = new HashMap<String, ClientStatus>();
+  private final Map<Integer, ClientStatus> clientStatuses = new HashMap<Integer, ClientStatus>();
 
   /**
    * The lock used to synchronize access to clientStatuses.
@@ -117,19 +152,18 @@
   /**
    * Called by the servlet to query for for the next block to test.
    * 
-   * @param clientId the ID of the client
-   * @param userAgent the user agent property of the client
+   * @param clientInfo information about the client
    * @param blockIndex the index of the test block to get
    * @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
    *         <code>testClassName</code>
    */
-  public TestBlock getTestBlock(String clientId, String clientDesc,
-      String userAgent, int blockIndex, long timeout) throws TimeoutException {
+  public TestBlock getTestBlock(ClientInfoExt clientInfo, int blockIndex,
+      long timeout) throws TimeoutException {
     synchronized (clientStatusesLock) {
-      userAgents.add(userAgent);
-      ClientStatus clientStatus = ensureClientStatus(clientId, clientDesc);
+      userAgents.add(clientInfo.getUserAgent());
+      ClientStatus clientStatus = ensureClientStatus(clientInfo);
       clientStatus.blockIndex = blockIndex;
 
       // The client has finished all of the tests.
@@ -145,8 +179,8 @@
           double elapsed = (System.currentTimeMillis() - startTime) / 1000.0;
           throw new TimeoutException("The servlet did not respond to the "
               + "next query to test within " + timeout + "ms.\n"
-              + " Client id: " + clientId + "\n" + " Actual time elapsed: "
-              + elapsed + " seconds.\n");
+              + " Client description: " + clientInfo.getDesc() + "\n"
+              + " Actual time elapsed: " + elapsed + " seconds.\n");
         }
         try {
           clientStatusesLock.wait(timeToWait);
@@ -170,32 +204,36 @@
     }
   }
 
-  public void reportFatalLaunch(String clientId, String clientDesc, String userAgent,
-      JUnitResult result) {
+  /**
+   * Reports a failure from a client that cannot startup.
+   * 
+   * @param clientInfo information about the client
+   * @param result the failure result
+   */
+  public void reportFatalLaunch(ClientInfoExt clientInfo, JUnitResult result) {
     // Fatal launch error, cause this client to fail the whole block.
-    ClientStatus clientStatus = ensureClientStatus(clientId, clientDesc);
+    ClientStatus clientStatus = ensureClientStatus(clientInfo);
     Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
     for (TestInfo testInfo : testBlocks.get(clientStatus.blockIndex)) {
       results.put(testInfo, result);
     }
-    reportResults(clientId, clientDesc, userAgent, results);
+    reportResults(clientInfo, results);
   }
 
   /**
    * Called by the servlet to report the results of the last test to run.
    * 
-   * @param clientId the ID of the client
-   * @param userAgent the user agent property of the client
+   * @param clientInfo information about the client
    * @param results the result of running the test block
    */
-  public void reportResults(String clientId, String clientDesc, String userAgent,
+  public void reportResults(ClientInfoExt clientInfo,
       Map<TestInfo, JUnitResult> results) {
     synchronized (clientStatusesLock) {
       if (results == null) {
         throw new IllegalArgumentException("results cannot be null");
       }
-      userAgents.add(userAgent);
-      ClientStatus clientStatus = ensureClientStatus(clientId, clientDesc);
+      userAgents.add(clientInfo.getUserAgent());
+      ClientStatus clientStatus = ensureClientStatus(clientInfo);
 
       // Cache the test results.
       for (Map.Entry<TestInfo, JUnitResult> entry : results.entrySet()) {
@@ -240,7 +278,7 @@
       List<String> results = new ArrayList<String>();
       for (ClientStatus clientStatus : clientStatuses.values()) {
         if (clientStatus.isNew) {
-          results.add(clientStatus.clientDesc);
+          results.add(clientStatus.getDesc());
           // Record that this client is no longer new.
           clientStatus.isNew = false;
         }
@@ -323,7 +361,7 @@
         } else {
           buf.append(" - (ok): ");
         }
-        buf.append(clientStatus.clientDesc);
+        buf.append(clientStatus.getDesc());
         ++lineCount;
       }
       int difference = numClients - getNumClientsRetrievedTest(testInfo);
@@ -365,7 +403,7 @@
       if (results != null) {
         for (Map.Entry<ClientStatus, JUnitResult> entry : results.entrySet()) {
           if (entry.getValue() == null) {
-            buf.append(entry.getKey().clientDesc);
+            buf.append(entry.getKey().getDesc());
             buf.append("\n");
             itemCount++;
           }
@@ -454,14 +492,15 @@
    * @param clientId the id of the client
    * @return the {@link ClientStatus} for the client
    */
-  private ClientStatus ensureClientStatus(String clientId, String clientDesc) {
-    ClientStatus clientStatus = clientStatuses.get(clientId);
+  private ClientStatus ensureClientStatus(ClientInfoExt clientInfo) {
+    int id = clientInfo.getSessionId();
+    ClientStatus clientStatus = clientStatuses.get(id);
     if (clientStatus == null) {
-      clientStatus = new ClientStatus(clientId, clientDesc);
-      clientStatuses.put(clientId, clientStatus);
+      clientStatus = new ClientStatus(clientInfo);
+      clientStatuses.put(id, clientStatus);
     } else {
-      // Maybe update the description (ip might change if through a proxy).
-      clientStatus.clientDesc = clientDesc;
+      // Maybe update the client info (IP might change if through a proxy).
+      clientStatus.updateClientInfo(clientInfo);
     }
     return clientStatus;
   }
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index 813c514..28d4bf3 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -1068,7 +1068,7 @@
 
       // Let the user know the browser in which the failure happened.
       if (exception != null) {
-        String msg = "Remote test failed at " + client.clientDesc;
+        String msg = "Remote test failed at " + client.getDesc();
         if (exception instanceof AssertionFailedError) {
           String oldMessage = exception.getMessage();
           if (oldMessage != null) {
diff --git a/user/src/com/google/gwt/junit/client/impl/ExceptionWrapper.java b/user/src/com/google/gwt/junit/client/impl/ExceptionWrapper.java
index 0226bab..d653ead 100644
--- a/user/src/com/google/gwt/junit/client/impl/ExceptionWrapper.java
+++ b/user/src/com/google/gwt/junit/client/impl/ExceptionWrapper.java
@@ -15,13 +15,13 @@
  */
 package com.google.gwt.junit.client.impl;
 
-import com.google.gwt.user.client.rpc.IsSerializable;
+import java.io.Serializable;
 
 /**
  * A helper class for converting a generic {@link Throwable} into an Object that
  * can be serialized for RPC.
  */
-public final class ExceptionWrapper implements IsSerializable {
+public final class ExceptionWrapper implements Serializable {
 
   /**
    * Corresponds to {@link Throwable#getCause()}.
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 2ccaa88..711b6da 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHost.java
@@ -16,9 +16,9 @@
 package com.google.gwt.junit.client.impl;
 
 import com.google.gwt.junit.client.TimeoutException;
-import com.google.gwt.user.client.rpc.IsSerializable;
 import com.google.gwt.user.client.rpc.RemoteService;
 
+import java.io.Serializable;
 import java.util.HashMap;
 
 /**
@@ -28,11 +28,79 @@
 public interface JUnitHost extends RemoteService {
 
   /**
+   * Information about the client browser.
+   */
+  public static class ClientInfo implements Serializable {
+    /**
+     * This client's unique session id.
+     */
+    private int sessionId;
+
+    /**
+     * The GWT user.agent property of this client, e.g. "ie6", "safari", etc.
+     */
+    private String userAgent;
+
+    public ClientInfo(int sessionId, String userAgent) {
+      this.sessionId = sessionId;
+      this.userAgent = userAgent;
+    }
+
+    /**
+     * Constructor for serialization.
+     */
+    ClientInfo() {
+    }
+
+    public int getSessionId() {
+      return sessionId;
+    }
+
+    public String getUserAgent() {
+      return userAgent;
+    }
+  }
+
+  /**
+   * An initial response that sets the client session id.
+   */
+  public static class InitialResponse implements Serializable {
+    /**
+     * The unique client session id.
+     */
+    private int sessionId;
+
+    /**
+     * The first test block to run.
+     */
+    private TestBlock testBlock;
+
+    public InitialResponse(int sessionId, TestBlock testBlock) {
+      this.sessionId = sessionId;
+      this.testBlock = testBlock;
+    }
+
+    /**
+     * Constructor for serialization.
+     */
+    InitialResponse() {
+    }
+
+    public int getSessionId() {
+      return sessionId;
+    }
+
+    public TestBlock getTestBlock() {
+      return testBlock;
+    }
+  }
+
+  /**
    * Returned from the server to tell the system what test to run next.
    */
-  public static class TestBlock implements IsSerializable {
-    private TestInfo[] tests;
+  public static class TestBlock implements Serializable {
     private int index;
+    private TestInfo[] tests;
 
     public TestBlock(TestInfo[] tests, int index) {
       this.tests = tests;
@@ -57,7 +125,7 @@
   /**
    * Returned from the server to tell the system what test to run next.
    */
-  public static class TestInfo implements IsSerializable {
+  public static class TestInfo implements Serializable {
     private String testClass;
     private String testMethod;
     private String testModule;
@@ -112,11 +180,12 @@
    * Gets a specific block of tests to run.
    * 
    * @param blockIndex the index of the test block to retrieve
-   * @param userAgent the user agent property of this client
-   * @return the test block
+   * @param clientInfo the info for this client
+   * @return the initial response
    * @throws TimeoutException if the wait for the next method times out.
    */
-  TestBlock getTestBlock(int blockIndex, String userAgent) throws TimeoutException;
+  InitialResponse getTestBlock(int blockIndex, ClientInfo clientInfo)
+      throws TimeoutException;
 
   /**
    * Reports results for the last method run and gets the name of next method to
@@ -124,11 +193,11 @@
    * 
    * @param results the results of executing the test
    * @param blockIndex the index of the test block to retrieve
-   * @param userAgent the user agent property of this client
+   * @param clientInfo the info for this client
    * @return the next test block
    * @throws TimeoutException if the wait for the next method times out.
    */
   TestBlock reportResultsAndGetTestBlock(
-      HashMap<TestInfo, JUnitResult> results, int blockIndex, String userAgent)
-      throws TimeoutException;
+      HashMap<TestInfo, JUnitResult> results, int blockIndex,
+      ClientInfo clientInfo) 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 88b7592..4700a85 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitHostAsync.java
@@ -15,6 +15,8 @@
  */
 package com.google.gwt.junit.client.impl;
 
+import com.google.gwt.junit.client.impl.JUnitHost.ClientInfo;
+import com.google.gwt.junit.client.impl.JUnitHost.InitialResponse;
 import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 import com.google.gwt.user.client.rpc.AsyncCallback;
@@ -30,12 +32,11 @@
    * Gets a specific block of tests to run.
    * 
    * @param blockIndex the index of the test block to retrieve
-   * @param userAgent the user agent property of this client
-   * @param callBack the object that will receive the name of the next method to
-   *          run
+   * @param clientInfo the info for this client
+   * @param callBack the object that will receive the initial response
    */
-  void getTestBlock(int blockIndex, String userAgent,
-      AsyncCallback<TestBlock> callBack);
+  void getTestBlock(int blockIndex, ClientInfo clientInfo,
+      AsyncCallback<InitialResponse> callBack);
 
   /**
    * Reports results for the last method run and gets the name of next method to
@@ -43,10 +44,9 @@
    * 
    * @param results the results of executing the test
    * @param blockIndex the index of the test block to retrieve
-   * @param userAgent the user agent property of this client
-   * @param callBack the object that will receive the name of the next method to
-   *          run
+   * @param clientInfo the info for this client
+   * @param callBack the object that will receive the next test block
    */
   void reportResultsAndGetTestBlock(HashMap<TestInfo, JUnitResult> results,
-      int blockIndex, String userAgent, AsyncCallback<TestBlock> callBack);
+      int blockIndex, ClientInfo clientInfo, AsyncCallback<TestBlock> callBack);
 }
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 2f63e26..3918349 100644
--- a/user/src/com/google/gwt/junit/client/impl/JUnitResult.java
+++ b/user/src/com/google/gwt/junit/client/impl/JUnitResult.java
@@ -15,7 +15,7 @@
  */
 package com.google.gwt.junit.client.impl;
 
-import com.google.gwt.user.client.rpc.IsSerializable;
+import java.io.Serializable;
 
 /**
  * Encapsulates the results of the execution of a single benchmark. A TestResult
@@ -28,7 +28,7 @@
  * @see com.google.gwt.junit.JUnitMessageQueue
  * @see com.google.gwt.junit.JUnitShell
  */
-public class JUnitResult implements IsSerializable {
+public class JUnitResult implements Serializable {
 
   // Computed at the server, via HTTP header.
   private transient String agent;
diff --git a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
index 7990a97..57063cc 100644
--- a/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
+++ b/user/src/com/google/gwt/junit/server/JUnitHostImpl.java
@@ -18,6 +18,7 @@
 import com.google.gwt.junit.JUnitFatalLaunchException;
 import com.google.gwt.junit.JUnitMessageQueue;
 import com.google.gwt.junit.JUnitShell;
+import com.google.gwt.junit.JUnitMessageQueue.ClientInfoExt;
 import com.google.gwt.junit.client.TimeoutException;
 import com.google.gwt.junit.client.impl.ExceptionWrapper;
 import com.google.gwt.junit.client.impl.JUnitHost;
@@ -31,10 +32,9 @@
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
 import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import javax.servlet.ServletException;
-import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -57,7 +57,10 @@
    */
   private static final int TIME_TO_WAIT_FOR_TESTNAME = 300000;
 
-  private static final AtomicLong uniqueSessionId = new AtomicLong();
+  /**
+   * Monotonic increase counter to create unique client session ids.
+   */
+  private static final AtomicInteger uniqueSessionId = new AtomicInteger();
 
   /**
    * Tries to grab the GWTUnitTestShell sHost environment to communicate with
@@ -85,28 +88,32 @@
     fld.set(obj, value);
   }
 
-  public TestBlock getTestBlock(int blockIndex, String userAgent)
+  public InitialResponse getTestBlock(int blockIndex, ClientInfo clientInfo)
       throws TimeoutException {
-    return getHost().getTestBlock(
-        getClientId(getThreadLocalRequest(), getThreadLocalResponse()),
-        getClientDesc(getThreadLocalRequest()), userAgent, blockIndex,
-        TIME_TO_WAIT_FOR_TESTNAME);
+    ClientInfoExt clientInfoExt;
+    if (clientInfo.getSessionId() < 0) {
+      clientInfoExt = createNewClientInfo(clientInfo.getUserAgent());
+    } else {
+      clientInfoExt = createClientInfo(clientInfo);
+    }
+    TestBlock initialTestBlock = getHost().getTestBlock(clientInfoExt,
+        blockIndex, TIME_TO_WAIT_FOR_TESTNAME);
+    // Send back the updated session id.
+    return new InitialResponse(clientInfoExt.getSessionId(), initialTestBlock);
   }
 
   public TestBlock reportResultsAndGetTestBlock(
-      HashMap<TestInfo, JUnitResult> results, int testBlock, String userAgent)
-      throws TimeoutException {
+      HashMap<TestInfo, JUnitResult> results, int testBlock,
+      ClientInfo clientInfo) 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(),
-        getThreadLocalResponse());
-    String clientDesc = getClientDesc(getThreadLocalRequest());
-    host.reportResults(clientId, clientDesc, userAgent, results);
-    return host.getTestBlock(clientId, clientDesc, userAgent, testBlock,
+    ClientInfoExt clientInfoExt = createClientInfo(clientInfo);
+    host.reportResults(clientInfoExt, results);
+    return host.getTestBlock(clientInfoExt, testBlock,
         TIME_TO_WAIT_FOR_TESTNAME);
   }
 
@@ -119,13 +126,27 @@
       JUnitResult result = new JUnitResult();
       initResult(request, result);
       result.setException(new JUnitFatalLaunchException(requestPayload));
-      getHost().reportFatalLaunch(getClientId(request, response),
-          getClientDesc(request), null, result);
+      getHost().reportFatalLaunch(createNewClientInfo(null), result);
     } else {
       super.service(request, response);
     }
   }
 
+  private ClientInfoExt createClientInfo(ClientInfo clientInfo) {
+    assert (clientInfo.getSessionId() >= 0);
+    return new ClientInfoExt(clientInfo.getSessionId(),
+        clientInfo.getUserAgent(), getClientDesc(getThreadLocalRequest()));
+  }
+
+  private ClientInfoExt createNewClientInfo(String userAgent) {
+    return new ClientInfoExt(createSessionId(), userAgent,
+        getClientDesc(getThreadLocalRequest()));
+  }
+
+  private int createSessionId() {
+    return uniqueSessionId.getAndIncrement();
+  }
+
   /**
    * Deserializes an ExceptionWrapper back into a Throwable.
    */
@@ -232,30 +253,15 @@
     return result;
   }
 
+  /**
+   * Returns a client description for the current request.
+   */
   private String getClientDesc(HttpServletRequest request) {
     String machine = request.getRemoteHost();
     String agent = request.getHeader("User-Agent");
     return machine + " / " + agent;
   }
 
-  /**
-   * Returns a "client id" for the current request.
-   */
-  private String getClientId(HttpServletRequest request,
-      HttpServletResponse response) {
-    Cookie[] cookies = request.getCookies();
-    if (cookies != null) {
-      for (Cookie cookie : cookies) {
-        if ("gwt.junit.sessionCookie".equals(cookie.getName())) {
-          return cookie.getValue();
-        }
-      }
-    }
-    String cookie = String.valueOf(uniqueSessionId.getAndIncrement());
-    response.addCookie(new Cookie("gwt.junit.sessionCookie", cookie));
-    return cookie;
-  }
-
   private void initResult(HttpServletRequest request, JUnitResult result) {
     String agent = request.getHeader("User-Agent");
     result.setAgent(agent);
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 c9eab71..bf3cb59 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,10 +19,11 @@
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.http.client.UrlBuilder;
 import com.google.gwt.junit.client.GWTTestCase;
+import com.google.gwt.junit.client.impl.JUnitHost.ClientInfo;
+import com.google.gwt.junit.client.impl.JUnitHost.InitialResponse;
 import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
 import com.google.gwt.junit.client.impl.JUnitHost.TestInfo;
 import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.Cookies;
 import com.google.gwt.user.client.DeferredCommand;
 import com.google.gwt.user.client.Timer;
 import com.google.gwt.user.client.Window;
@@ -40,11 +41,32 @@
  */
 public abstract class GWTRunner implements EntryPoint {
 
+  private final class InitialResponseListener implements
+      AsyncCallback<InitialResponse> {
+
+    /**
+     * Delegate to the {@link TestBlockListener}.
+     */
+    public void onFailure(Throwable caught) {
+      testBlockListener.onFailure(caught);
+    }
+
+    /**
+     * Update our client info with the server-provided session id then delegate
+     * to the {@link TestBlockListener}.
+     */
+    public void onSuccess(InitialResponse result) {
+      clientInfo = new ClientInfo(result.getSessionId(),
+          clientInfo.getUserAgent());
+      testBlockListener.onSuccess(result.getTestBlock());
+    }
+  }
+
   /**
    * The RPC callback object for {@link GWTRunner#junitHost}. When
    * {@link #onSuccess} is called, it's time to run the next test case.
    */
-  private final class JUnitHostListener implements AsyncCallback<TestBlock> {
+  private final class TestBlockListener implements AsyncCallback<TestBlock> {
 
     /**
      * The number of times we've failed to communicate with the server on the
@@ -99,12 +121,12 @@
   /**
    * The singleton instance.
    */
-  private static GWTRunner sInstance;
+  static GWTRunner sInstance;
 
   /**
    * A query param specifying my unique session cookie.
    */
-  private static final String SESSIONCOOKIE_QUERY_PARAM = "gwt.junit.sessionCookie";
+  private static final String SESSIONID_QUERY_PARAM = "gwt.junit.sessionId";
 
   /**
    * A query param specifying the test class to run, for serverless mode.
@@ -132,6 +154,11 @@
   }
 
   /**
+   * This client's info.
+   */
+  private ClientInfo clientInfo;
+
+  /**
    * The current block of tests to execute.
    */
   private TestBlock currentBlock;
@@ -157,9 +184,14 @@
   private final JUnitHostAsync junitHost = (JUnitHostAsync) GWT.create(JUnitHost.class);
 
   /**
-   * Handles all RPC responses.
+   * Handles all {@link InitialResponse InitialResponses}.
    */
-  private final JUnitHostListener junitHostListener = new JUnitHostListener();
+  private final InitialResponseListener initialResponseListener = new InitialResponseListener();
+
+  /**
+   * Handles all {@link TestBlock TestBlocks}.
+   */
+  private final TestBlockListener testBlockListener = new TestBlockListener();
 
   /**
    * The maximum number of times to retry communication with the server per
@@ -187,12 +219,8 @@
   }
 
   public void onModuleLoad() {
-    // Try to import a session cookie from the previous module.
-    String value = Window.Location.getParameter(SESSIONCOOKIE_QUERY_PARAM);
-    if (value != null) {
-      Cookies.setCookie(SESSIONCOOKIE_QUERY_PARAM, value);
-    }
-
+    clientInfo = new ClientInfo(parseQueryParamInteger(
+        SESSIONID_QUERY_PARAM, -1), getUserAgentProperty());
     maxRetryCount = parseQueryParamInteger(RETRYCOUNT_QUERY_PARAM, -1);
     currentBlock = checkForQueryParamTestToRun();
     if (currentBlock != null) {
@@ -278,10 +306,10 @@
       builder.setParameter(BLOCKINDEX_QUERY_PARAM,
           Integer.toString(currentBlock.getIndex())).setPath(
           newModule + pathSuffix);
-      // Hand off the session cookie to the next module.
-      String sessionCookie = Cookies.getCookie(SESSIONCOOKIE_QUERY_PARAM);
-      if (sessionCookie != null) {
-        builder.setParameter(SESSIONCOOKIE_QUERY_PARAM, sessionCookie);
+      // Hand off the session id to the next module.
+      if (clientInfo.getSessionId() >= 0) {
+        builder.setParameter(SESSIONID_QUERY_PARAM,
+            String.valueOf(clientInfo.getSessionId()));
       }
       Window.Location.replace(builder.buildString());
       currentBlock = null;
@@ -348,12 +376,11 @@
   private void syncToServer() {
     if (currentBlock == null) {
       int firstBlockIndex = parseQueryParamInteger(BLOCKINDEX_QUERY_PARAM, 0);
-      junitHost.getTestBlock(firstBlockIndex, getUserAgentProperty(),
-          junitHostListener);
+      junitHost.getTestBlock(firstBlockIndex, clientInfo,
+          initialResponseListener);
     } else {
       junitHost.reportResultsAndGetTestBlock(currentResults,
-          currentBlock.getIndex() + 1, getUserAgentProperty(),
-          junitHostListener);
+          currentBlock.getIndex() + 1, clientInfo, testBlockListener);
     }
   }
 
diff --git a/user/test/com/google/gwt/junit/JUnitMessageQueueTest.java b/user/test/com/google/gwt/junit/JUnitMessageQueueTest.java
index 1579d0e..2428df8 100644
--- a/user/test/com/google/gwt/junit/JUnitMessageQueueTest.java
+++ b/user/test/com/google/gwt/junit/JUnitMessageQueueTest.java
@@ -16,6 +16,7 @@
 package com.google.gwt.junit;
 
 import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.junit.JUnitMessageQueue.ClientInfoExt;
 import com.google.gwt.junit.JUnitMessageQueue.ClientStatus;
 import com.google.gwt.junit.client.impl.JUnitResult;
 import com.google.gwt.junit.client.impl.JUnitHost.TestBlock;
@@ -24,7 +25,9 @@
 import junit.framework.TestCase;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
@@ -91,25 +94,25 @@
 
     // Add some clients in a few ways.
     {
-      queue.getTestBlock("client0", "desc0", "ie6", 0, timeout);
-      queue.reportFatalLaunch("client1", "desc1", "gecko", null);
-      queue.reportResults("client2", "desc2", "safari", createTestResults(0));
+      queue.getTestBlock(createClientInfo(0, "ie6"), 0, timeout);
+      queue.reportFatalLaunch(createClientInfo(1, "gecko"), null);
+      queue.reportResults(createClientInfo(2, "safari"), createTestResults(0));
       assertEquals(3, queue.getNumConnectedClients());
     }
 
     // Add duplicate clients.
     {
-      queue.getTestBlock("client3", "desc3", "ie6", 0, timeout);
-      queue.reportFatalLaunch("client3", "desc3", "ie6", null);
-      queue.reportResults("client4", "desc3", "safari", createTestResults(0));
+      queue.getTestBlock(createClientInfo(3, "ie6"), 0, timeout);
+      queue.reportFatalLaunch(createClientInfo(3, "ie6"), null);
+      queue.reportResults(createClientInfo(4, "safari"), createTestResults(0));
       assertEquals(5, queue.getNumConnectedClients());
     }
 
     // Add existing clients.
     {
-      queue.getTestBlock("client0", "desc0", "ie6", 0, timeout);
-      queue.reportFatalLaunch("client1", "desc1", "gecko", null);
-      queue.reportResults("client2", "desc2", "safari", createTestResults(0));
+      queue.getTestBlock(createClientInfo(0, "ie6"), 0, timeout);
+      queue.reportFatalLaunch(createClientInfo(1, "gecko"), null);
+      queue.reportResults(createClientInfo(2, "safari"), createTestResults(0));
       assertEquals(5, queue.getNumConnectedClients());
     }
   }
@@ -134,7 +137,7 @@
 
     // First client retrieves the first test block.
     {
-      queue.getTestBlock("client0", "desc0", "ie6", 0, timeout);
+      queue.getTestBlock(createClientInfo(0, "ie6"), 0, timeout);
       assertEquals(1, queue.getNumClientsRetrievedTest(test0_0));
       assertEquals(1, queue.getNumClientsRetrievedTest(test0_1));
       assertEquals(1, queue.getNumClientsRetrievedTest(test0_2));
@@ -145,7 +148,7 @@
 
     // Second client retrieves the first test block.
     {
-      queue.getTestBlock("client1", "desc1", "ie6", 0, timeout);
+      queue.getTestBlock(createClientInfo(1, "ie6"), 0, timeout);
       assertEquals(2, queue.getNumClientsRetrievedTest(test0_0));
       assertEquals(2, queue.getNumClientsRetrievedTest(test0_1));
       assertEquals(2, queue.getNumClientsRetrievedTest(test0_2));
@@ -156,7 +159,7 @@
 
     // First client retrieves the second test block.
     {
-      queue.getTestBlock("client0", "desc0", "ie6", 1, timeout);
+      queue.getTestBlock(createClientInfo(0, "ie6"), 1, timeout);
       assertEquals(2, queue.getNumClientsRetrievedTest(test0_0));
       assertEquals(2, queue.getNumClientsRetrievedTest(test0_1));
       assertEquals(2, queue.getNumClientsRetrievedTest(test0_2));
@@ -167,7 +170,7 @@
 
     // First client retrieves the second test block again.
     {
-      queue.getTestBlock("client0", "desc0", "ie6", 1, timeout);
+      queue.getTestBlock(createClientInfo(0, "ie6"), 1, timeout);
       assertEquals(2, queue.getNumClientsRetrievedTest(test0_0));
       assertEquals(2, queue.getNumClientsRetrievedTest(test0_1));
       assertEquals(2, queue.getNumClientsRetrievedTest(test0_2));
@@ -194,21 +197,21 @@
     {
       Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
       results.put(test0_0, result0);
-      queue.reportResults("client0", "desc0", "ie6", results);
+      queue.reportResults(createClientInfo(0, "ie6"), results);
     }
 
     // Client 1 reports results for first test case.
     {
       Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
       results.put(test0_0, result1);
-      queue.reportResults("client1", "desc1", "ie6", results);
+      queue.reportResults(createClientInfo(1, "ie6"), results);
     }
 
     // Client 2 reports results for first test case.
     {
       Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
       results.put(test0_0, result2);
-      queue.reportResults("client2", "desc2", "ie6", results);
+      queue.reportResults(createClientInfo(2, "ie6"), results);
     }
 
     // Get the results
@@ -217,14 +220,19 @@
     for (Entry<ClientStatus, JUnitResult> entry : results.entrySet()) {
       ClientStatus client = entry.getKey();
       JUnitResult result = entry.getValue();
-      if ("client0".equals(client.clientId)) {
-        assertEquals(result0, result);
-      } else if ("client1".equals(client.clientId)) {
-        assertEquals(result1, result);
-      } else if ("client2".equals(client.clientId)) {
-        assertEquals(result2, result);
-      } else {
-        fail("Unexpected client");
+      switch (client.getId()) {
+        case 0:
+          assertEquals(result0, result);
+          break;
+        case 1:
+          assertEquals(result1, result);
+          break;
+        case 2:
+          assertEquals(result2, result);
+          break;
+        default:
+          fail("Unexpected client");
+          break;
       }
     }
   }
@@ -237,7 +245,7 @@
 
     // Get the first test block.
     {
-      TestBlock block = queue.getTestBlock("client0", "desc0", "ie6", 0,
+      TestBlock block = queue.getTestBlock(createClientInfo(0, "ie6"), 0,
           timeout);
       assertEquals(testBlock0, block.getTests());
       assertEquals(0, block.getIndex());
@@ -245,7 +253,7 @@
 
     // Get the second test block.
     {
-      TestBlock block = queue.getTestBlock("client0", "desc0", "ie6", 1,
+      TestBlock block = queue.getTestBlock(createClientInfo(0, "ie6"), 1,
           timeout);
       assertEquals(testBlock1, block.getTests());
       assertEquals(1, block.getIndex());
@@ -253,7 +261,7 @@
 
     // Get the third test block.
     {
-      assertNull(queue.getTestBlock("client0", "desc0", "ie6", 2, timeout));
+      assertNull(queue.getTestBlock(createClientInfo(0, "ie6"), 2, timeout));
     }
   }
 
@@ -264,28 +272,28 @@
 
     // Add some clients in a few ways.
     {
-      queue.getTestBlock("client0", "desc0", "ie6", 0, timeout);
-      queue.reportFatalLaunch("client1", "desc1", "gecko", null);
-      queue.reportResults("client2", "desc2", "safari", createTestResults(0));
+      queue.getTestBlock(createClientInfo(0, "ie6"), 0, timeout);
+      queue.reportFatalLaunch(createClientInfo(1, "gecko"), null);
+      queue.reportResults(createClientInfo(2, "safari"), createTestResults(0));
       assertSimilar(new String[] {"ie6", "gecko", "safari"},
           queue.getUserAgents());
     }
 
     // Add duplicate clients.
     {
-      queue.getTestBlock("client3", "desc3", "ie7", 0, timeout);
-      queue.reportFatalLaunch("client3", "desc3", "ie7", null);
-      queue.reportResults("client4", "desc4", "gecko1_8", createTestResults(0));
-      queue.getTestBlock("client3", "desc3", "ie7", 0, timeout);
+      queue.getTestBlock(createClientInfo(3, "ie7"), 0, timeout);
+      queue.reportFatalLaunch(createClientInfo(3, "ie7"), null);
+      queue.reportResults(createClientInfo(4, "gecko1_8"), createTestResults(0));
+      queue.getTestBlock(createClientInfo(3, "ie7"), 0, timeout);
       assertSimilar(new String[] {"ie6", "ie7", "gecko", "gecko1_8", "safari"},
           queue.getUserAgents());
     }
 
     // Add existing clients.
     {
-      queue.getTestBlock("client0", "desc0", "ie6", 0, timeout);
-      queue.reportFatalLaunch("client1", "desc1", "gecko", null);
-      queue.reportResults("client2", "desc2", "safari", createTestResults(0));
+      queue.getTestBlock(createClientInfo(0, "ie6"), 0, timeout);
+      queue.reportFatalLaunch(createClientInfo(1, "gecko"), null);
+      queue.reportResults(createClientInfo(2, "safari"), createTestResults(0));
       assertSimilar(new String[] {"ie6", "ie7", "gecko", "gecko1_8", "safari"},
           queue.getUserAgents());
     }
@@ -312,7 +320,7 @@
     {
       Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
       results.put(test0_0, new JUnitResult());
-      queue.reportResults("client0", "desc0", "ie6", results);
+      queue.reportResults(createClientInfo(0, "ie6"), results);
       assertFalse(queue.hasResults(test0_0));
       assertFalse(queue.hasResults(test0_1));
       assertFalse(queue.hasResults(test0_2));
@@ -325,7 +333,7 @@
     {
       Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
       results.put(test0_0, new JUnitResult());
-      queue.reportResults("client1", "desc1", "ie6", results);
+      queue.reportResults(createClientInfo(1, "ie6"), results);
       assertFalse(queue.hasResults(test0_0));
       assertFalse(queue.hasResults(test0_1));
       assertFalse(queue.hasResults(test0_2));
@@ -338,7 +346,7 @@
     {
       Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
       results.put(test0_1, new JUnitResult());
-      queue.reportResults("client0", "desc0", "ie6", results);
+      queue.reportResults(createClientInfo(0, "ie6"), results);
       assertFalse(queue.hasResults(test0_0));
       assertFalse(queue.hasResults(test0_1));
       assertFalse(queue.hasResults(test0_2));
@@ -351,7 +359,7 @@
     {
       Map<TestInfo, JUnitResult> results = new HashMap<TestInfo, JUnitResult>();
       results.put(test0_0, new JUnitResult());
-      queue.reportResults("client2", "desc2", "ie6", results);
+      queue.reportResults(createClientInfo(2, "ie6"), results);
       assertTrue(queue.hasResults(test0_0));
       assertFalse(queue.hasResults(test0_1));
       assertFalse(queue.hasResults(test0_2));
@@ -370,12 +378,12 @@
     JUnitResult junitResult = new JUnitResult();
     junitResult.setException(new UnableToCompleteException());
     results.put(testInfo, junitResult);
-    queue.reportResults("client0", "desc0", "ie6", results);
+    queue.reportResults(createClientInfo(0, "ie6"), results);
     results = new HashMap<TestInfo, JUnitResult>();
     junitResult = new JUnitResult();
     junitResult.setException(new JUnitFatalLaunchException());
     results.put(testInfo, junitResult);
-    queue.reportResults("client1", "desc1", "ff3",
+    queue.reportResults(createClientInfo(1, "ff3"),
         createTestResults(ONE_TEST_PER_BLOCK));
     assertTrue(queue.needsRerunning(testInfo));
 
@@ -385,8 +393,8 @@
     junitResult = new JUnitResult();
     junitResult.setException(new JUnitFatalLaunchException());
     results.put(testInfo, junitResult);
-    queue.reportResults("client0", "desc0", "ie6", results);
-    queue.reportResults("client1", "desc1", "ff3",
+    queue.reportResults(createClientInfo(0, "ie6"), results);
+    queue.reportResults(createClientInfo(1, "ff3"),
         createTestResults(ONE_TEST_PER_BLOCK));
     assertFalse(queue.needsRerunning(testInfo));
   }
@@ -398,11 +406,11 @@
 
     // incomplete results
     assertTrue(queue.needsRerunning(testInfo));
-    queue.reportResults("client0", "desc0", "ff3", createTestResults(1));
+    queue.reportResults(createClientInfo(0, "ff3"), createTestResults(1));
     assertTrue(queue.needsRerunning(testInfo));
 
     // complete results
-    queue.reportResults("client1", "desc1", "ie7", createTestResults(1));
+    queue.reportResults(createClientInfo(1, "ie7"), createTestResults(1));
     assertFalse(queue.needsRerunning(testInfo));
   }
 
@@ -413,9 +421,9 @@
 
     // Add some clients in a few ways.
     {
-      queue.getTestBlock("client0", "desc0", "ie6", 0, timeout);
-      queue.reportFatalLaunch("client1", "desc1", "gecko", null);
-      queue.reportResults("client2", "desc2", "safari", createTestResults(0));
+      queue.getTestBlock(createClientInfo(0, "ie6"), 0, timeout);
+      queue.reportFatalLaunch(createClientInfo(1, "gecko"), null);
+      queue.reportResults(createClientInfo(2, "safari"), createTestResults(0));
       assertSimilar(new String[] {"desc0", "desc1", "desc2"},
           queue.getNewClients());
       assertEquals(0, queue.getNewClients().length);
@@ -423,19 +431,19 @@
 
     // Add duplicate clients.
     {
-      queue.getTestBlock("client3", "desc3", "ie6", 0, timeout);
-      queue.reportFatalLaunch("client3", "desc3", "ie6", null);
-      queue.reportResults("client4", "desc4", "safari", createTestResults(0));
-      queue.getTestBlock("client3", "desc3", "ie6", 0, timeout);
+      queue.getTestBlock(createClientInfo(3, "ie6"), 0, timeout);
+      queue.reportFatalLaunch(createClientInfo(3, "ie6"), null);
+      queue.reportResults(createClientInfo(4, "safari"), createTestResults(0));
+      queue.getTestBlock(createClientInfo(3, "ie6"), 0, timeout);
       assertSimilar(new String[] {"desc3", "desc4"}, queue.getNewClients());
       assertEquals(0, queue.getNewClients().length);
     }
 
     // Add existing clients.
     {
-      queue.getTestBlock("client0", "desc0", "ie6", 0, timeout);
-      queue.reportFatalLaunch("client1", "desc1", "gecko", null);
-      queue.reportResults("client2", "desc2", "safari", createTestResults(0));
+      queue.getTestBlock(createClientInfo(0, "ie6"), 0, timeout);
+      queue.reportFatalLaunch(createClientInfo(1, "gecko"), null);
+      queue.reportResults(createClientInfo(2, "safari"), createTestResults(0));
       assertEquals(0, queue.getNewClients().length);
     }
   }
@@ -446,10 +454,10 @@
     TestInfo testInfo = queue.getTestBlocks().get(0)[0];
     assertFalse(queue.hasResults(testInfo));
 
-    queue.reportResults("client0", "desc0", "ie6",
+    queue.reportResults(createClientInfo(0, "ie6"),
         createTestResults(ONE_TEST_PER_BLOCK));
     assertFalse(queue.hasResults(testInfo));
-    queue.reportResults("client1", "desc1", "ff3",
+    queue.reportResults(createClientInfo(1, "ff3"),
         createTestResults(ONE_TEST_PER_BLOCK));
     assertTrue(queue.hasResults(testInfo));
 
@@ -465,7 +473,7 @@
     JUnitResult junitResult = new JUnitResult();
     junitResult.setException(new AssertionError());
     results.put(testInfo, junitResult);
-    queue.reportResults("client0", "desc0", "ff3", results);
+    queue.reportResults(createClientInfo(0, "ff3"), results);
     assertTrue(queue.needsRerunning(testInfo));
     Map<ClientStatus, JUnitResult> queueResults = queue.getResults(testInfo);
     assertEquals(1, queueResults.size());
@@ -475,9 +483,9 @@
 
     queue.removeResults(testInfo);
 
-    queue.reportResults("client0", "desc0", "ff3",
+    queue.reportResults(createClientInfo(0, "ff3"),
         createTestResults(ONE_TEST_PER_BLOCK));
-    queue.reportResults("client1", "desc1", "ie6",
+    queue.reportResults(createClientInfo(1, "ie6"),
         createTestResults(ONE_TEST_PER_BLOCK));
     assertFalse(queue.needsRerunning(testInfo));
     // check that the updated result appears now.
@@ -496,17 +504,12 @@
    * @param actual the actual array
    */
   private void assertSimilar(String[] expected, String[] actual) {
-    assertEquals(expected.length, actual.length);
-    for (int i = 0; i < expected.length; i++) {
-      String expectedItem = expected[i];
-      boolean matched = false;
-      for (int j = 0; j < actual.length; j++) {
-        if (expectedItem == actual[j]) {
-          matched = true;
-        }
-      }
-      assertTrue(matched);
-    }
+    assertEquals(new HashSet<String>(Arrays.asList(expected)),
+        new HashSet<String>(Arrays.asList(actual)));
+  }
+
+  private ClientInfoExt createClientInfo(int sessionId, String userAgent) {
+    return new ClientInfoExt(sessionId, userAgent, "desc" + sessionId);
   }
 
   /**