| /* | |
| * 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.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. | |
| 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().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); | |
| } | |
| } |