All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.gwt.junit.RunStyleSelenium Maven / Gradle / Ivy

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

  /**
   * 

* 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. *

*

* We wait until {@link LaunchThread#isComplete()} returns true * before starting the keep alive thread or creating a {@link StopThread}, so * no other thread can be accessing {@link Selenium} at the same time. *

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

* The {@link Thread} used to stop a selenium instance. *

*

* 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. *

*/ 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 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 launchThreads = new ArrayList(); /** * 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 stopThreads = new ArrayList(); 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(); } 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 String getIncompleteSpecifierList( List 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 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 void waitForThreadsToComplete( List 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); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy