Adds support for running GWT Unit Tests through a Selenium-RC server.

For information about Selenium-RC, see: http://selenium-rc.openqa.org/

Right now, it does not allow you do perform any of the neat things 
Selenium will allow you to do, but simply uses the client/server 
infrastructure to launch a browser to talk to the GWTServlet instance 
started by JUnitShell.  Selenium-RC handles a lot of nasty problems when 
launching a browser - like handling startup/shutdown dialogs that require 
user interaction and allowing more than one mozilla instance to run on a 
machine at a time.

GWT will be packaged with the client and server jars in the tools directory.  
The client jar will be bundled in to gwt-user.jar and you can add 
selenium servers as targets to run the GWT unit tests against by setting the 
property -Dgwt.selenium.hosts when you run 'ant'.

Patch by: jgw,zundel
Review by: jgw



git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@3005 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/common.ant.xml b/common.ant.xml
index 4070dfd..6708595 100755
--- a/common.ant.xml
+++ b/common.ant.xml
@@ -150,6 +150,7 @@
 				<classpath>
 					<pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
 					<pathelement location="${gwt.tools.antlib}/ant-junit-1.6.5.jar" />
+                                        <pathelement location="${gwt.tools.lib}/selenium/selenium-java-client-driver.jar" />
 				</classpath>
 			</taskdef>
 
@@ -171,6 +172,7 @@
 					<pathelement location="${javac.out}" />
 					<pathelement location="${gwt.dev.staging.jar}" />
 					<pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
+                                        <pathelement location="${gwt.tools.lib}/selenium/selenium-java-client-driver.jar" />
 					<extraclasspaths />
 				</classpath>
 
diff --git a/eclipse/user/.classpath b/eclipse/user/.classpath
index e7b1f4c..8e4fa1b 100644
--- a/eclipse/user/.classpath
+++ b/eclipse/user/.classpath
@@ -7,6 +7,7 @@
 	<classpathentry exported="true" kind="var" path="GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2.jar" sourcepath="/GWT_TOOLS/lib/apache/tapestry-util-text-4.0.2-src.zip"/>
 	<classpathentry exported="true" kind="var" path="GWT_TOOLS/lib/junit/junit-3.8.1.jar" sourcepath="/GWT_TOOLS/lib/junit/junit-3.8.1-src.zip"/>
 	<classpathentry exported="true" kind="var" path="GWT_TOOLS/lib/tomcat/servlet-api-2.4.jar" sourcepath="/GWT_TOOLS/lib/tomcat/jakarta-tomcat-5.0.28-src.zip"/>
+	<classpathentry kind="var" path="GWT_TOOLS/lib/selenium/selenium-java-client-driver.jar"/>
 	<classpathentry combineaccessrules="false" kind="src" path="/gwt-dev-windows"/>
 	<classpathentry kind="output" path="bin"/>
 </classpath>
diff --git a/user/build.xml b/user/build.xml
index 3718922..8e85c2f 100755
--- a/user/build.xml
+++ b/user/build.xml
@@ -26,6 +26,7 @@
 				<pathelement location="${gwt.tools.lib}/tomcat/servlet-api-2.4.jar" />
 				<pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
 				<pathelement location="${gwt.tools.lib}/jfreechart/jfreechart-1.0.3.jar" />
+				<pathelement location="${gwt.tools.lib}/selenium/selenium-java-client-driver.jar" />
 				<pathelement location="${gwt.dev.jar}" />
 			</classpath>
 		</gwt.javac>
@@ -50,6 +51,7 @@
 				<pathelement location="${javac.out}" />
 				<pathelement location="${gwt.build}/out/dev/core/bin-test" />
 				<pathelement location="${gwt.tools.lib}/junit/junit-3.8.1.jar" />
+				<pathelement location="${gwt.tools.lib}/selenium/selenium-java-client-driver.jar" />
 				<pathelement location="${gwt.dev.staging.jar}" />
 			</classpath>
 		</gwt.javac>
@@ -62,6 +64,7 @@
 			<fileset dir="super" excludes="**/package.html" />
 			<fileset dir="${javac.out}" />
 			<zipfileset src="${gwt.tools.lib}/tomcat/servlet-api-2.4.jar" />
+		        <zipfileset src="${gwt.tools.lib}/selenium/selenium-java-client-driver.jar" />
 		</gwt.jar>
 	</target>
 
@@ -75,7 +78,16 @@
 
 	<target name="remoteweb-test" description="Run a remoteweb test at the given host and path" if="gwt.remote.browsers">
 		<echo message="Performing remote browser testing at ${gwt.remote.browsers}" />
-		<gwt.junit test.args="${test.args} -out www -web -remoteweb ${gwt.remote.browsers}" test.out="${junit.out}/remoteweb" test.cases="default.web.tests" >
+		<gwt.junit test.args="${test.args} -out www -remoteweb ${gwt.remote.browsers}" test.out="${junit.out}/remoteweb" test.cases="default.web.tests" >
+			<extraclasspaths>
+				<pathelement location="${gwt.build}/out/dev/core/bin-test" />
+			</extraclasspaths>
+		</gwt.junit>
+	</target>
+
+	<target name="selenium-test" description="Run a remote test using Selenium RC test at the given host and path" if="gwt.selenium.hosts">
+		<echo message="Performing remote browser testing using Selenium RC at ${gwt.selenium.hosts}" />
+		<gwt.junit test.args="${test.args} -out www -selenium ${gwt.selenium.hosts}" test.out="${junit.out}/selenium" test.cases="default.web.tests" >
 			<extraclasspaths>
 				<pathelement location="${gwt.build}/out/dev/core/bin-test" />
 			</extraclasspaths>
@@ -98,7 +110,7 @@
 		</gwt.junit>
 	</target>
 
-	<target name="test" depends="compile, compile.tests" description="Run hosted-mode, web-mode and remoteweb tests for this project.">
+	<target name="test" depends="compile, compile.tests" description="Run hosted-mode, web-mode, remoteweb, and selenium tests for this project.">
 		<property.ensure name="distro.built" location="${gwt.dev.staging.jar}" message="GWT must be built before performing any tests.  This can be fixed by running ant in the ${gwt.root} directory." />
 
 		<!--
@@ -107,6 +119,8 @@
 		-->
 		<limit failonerror="true" hours="2">
 		<parallel threadsPerProcessor="1">
+			<!-- selenium-test is a no-op unless gwt.selenium.hosts is defined -->
+			<antcall target="selenium-test"/>
 			<!-- remoteweb-test is a no-op unless gwt.remote.browsers is defined -->
 			<antcall target="remoteweb-test"/>
 			<antcall target="test.hosted"/>
diff --git a/user/src/com/google/gwt/junit/JUnitShell.java b/user/src/com/google/gwt/junit/JUnitShell.java
index 74d2837..f31a6fb 100644
--- a/user/src/com/google/gwt/junit/JUnitShell.java
+++ b/user/src/com/google/gwt/junit/JUnitShell.java
@@ -272,7 +272,8 @@
 
       @Override
       public String getPurpose() {
-        return "Runs web mode via RMI to a BrowserManagerServer; e.g. rmi://localhost/ie6";
+        return "Runs web mode via RMI to a set of BrowserManagerServers; "
+            + "e.g. rmi://localhost/ie6,rmi://localhost/firefox";
       }
 
       @Override
@@ -303,6 +304,33 @@
 
       @Override
       public String getPurpose() {
+        return "Runs web mode via HTTP to a set of Selenium servers; "
+            + "e.g. localhost:4444/*firefox,remotehost:4444/*iexplore";
+      }
+
+      @Override
+      public String getTag() {
+        return "-selenium";
+      }
+
+      @Override
+      public String[] getTagArgs() {
+        return new String[] {"seleniumHost"};
+      }
+
+      @Override
+      public boolean setString(String str) {
+        String[] targets = str.split(",");
+        numClients = targets.length;
+        runStyle = RunStyleSelenium.create(JUnitShell.this, targets);
+        return runStyle != null;
+      }
+    });
+
+    registerHandler(new ArgHandlerString() {
+
+      @Override
+      public String getPurpose() {
         return "Run external browsers in web mode (pass a comma separated list of executables.)";
       }
 
diff --git a/user/src/com/google/gwt/junit/RunStyleSelenium.java b/user/src/com/google/gwt/junit/RunStyleSelenium.java
new file mode 100644
index 0000000..c8d8baf
--- /dev/null
+++ b/user/src/com/google/gwt/junit/RunStyleSelenium.java
@@ -0,0 +1,202 @@
+/*
+ * 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;
+
+import com.thoughtworks.selenium.DefaultSelenium;
+import com.thoughtworks.selenium.Selenium;
+import com.thoughtworks.selenium.SeleniumException;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Runs in web mode via browsers managed by Selenium.
+ */
+public class RunStyleSelenium extends RunStyleRemote {
+
+  private static class RCSelenium {
+    final String browser;
+    final String host;
+    final int port;
+    Selenium selenium;
+
+    public RCSelenium(String browser, String host, int port) {
+      this.browser = browser;
+      this.host = host;
+      this.port = port;
+    }
+
+    public void createSelenium(String domain) {
+      this.selenium = new DefaultSelenium(host, port, browser, domain);
+    }
+
+    public String getBrowser() {
+      return browser;
+    }
+
+    public String getHost() {
+      return host;
+    }
+
+    public int getPort() {
+      return port;
+    }
+
+    public Selenium getSelenium() {
+      return selenium;
+    }
+  }
+
+  public static RunStyle create(JUnitShell shell, String[] targetsIn) {
+    RCSelenium targets[] = new RCSelenium[targetsIn.length];
+
+    Pattern pattern = Pattern.compile("([\\w\\.-]+):([\\d]+)/([\\w\\*]+)");
+    for (int i = 0; i < targets.length; ++i) {
+      Matcher matcher = pattern.matcher(targetsIn[i]);
+      if (!matcher.matches()) {
+        throw new JUnitFatalLaunchException("Unable to parse Selenium target "
+            + targets[i] + " (expected format is [host]:[port]/[browser])");
+      }
+      RCSelenium instance = new RCSelenium(matcher.group(3), matcher.group(1),
+          Integer.parseInt(matcher.group(2)));
+      targets[i] = instance;
+    }
+
+    return new RunStyleSelenium(shell, targets);
+  }
+
+  private RCSelenium remotes[];
+
+  /**
+   * Whether one of the remote browsers was interrupted.
+   */
+  private boolean wasInterrupted;
+
+  /**
+   * A separate lock to control access to {@link #wasInterrupted}. This keeps
+   * the main thread calls into {@link #wasInterrupted()} from having to be
+   * synchronized on the containing instance and potentially block on RPC calls.
+   * It is okay to take the {@link #wasInterruptedLock} while locking the
+   * containing instance; it is NOT okay to do the opposite or deadlock could
+   * occur.
+   */
+  private final Object wasInterruptedLock = new Object();
+
+  public RunStyleSelenium(final JUnitShell shell, RCSelenium targets[]) {
+
+    super(shell);
+    this.remotes = targets;
+
+    // Install a shutdown hook that will close all of our outstanding Selenium
+    // sessions.
+    Runtime.getRuntime().addShutdownHook(new Thread() {
+      @Override
+      public void run() {
+        for (RCSelenium remote : remotes) {
+          if (remote.getSelenium() != null) {
+            try {
+              remote.getSelenium().stop();
+            } catch (SeleniumException se) {
+              shell.getTopLogger().log(TreeLogger.WARN,
+                  "Error stoping selenium session", se);
+            }
+          }
+        }
+      }
+    });
+
+    // Crank up the keep-alive thread. This will periodically check for failure
+    // of the Selenium session and stop the test if something goes wrong.
+    Thread keepAliveThread = new Thread() {
+      @Override
+      public void run() {
+        do {
+          try {
+            Thread.sleep(1000);
+          } catch (InterruptedException ignored) {
+          }
+        } while (doKeepAlives());
+      }
+    };
+    keepAliveThread.setDaemon(true);
+    keepAliveThread.start();
+  }
+
+  @Override
+  public synchronized void launchModule(String moduleName)
+      throws UnableToCompleteException {
+    // Get the localhost address.
+    String domain;
+    try {
+      String localhost = InetAddress.getLocalHost().getHostAddress();
+      domain = "http://" + localhost + ":" + shell.getPort() + "/";
+    } catch (UnknownHostException e) {
+      throw new RuntimeException("Unable to determine my ip address", e);
+    }
+
+    // Startup all the selenia and point them at the module url.
+    for (RCSelenium remote : remotes) {
+      try {
+        shell.getTopLogger().log(TreeLogger.TRACE,
+            "Starting with domain: " + domain 
+            + " Opening URL: " + getMyUrl(moduleName)); 
+        remote.createSelenium(domain);
+        remote.getSelenium().start();
+        remote.getSelenium().open(getMyUrl(moduleName));
+      } catch (Exception e) {
+        shell.getTopLogger().log(TreeLogger.ERROR,
+            "Error launching browser via Selenium-RC at " + remote.getHost(), e);
+      }
+    }
+  }
+
+  @Override
+  public boolean wasInterrupted() {
+    synchronized (wasInterruptedLock) {
+      return wasInterrupted;
+    }
+  }
+
+  private synchronized boolean doKeepAlives() {
+    if (remotes != null) {
+      for (RCSelenium remote : remotes) {
+        // Use getTitle() as a cheap way to see if the Selenium server's still
+        // responding (Selenium seems to provide no way to check the server
+        // status directly).
+        try {
+          if (remote.getSelenium() != null) {
+            remote.getSelenium().getTitle();
+          }
+        } catch (Throwable e) {
+          setWasInterrupted(true);
+        }
+      }
+    }
+
+    return !wasInterrupted();
+  }
+
+  private void setWasInterrupted(boolean wasInterrupted) {
+    synchronized (wasInterruptedLock) {
+      this.wasInterrupted = wasInterrupted;
+    }
+  }
+}