| /* |
| * 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 java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.Set; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * 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. |
| */ |
| protected static interface SeleniumWrapper { |
| void createSelenium(String domain); |
| |
| Selenium getSelenium(); |
| |
| String getSpecifier(); |
| } |
| |
| /** |
| * Implements SeleniumWrapper using DefaultSelenium. Visible for testing. |
| */ |
| static class RCSelenium implements SeleniumWrapper { |
| |
| private static final Pattern PATTERN = Pattern.compile("([\\w\\.-]+):([\\d]+)/(.+)"); |
| |
| /* |
| * Visible for testing. |
| */ |
| String browser; |
| String host; |
| int port; |
| |
| private Selenium selenium; |
| private final String specifier; |
| |
| public RCSelenium(String specifier) { |
| this.specifier = specifier; |
| parseSpecifier(); |
| } |
| |
| public void createSelenium(String domain) { |
| this.selenium = new DefaultSelenium(host, port, browser, domain); |
| } |
| |
| public Selenium getSelenium() { |
| return selenium; |
| } |
| |
| public String getSpecifier() { |
| return specifier; |
| } |
| |
| private void parseSpecifier() { |
| Matcher matcher = PATTERN.matcher(specifier); |
| if (!matcher.matches()) { |
| throw new IllegalArgumentException("Unable to parse Selenium target " |
| + specifier + " (expected format is [host]:[port]/[browser])"); |
| } |
| this.browser = matcher.group(3); |
| this.host = matcher.group(1); |
| this.port = Integer.parseInt(matcher.group(2)); |
| } |
| } |
| |
| /** |
| * 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. |
| if (shell.getTopLogger().isLoggable(TreeLogger.TRACE)) { |
| 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; |
| |
| /** |
| * 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; |
| |
| private SeleniumWrapper remotes[]; |
| |
| /** |
| * A separate lock to control access to {@link Selenium}, {@link #stopped}, |
| * {@link #remotes}, and {@link #interruptedHosts}. This ensures that the |
| * keepAlive thread doesn't call getTitle after the shutdown thread calls |
| * {@link Selenium#stop()}. |
| */ |
| private final Object lock = new Object(); |
| |
| public RunStyleSelenium(final JUnitShell shell) { |
| super(shell); |
| } |
| |
| @Override |
| public String[] getInterruptedHosts() { |
| synchronized (lock) { |
| if (interruptedHosts == null) { |
| return null; |
| } |
| return interruptedHosts.toArray(new String[interruptedHosts.size()]); |
| } |
| } |
| |
| @Override |
| public int initialize(String args) { |
| if (args == null || args.length() == 0) { |
| getLogger().log(TreeLogger.ERROR, |
| "Selenium runstyle requires comma-separated Selenium-RC targets"); |
| return -1; |
| } |
| String[] targetsIn = args.split(","); |
| SeleniumWrapper targets[] = new SeleniumWrapper[targetsIn.length]; |
| |
| for (int i = 0; i < targets.length; ++i) { |
| try { |
| targets[i] = createSeleniumWrapper(targetsIn[i]); |
| } catch (IllegalArgumentException e) { |
| getLogger().log(TreeLogger.ERROR, e.getMessage()); |
| return -1; |
| } |
| } |
| |
| // We don't need a lock at this point because we haven't started the keep- |
| // alive thread. |
| this.remotes = targets; |
| |
| // Install a shutdown hook that will close all of our outstanding Selenium |
| // sessions. The hook is only executed if the JVM is exited normally. If the |
| // process is terminated, the shutdown hook will not run, which leaves |
| // browser instances open on the Selenium server. We'll need to modify |
| // Selenium Server to do its own cleanup after a timeout. |
| Runtime.getRuntime().addShutdownHook(new Thread() { |
| @Override |
| public void run() { |
| List<StopThread> stopThreads = new ArrayList<StopThread>(); |
| synchronized (lock) { |
| stopped = true; |
| 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. |
| } |
| } |
| }); |
| return targets.length; |
| } |
| |
| @Override |
| 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 (LaunchThread thread : launchThreads) { |
| if (thread.getException() != null) { |
| // The thread has already logged the exception. |
| throw new UnableToCompleteException(); |
| } |
| } |
| } |
| |
| // Start the keep alive thread. |
| start(); |
| } |
| |
| /** |
| * Factory method for {@link SeleniumWrapper}. |
| * |
| * @param seleniumSpecifier Specifies the Selenium instance to create |
| * @return an instance of {@link SeleniumWrapper} |
| */ |
| protected SeleniumWrapper createSeleniumWrapper(String seleniumSpecifier) { |
| return new RCSelenium(seleniumSpecifier); |
| } |
| |
| /** |
| * Create the keep-alive thread. |
| */ |
| protected void start() { |
| // 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) { |
| break; |
| } |
| } while (doKeepAlives()); |
| } |
| }; |
| keepAliveThread.setDaemon(true); |
| keepAliveThread.start(); |
| } |
| |
| private boolean doKeepAlives() { |
| synchronized (lock) { |
| if (remotes != null) { |
| // If the shutdown thread has already executed, then we can stop this |
| // thread. |
| if (stopped) { |
| return false; |
| } |
| |
| for (SeleniumWrapper 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) { |
| // 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(Locale.ROOT).contains("permission denied")) { |
| if (interruptedHosts == null) { |
| interruptedHosts = new HashSet<String>(); |
| } |
| interruptedHosts.add(remote.getSpecifier()); |
| } |
| } |
| } |
| } |
| 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); |
| } |
| } |