/* * The MIT License * * Copyright (c) 2010, InfraDNA, Inc. * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package com.saucelabs.sauce_ondemand.driver; import com.saucelabs.selenium.client.factory.SeleniumFactory; import com.saucelabs.selenium.client.factory.spi.SeleniumFactorySPI; import com.thoughtworks.selenium.Selenium; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import org.kohsuke.MetaInfServices; import org.openqa.selenium.WebDriver; import org.openqa.selenium.firefox.FirefoxProfile; import org.openqa.selenium.remote.CapabilityType; import org.openqa.selenium.remote.DesiredCapabilities; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import java.text.MessageFormat; import java.util.*; import java.util.Map.Entry; import java.util.concurrent.*; /** * {@link SeleniumFactorySPI} that talks to Sauce OnDemand. * * @author Kohsuke Kawaguchi */ @MetaInfServices public class SauceOnDemandSPIImpl extends SeleniumFactorySPI { private static final String DEFAULT_WEBDRIVER_HOST = "ondemand.saucelabs.com"; private static final String DEFAULT_WEBDRIVER_PORT = "80"; private static final String DEFAULT_SELENIUM_HOST = "saucelabs.com"; private static final int DEFAULT_SELENIUM_PORT = 4444; public static final String SELENIUM_HOST = "SELENIUM_HOST"; public static final String SELENIUM_PORT = "SELENIUM_PORT"; public static final String OS = "os"; public static final String BROWSER = "browser"; public static final String BROWSER_VERSION = "browser-version"; public static final String USERNAME = "username"; public static final String ACCESS_KEY = "access-key"; private static final String[] NON_PROFILE_PARAMETERS = new String[]{ACCESS_KEY, BROWSER, BROWSER_VERSION, OS, USERNAME}; @Override public Selenium createSelenium(SeleniumFactory factory, String browserURL) { String uri = factory.getUri(); if (!canHandle(uri)) return null; // not ours uri = uri.substring(SCHEME.length()); if (!uri.startsWith("?")) throw new IllegalArgumentException("Missing '?':" + factory.getUri()); Map<String, List<String>> paramMap = populateParameterMap(uri); String host = readPropertyOrEnv("SELENIUM_HOST", DEFAULT_SELENIUM_HOST); String portAsString = readPropertyOrEnv("SELENIUM_PORT", null); int port; if (portAsString == null || portAsString.equals("")) { port = DEFAULT_SELENIUM_PORT; } else { port = Integer.parseInt(portAsString); } return new SeleniumImpl( host, port, toJSON(paramMap), browserURL, new Credential(paramMap.get(USERNAME).get(0), paramMap.get(ACCESS_KEY).get(0)), paramMap.get("job-name").get(0)); } private Map<String, List<String>> populateParameterMap(String uri) { // massage parameter into JSON format Map<String, List<String>> paramMap = new HashMap<String, List<String>>(); for (String param : uri.substring(1).split("&")) { int idx = param.indexOf('='); if (idx < 0) throw new IllegalArgumentException("Invalid parameter format: " + uri); String key = param.substring(0, idx); String value = param.substring(idx + 1); List<String> v = paramMap.get(key); if (v == null) paramMap.put(key, v = new ArrayList<String>()); v.add(value); } if (paramMap.get(USERNAME) == null && paramMap.get(ACCESS_KEY) == null) { try { // read the credential from a credential file Credential cred = new Credential(); paramMap.put(USERNAME, Collections.singletonList(cred.getUsername())); paramMap.put(ACCESS_KEY, Collections.singletonList(cred.getKey())); } catch (IOException e) { throw new IllegalArgumentException("Failed to read " + Credential.getDefaultCredentialFile(), e); } } if (paramMap.get("job-name") == null) paramMap.put("job-name", Collections.singletonList(getJobName())); return paramMap; } @Override public WebDriver createWebDriver(SeleniumFactory factory, String browserURL, DesiredCapabilities capabilities) { String uri = factory.getUri(); if (!uri.startsWith(SCHEME)) return null; // not ours uri = uri.substring(SCHEME.length()); if (!uri.startsWith("?")) throw new IllegalArgumentException("Missing '?':" + factory.getUri()); return createWebDriver(browserURL, capabilities, uri); } private WebDriver createWebDriver(String browserURL, DesiredCapabilities capabilities, String uri) { // massage parameter into JSON format Map<String, List<String>> paramMap = populateParameterMap(uri); DesiredCapabilities desiredCapabilities; if (hasParameter(paramMap, OS) && hasParameter(paramMap, BROWSER) && hasParameter(paramMap, BROWSER_VERSION)) { String browser = getFirstParameter(paramMap, BROWSER); desiredCapabilities = new DesiredCapabilities(capabilities); desiredCapabilities.setBrowserName(browser); desiredCapabilities.setVersion(getFirstParameter(paramMap, BROWSER_VERSION)); desiredCapabilities.setCapability(CapabilityType.PLATFORM, getFirstParameter(paramMap, OS)); if (browser.equals("firefox")) { setFirefoxProfile(paramMap, desiredCapabilities); } populateDesiredCapabilities(paramMap, desiredCapabilities); } else { //use Firefox as a default desiredCapabilities = capabilities; desiredCapabilities.merge(DesiredCapabilities.firefox()); setFirefoxProfile(paramMap, desiredCapabilities); } String host = readPropertyOrEnv(SELENIUM_HOST, DEFAULT_WEBDRIVER_HOST); String portAsString = readPropertyOrEnv(SELENIUM_PORT, null); if (portAsString == null || portAsString.equals("")) { portAsString = DEFAULT_WEBDRIVER_PORT; } try { WebDriver driver = new RemoteWebDriverImpl( new URL( MessageFormat.format( "http://{2}:{3}@{0}:{1}/wd/hub", host, portAsString, getFirstParameter(paramMap, USERNAME), getFirstParameter(paramMap, ACCESS_KEY))), desiredCapabilities, new Credential(getFirstParameter(paramMap, USERNAME), getFirstParameter(paramMap, ACCESS_KEY)), getFirstParameter(paramMap, "job-name")); if (browserURL != null) { driver.get(browserURL); } return driver; } catch (MalformedURLException e) { throw new IllegalArgumentException("Invalid URL: " + uri, e); } } private void populateDesiredCapabilities(Map<String, List<String>> paramMap, DesiredCapabilities desiredCapabilities) { for (Entry<String, List<String>> entry : paramMap.entrySet()) { desiredCapabilities.setCapability(entry.getKey(), entry.getValue().get(0)); } } private void setFirefoxProfile(Map<String, List<String>> paramMap, DesiredCapabilities desiredCapabilities) { FirefoxProfile profile = new FirefoxProfile(); populateProfilePreferences(profile, paramMap); desiredCapabilities.setCapability("firefox_profile", profile); } private void populateProfilePreferences(FirefoxProfile profile, Map<String, List<String>> paramMap) { for (Map.Entry<String, List<String>> mapEntry : paramMap.entrySet()) { String key = mapEntry.getKey(); if (Arrays.binarySearch(NON_PROFILE_PARAMETERS, key) == -1) { //add it to the profile profile.setPreference(key, getFirstParameter(paramMap, key)); } } } @Override public boolean canHandle(String uri) { return uri.startsWith(SCHEME); } /** * Try to find the name of the test as best we can. */ public String getJobName() { // look for the caller of SeleniumFactory StackTraceElement[] trace = Thread.currentThread().getStackTrace(); boolean foundFactory = false; String callerName = null; for (StackTraceElement e : trace) { if (foundFactory) callerName = e.getClassName() + "." + e.getMethodName(); foundFactory = e.getClassName().equals(SeleniumFactory.class.getName()); } return callerName; } /** * Converts a multi-map to a JSON format. */ private String toJSON(Map<String, List<String>> paramMap) { boolean first = true; StringBuilder buf = new StringBuilder("{"); for (Entry<String, List<String>> e : paramMap.entrySet()) { if (first) first = false; else buf.append(','); buf.append('"').append(e.getKey()).append("\":"); List<String> v = e.getValue(); if (v.size() == 1) { buf.append('"').append(v.get(0)).append('"'); } else { buf.append('['); for (int i = 0; i < v.size(); i++) { if (i != 0) buf.append(','); buf.append('"').append(v.get(i)).append('"'); } buf.append(']'); } } buf.append('}'); return buf.toString(); } private String getFirstParameter(Map<String, List<String>> paramMap, String parameterName) { List<String> values = paramMap.get(parameterName); return values.get(0); } private boolean hasParameter(Map<String, List<String>> paramMap, String parameterName) { List<String> values = paramMap.get(parameterName); return values != null && !values.isEmpty(); } private static final String SCHEME = "sauce-ondemand:"; private static String readPropertyOrEnv(String key, String defaultValue) { String v = System.getProperty(key); if (v == null) v = System.getenv(key); if (v == null) v = defaultValue; return v; } /** * Creates a list of WebDriver instances based on the contents of a SAUCE_ONDEMAND_BROWSERS environment variable (typically set * by the Sauce Jenkins plugin). * * @param seleniumFactory * @param browserURL * @return */ @Override public List<WebDriver> createWebDrivers(SeleniumFactory seleniumFactory, final String browserURL) { List<WebDriver> webDrivers = new ArrayList<WebDriver>(); String browserJson = readPropertyOrEnv("SAUCE_ONDEMAND_BROWSERS", null); if (browserJson == null) { throw new IllegalArgumentException("Unable to find SAUCE_ONDEMAND_BROWSERS environment variable"); } //parse JSON and extract the browser urls, so that we know how many threads to schedule List<String> browsers = new ArrayList<String>(); try { JSONArray array = new JSONArray(new JSONTokener(browserJson)); for (int i = 0; i < array.length(); i++) { JSONObject object = array.getJSONObject(i); String uri = object.getString("url"); if (!uri.startsWith(SCHEME)) return null; // not ours uri = uri.substring(SCHEME.length()); if (!uri.startsWith("?")) throw new IllegalArgumentException("Missing '?':" + uri); browsers.add(uri); } } catch (JSONException e) { throw new IllegalArgumentException("Error parsing JSON", e); } //create a fixed thread pool for the number of browser ExecutorService service = Executors.newFixedThreadPool(browsers.size()); List<Callable<WebDriver>> callables = new ArrayList<Callable<WebDriver>>(); for (final String browser : browsers) { callables.add(new Callable<WebDriver>() { public WebDriver call() throws Exception { return createWebDriver(browserURL, null, browser); } }); } //invoke all the callables, and wait for each thread to return try { List<Future<WebDriver>> futures = service.invokeAll(callables); for (Future<WebDriver> future : futures) { webDrivers.add(future.get()); } } catch (InterruptedException e) { throw new IllegalArgumentException("Error retrieving webdriver", e); } catch (ExecutionException e) { throw new IllegalArgumentException("Error retrieving webdriver", e); } service.shutdown(); return Collections.unmodifiableList(webDrivers); } }