| /* |
| * 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.thoughtworks.selenium.DefaultSelenium; |
| import com.thoughtworks.selenium.Selenium; |
| import com.thoughtworks.selenium.SeleniumException; |
| |
| import java.util.HashSet; |
| 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 { |
| /** |
| * 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)); |
| } |
| } |
| |
| /** |
| * The list of hosts that were interrupted. |
| */ |
| private Set<String> interruptedHosts; |
| |
| private SeleniumWrapper remotes[]; |
| |
| /** |
| * A separate lock to control access to {@link #interruptedHosts}. This keeps |
| * the main thread calls into {@link #getInterruptedHosts()} 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) { |
| super(shell); |
| } |
| |
| @Override |
| public String[] getInterruptedHosts() { |
| synchronized (wasInterruptedLock) { |
| 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; |
| } |
| } |
| |
| 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 (SeleniumWrapper remote : remotes) { |
| if (remote.getSelenium() != null) { |
| try { |
| remote.getSelenium().stop(); |
| } catch (SeleniumException se) { |
| shell.getTopLogger().log(TreeLogger.WARN, |
| "Error stopping selenium session", se); |
| } |
| } |
| } |
| } |
| }); |
| start(); |
| return targets.length; |
| } |
| |
| @Override |
| public synchronized void launchModule(String moduleName) { |
| // Get the localhost address. |
| String domain = "http://" + getLocalHostName() + ":" + shell.getPort() |
| + "/"; |
| |
| // Startup all the selenia and point them at the module url. |
| 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); |
| } |
| } |
| } |
| |
| /** |
| * 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) { |
| } |
| } while (doKeepAlives()); |
| } |
| }; |
| keepAliveThread.setDaemon(true); |
| keepAliveThread.start(); |
| } |
| |
| private synchronized boolean doKeepAlives() { |
| if (remotes != null) { |
| 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) { |
| synchronized (wasInterruptedLock) { |
| if (interruptedHosts == null) { |
| interruptedHosts = new HashSet<String>(); |
| } |
| interruptedHosts.add(remote.getSpecifier()); |
| } |
| } |
| } |
| } |
| |
| return interruptedHosts == null; |
| } |
| } |