Fix for RunStyleSelenium when Selenium.start() blocks indefinitely.
http://gwt-code-reviews.appspot.com/296801/show
Review by: rdayal@google.com
git-svn-id: https://google-web-toolkit.googlecode.com/svn/trunk@7861 8db76d5a-ed1c-0410-87a9-c151d255dfc7
diff --git a/user/src/com/google/gwt/junit/RunStyleSelenium.java b/user/src/com/google/gwt/junit/RunStyleSelenium.java
index afaa502..74e2332 100644
--- a/user/src/com/google/gwt/junit/RunStyleSelenium.java
+++ b/user/src/com/google/gwt/junit/RunStyleSelenium.java
@@ -16,12 +16,14 @@
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.util.ArrayList;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -30,6 +32,13 @@
* Runs via browsers managed by Selenium.
*/
public class RunStyleSelenium extends RunStyle {
+
+ /**
+ * The maximum amount of time that a selenia can take to start in
+ * milliseconds. 10 minutes.
+ */
+ private static final int LAUNCH_TIMEOUT = 10 * 60 * 1000;
+
/**
* Wraps a Selenium instance.
*/
@@ -88,13 +97,184 @@
}
/**
+ * A {@link Thread} used to interact with {@link Selenium} instances. Selenium
+ * does not support execution of multiple methods at the same time, so its
+ * important to make sure that {@link SeleniumThread#isComplete()} returns
+ * true before calling more methods in {@link Selenium}.
+ */
+ class SeleniumThread extends Thread {
+
+ /**
+ * {@link RunStyleSelenium#lock} is sometimes active when calling
+ * {@link #isComplete()}, so we need a separate lock to avoid deadlock.
+ */
+ Object accessLock = new Object();
+
+ /**
+ * The exception thrown while running this thread, if any.
+ */
+ private Throwable exception;
+
+ /**
+ * True if the selenia has successfully completed the action. Protected by
+ * {@link #accessLock}.
+ */
+ private boolean isComplete;
+
+ private final SeleniumWrapper remote;
+
+ /**
+ * Construct a new {@link SeleniumThread}.
+ *
+ * @param remote the {@link SeleniumWrapper} instance
+ */
+ public SeleniumThread(SeleniumWrapper remote) {
+ this.remote = remote;
+ setDaemon(true);
+ }
+
+ /**
+ * Get the {@link Throwable} caused by the action.
+ *
+ * @return the exception if one occurred, null if none occurred
+ */
+ public Throwable getException() {
+ synchronized (accessLock) {
+ return exception;
+ }
+ }
+
+ public SeleniumWrapper getRemote() {
+ return remote;
+ }
+
+ public boolean isComplete() {
+ synchronized (accessLock) {
+ return isComplete;
+ }
+ }
+
+ protected void markComplete() {
+ synchronized (accessLock) {
+ isComplete = true;
+ }
+ }
+
+ protected void setException(Throwable e) {
+ synchronized (accessLock) {
+ this.exception = e;
+ isComplete = true;
+ }
+ }
+ }
+
+ /**
+ * <p>
+ * The {@link Thread} used to launch a module on a single Selenium target. We
+ * launch {@link Selenium} instances in a separate thread because
+ * {@link Selenium#start()} can hang if the browser cannot be opened
+ * successfully. Instead of blocking the test indefinitely, we use a separate
+ * thread and timeout if needed.
+ * </p>
+ * <p>
+ * We wait until {@link LaunchThread#isComplete()} returns <code>true</code>
+ * before starting the keep alive thread or creating a {@link StopThread}, so
+ * no other thread can be accessing {@link Selenium} at the same time.
+ * </p>
+ */
+ class LaunchThread extends SeleniumThread {
+
+ private final String moduleName;
+
+ /**
+ * Construct a new {@link LaunchThread}.
+ *
+ * @param remote the remote {@link SeleniumWrapper} instance
+ * @param moduleName the module to load
+ */
+ public LaunchThread(SeleniumWrapper remote, String moduleName) {
+ super(remote);
+ this.moduleName = moduleName;
+ }
+
+ @Override
+ public void run() {
+ SeleniumWrapper remote = getRemote();
+ try {
+ String domain = "http://" + getLocalHostName() + ":" + shell.getPort()
+ + "/";
+ String url = shell.getModuleUrl(moduleName);
+
+ // Create the selenium instance and open the browser.
+ shell.getTopLogger().log(TreeLogger.TRACE,
+ "Starting with domain: " + domain + " Opening URL: " + url);
+ remote.createSelenium(domain);
+ remote.getSelenium().start();
+
+ // We set the speed to 1000ms as a workaround a bug where Selenium#open
+ // can hang.
+ remote.getSelenium().setSpeed("1000");
+ remote.getSelenium().open(url);
+ remote.getSelenium().setSpeed("0");
+
+ markComplete();
+ } catch (Throwable e) {
+ shell.getTopLogger().log(
+ TreeLogger.ERROR,
+ "Error launching browser via Selenium-RC at "
+ + remote.getSpecifier(), e);
+ setException(e);
+ }
+ }
+ }
+
+ /**
+ * <p>
+ * The {@link Thread} used to stop a selenium instance.
+ * </p>
+ * <p>
+ * We stop the keep alive thread before creating {@link StopThread}s, and we
+ * do not create {@link StopThread}s if a {@link LaunchThread} is still
+ * running for a {@link Selenium} instance, so no other thread can possible be
+ * accessing {@link Selenium} at the same time.
+ * </p>
+ */
+ class StopThread extends SeleniumThread {
+
+ public StopThread(SeleniumWrapper remote) {
+ super(remote);
+ }
+
+ @Override
+ public void run() {
+ SeleniumWrapper remote = getRemote();
+ try {
+ remote.getSelenium().stop();
+ markComplete();
+ } catch (Throwable e) {
+ shell.getTopLogger().log(TreeLogger.WARN,
+ "Error stopping selenium session at " + remote.getSpecifier(), e);
+ setException(e);
+ }
+ }
+ }
+
+ /**
* The list of hosts that were interrupted. Protected by {@link #lock}.
*/
private Set<String> interruptedHosts;
/**
- * Indicates that testing has stopped, and we no longer need to run keep
- * alive checks. Protected by {@link #lock}.
+ * We keep a list of {@link LaunchThread} instances so that we know which
+ * selenia successfully started. Only selenia that have been successfully
+ * started should be stopped when the test is finished. Protected by
+ * {@link #lock};
+ */
+ private List<LaunchThread> launchThreads = new ArrayList<LaunchThread>();
+
+ /**
+ * Indicates that testing has stopped, and we no longer need to run keep alive
+ * checks. Protected by {@link #lock}.
*/
private boolean stopped;
@@ -153,49 +333,61 @@
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
+ List<StopThread> stopThreads = new ArrayList<StopThread>();
synchronized (lock) {
stopped = true;
- for (SeleniumWrapper remote : remotes) {
- if (remote.getSelenium() != null) {
- try {
- remote.getSelenium().stop();
- } catch (SeleniumException se) {
- shell.getTopLogger().log(TreeLogger.WARN,
- "Error stopping selenium session", se);
- }
+ for (LaunchThread launchThread : launchThreads) {
+ // Closing selenium instances that have not successfully started
+ // results in an error on the selenium client. By doing this check,
+ // we are ensuring that no other calls to the remote instance are
+ // being done by another thread.
+ if (launchThread.isComplete()) {
+ StopThread stopThread = new StopThread(launchThread.getRemote());
+ stopThreads.add(stopThread);
+ stopThread.start();
}
}
}
+
+ // Wait for all threads to stop.
+ try {
+ waitForThreadsToComplete(stopThreads, false, "stop", 500);
+ } catch (UnableToCompleteException e) {
+ // This should never happen.
+ }
}
});
- start();
return targets.length;
}
@Override
- public void launchModule(String moduleName) {
- // Get the localhost address.
- String domain = "http://" + getLocalHostName() + ":" + shell.getPort()
- + "/";
-
+ public void launchModule(String moduleName) throws UnableToCompleteException {
// Startup all the selenia and point them at the module url.
+ for (SeleniumWrapper remote : remotes) {
+ LaunchThread thread = new LaunchThread(remote, moduleName);
+ synchronized (lock) {
+ launchThreads.add(thread);
+ }
+ thread.start();
+ }
+
+ // Wait for all selenium targets to start.
+ waitForThreadsToComplete(launchThreads, true, "start", 1000);
+
+ // Check if any threads have thrown an exception. We wait until all threads
+ // have had a change to start so that we don't shutdown while some threads
+ // are still starting.
synchronized (lock) {
- for (SeleniumWrapper remote : remotes) {
- try {
- String url = shell.getModuleUrl(moduleName);
- shell.getTopLogger().log(TreeLogger.TRACE,
- "Starting with domain: " + domain + " Opening URL: " + url);
- remote.createSelenium(domain);
- remote.getSelenium().start();
- remote.getSelenium().open(url);
- } catch (Exception e) {
- shell.getTopLogger().log(
- TreeLogger.ERROR,
- "Error launching browser via Selenium-RC at "
- + remote.getSpecifier(), e);
+ for (LaunchThread thread : launchThreads) {
+ if (thread.getException() != null) {
+ // The thread has already logged the exception.
+ throw new UnableToCompleteException();
}
}
}
+
+ // Start the keep alive thread.
+ start();
}
/**
@@ -221,6 +413,7 @@
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
+ break;
}
} while (doKeepAlives());
}
@@ -250,8 +443,8 @@
// If we ask for the title of the page while a new module is
// loading, IE will throw a permission denied exception.
String message = e.getMessage();
- if (message != null
- && message.toLowerCase().contains("permission denied")) {
+ if (message == null
+ || !message.toLowerCase().contains("permission denied")) {
if (interruptedHosts == null) {
interruptedHosts = new HashSet<String>();
}
@@ -263,4 +456,73 @@
return interruptedHosts == null;
}
}
+
+ /**
+ * Get the display list of specifiers for threads that did not complete.
+ *
+ * @param threads the list of threads
+ * @return a list of specifiers
+ */
+ private <T extends SeleniumThread> String getIncompleteSpecifierList(
+ List<T> threads) {
+ String list = "";
+ for (SeleniumThread thread : threads) {
+ if (!thread.isComplete()) {
+ list += " " + thread.getRemote().getSpecifier() + "\n";
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Iterate over a list of {@link SeleniumThread}s, waiting for them to finish.
+ *
+ * @param <T> the thread type
+ * @param threads the list of threads
+ * @param fatalExceptions true to treat all exceptions as errors, false to
+ * treat exceptions as warnings
+ * @param action the action being performed by the thread
+ * @param sleepTime the amount of time to sleep in milliseconds
+ * @throws UnableToCompleteException if the thread times out and
+ * fatalExceptions is true
+ */
+ private <T extends SeleniumThread> void waitForThreadsToComplete(
+ List<T> threads, boolean fatalExceptions, String action, int sleepTime)
+ throws UnableToCompleteException {
+ boolean allComplete;
+ long endTime = System.currentTimeMillis() + LAUNCH_TIMEOUT;
+ do {
+ try {
+ Thread.sleep(sleepTime);
+ } catch (InterruptedException e) {
+ // This should not happen.
+ throw new UnableToCompleteException();
+ }
+
+ allComplete = true;
+ synchronized (lock) {
+ for (SeleniumThread thread : threads) {
+ if (!thread.isComplete()) {
+ allComplete = false;
+ }
+ }
+ }
+
+ // Check if we have timed out.
+ if (!allComplete && endTime < System.currentTimeMillis()) {
+ allComplete = true;
+ String message = "The following Selenium instances did not " + action
+ + " within " + LAUNCH_TIMEOUT + "ms:\n";
+ synchronized (lock) {
+ message += getIncompleteSpecifierList(threads);
+ }
+ if (fatalExceptions) {
+ shell.getTopLogger().log(TreeLogger.ERROR, message);
+ throw new UnableToCompleteException();
+ } else {
+ shell.getTopLogger().log(TreeLogger.WARN, message);
+ }
+ }
+ } while (!allComplete);
+ }
}