Adds the undocumented command line argument -externalbrowsers to
aid in running the continuous build unit tests.  The comma separated
list of executable paths provided will be launched to run the
unit tests in parallel.

Patch by: zundel
Review by: scottb



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@2614 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index 96709db..6aeddb6 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -286,6 +286,37 @@
       }
     });
 
+    registerHandler(new ArgHandlerString() {
+
+      @Override
+      public String getPurpose() {
+        return "Run external browsers in web mode (pass a comma separated list of executables.)";
+      }
+
+      @Override
+      public String getTag() {
+        return "-externalbrowser";
+      }
+
+      @Override
+      public String[] getTagArgs() {
+        return new String[] {"browserPaths"};
+      }
+
+      @Override
+      public boolean isUndocumented() {
+        return true;
+      }
+
+      @Override
+      public boolean setString(String str) {
+        String[] paths = str.split(",");
+        runStyle = new RunStyleExternalBrowser(JUnitShell.this, paths);
+        numClients = paths.length;
+        return runStyle != null;
+      }
+    });
+
     registerHandler(new ArgHandler() {
 
       @Override
@@ -300,7 +331,7 @@
 
       @Override
       public String getTag() {
-        return "-wait";
+        return "-manual";
       }
 
       @Override
@@ -328,7 +359,7 @@
       }
 
       public void setInt(int value) {
-        runStyle = new RunStyleWait(JUnitShell.this, value);
+        runStyle = new RunStyleManual(JUnitShell.this, value);
         numClients = value;
       }
 
@@ -411,7 +442,7 @@
   @Override
   protected boolean notDone() {
     int activeClients = messageQueue.getNumClientsRetrievedCurrentTest();
-    if (firstLaunch && runStyle instanceof RunStyleWait) {
+    if (firstLaunch && runStyle instanceof RunStyleManual) {
       String[] newClients = messageQueue.getNewClients();
       int printIndex = activeClients - newClients.length + 1;
       for (String newClient : newClients) {
diff --git a/user/src/com/google/gwt/junit/RunStyleExternalBrowser.java b/user/src/com/google/gwt/junit/RunStyleExternalBrowser.java
new file mode 100644
index 0000000..e3c1eea
--- /dev/null
+++ b/user/src/com/google/gwt/junit/RunStyleExternalBrowser.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2008 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.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+
+/**
+ * Runs in web mode via browsers managed as an external process. This feature is
+ * experimental and is not officially supported.
+ */
+class RunStyleExternalBrowser extends RunStyle {
+
+  private static class ExternalBrowser {
+    String browserPath;
+    Process process;
+
+    public ExternalBrowser(String browserPath) {
+      this.browserPath = browserPath;
+    }
+
+    public String getPath() {
+      return browserPath;
+    }
+
+    public Process getProcess() {
+      return process;
+    }
+
+    public void setProcess(Process process) {
+      this.process = process;
+    }
+  }
+
+  private final ExternalBrowser[] externalBrowsers;
+
+  /**
+   * @param shell the containing shell
+   * @param browsers an array of path names pointing to browser executables.
+   */
+  public RunStyleExternalBrowser(JUnitShell shell, String browsers[]) {
+    super(shell);
+    synchronized (this) {
+      this.externalBrowsers = new ExternalBrowser[browsers.length];
+      for (int i = 0; i < browsers.length; ++i) {
+        externalBrowsers[i] = new ExternalBrowser(browsers[i]);
+      }
+    }
+    Runtime.getRuntime().addShutdownHook(new ShutdownCb());
+  }
+
+  @Override
+  public boolean isLocal() {
+    return false;
+  }
+
+  @Override
+  public synchronized void launchModule(String moduleName)
+      throws UnableToCompleteException {
+    String commandArray[] = new String[2];
+    // construct the URL for the browser to hit
+    commandArray[1] = "http://" + "localhost" + ":" + shell.getPort() + "/"
+        + getUrlSuffix(moduleName);
+
+    Process child = null;
+    for (ExternalBrowser browser : externalBrowsers) {
+      try {
+        commandArray[0] = browser.getPath();
+
+        child = Runtime.getRuntime().exec(commandArray);
+        if (child == null) {
+          getLogger().log(TreeLogger.ERROR,
+              "Problem exec()'ing " + commandArray[0]);
+          throw new UnableToCompleteException();
+        }
+      } catch (Exception e) {
+        getLogger().log(TreeLogger.ERROR,
+            "Error launching external browser at " + browser.getPath(), e);
+        throw new UnableToCompleteException();
+      }
+      browser.setProcess(child);
+    }
+  }
+
+  @Override
+  public boolean wasInterrupted() {
+    
+    // Make sure all browsers are still running 
+    for (ExternalBrowser browser : externalBrowsers) {
+      try {
+        browser.getProcess().exitValue();
+      } catch (IllegalThreadStateException e) {
+        // The process is still active, keep looking.
+        continue;
+      }
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Registered as a shutdown hook to make sure that any browsers that were not
+   * finished are killed.
+   */
+  private class ShutdownCb extends Thread {
+
+    @Override
+    public void run() {
+      for (ExternalBrowser browser : externalBrowsers) {
+        try {
+          browser.getProcess().exitValue();
+        } catch (IllegalThreadStateException e) {
+          // The process is still active. Kill it.
+          browser.getProcess().destroy();
+        }
+      }
+    }
+  }
+
+  @Override
+  public void maybeCompileModule(String moduleName)
+      throws UnableToCompleteException {
+    shell.compileForWebMode(moduleName, null);
+  }
+
+}
diff --git a/user/src/com/google/gwt/junit/RunStyleWait.java b/user/src/com/google/gwt/junit/RunStyleManual.java
similarity index 89%
rename from user/src/com/google/gwt/junit/RunStyleWait.java
rename to user/src/com/google/gwt/junit/RunStyleManual.java
index 3dd38fe..1b36d16 100644
--- a/user/src/com/google/gwt/junit/RunStyleWait.java
+++ b/user/src/com/google/gwt/junit/RunStyleManual.java
@@ -21,11 +21,11 @@
  * Runs in web mode waiting for the user to contact the server with their own
  * browser.
  */
-class RunStyleWait extends RunStyleRemote {
+class RunStyleManual extends RunStyleRemote {
 
   private final int numClients;
 
-  public RunStyleWait(JUnitShell shell, int numClients) {
+  public RunStyleManual(JUnitShell shell, int numClients) {
     super(shell);
     this.numClients = numClients;
   }
@@ -49,7 +49,7 @@
   @Override
   public void maybeCompileModule(String moduleName)
       throws UnableToCompleteException {
-    System.out.print("Compling " + moduleName + "...");
+    System.out.print("Compiling " + moduleName + "...");
     super.maybeCompileModule(moduleName);
     System.out.println(" success.");
   }