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;
+ }
+ }
+}