001/** 002 * Copyright 2005-2014 The Kuali Foundation 003 * 004 * Licensed under the Educational Community License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.opensource.org/licenses/ecl2.php 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package org.kuali.rice.testtools.selenium; 017 018import com.saucelabs.common.SauceOnDemandAuthentication; 019import com.saucelabs.common.SauceOnDemandSessionIdProvider; 020import com.saucelabs.junit.SauceOnDemandTestWatcher; 021import com.saucelabs.saucerest.SauceREST; 022import org.junit.Assert; 023import org.openqa.selenium.Platform; 024import org.openqa.selenium.WebDriver; 025import org.openqa.selenium.ie.InternetExplorerDriver; 026import org.openqa.selenium.remote.DesiredCapabilities; 027import org.openqa.selenium.remote.RemoteWebDriver; 028 029import java.io.BufferedWriter; 030import java.io.File; 031import java.io.FileWriter; 032import java.io.IOException; 033import java.net.URL; 034import java.util.HashMap; 035import java.util.Map; 036 037/** 038 * <p> 039 * Helper class for {@link org.openqa.selenium.remote.RemoteWebDriver} when writing Selenium tests making use of Saucelabs 040 * or <a href="http://saucelabs.com/ondemand">Sauce OnDemand</a>. 041 * </p><p> 042 * Saucelabs properties need to be set as JVM arguments. See the SAUCE_ Constants defined below. Required <b>saucelabs</b> 043 * parameters are: {@see #REMOTE_DRIVER_SAUCELABS_PROPERTY} (master use saucelabs flag), {@see #SAUCE_USER_PROPERTY}, 044 * {@see #SAUCE_KEY_PROPERTY}, {@see #SAUCE_VERSION_PROPERTY}. 045 * </p><p> 046 * An example: 047 * <pre>{@code 048 * -Dremote.public.url=env14.rice.kuali.org -Dremote.driver.saucelabs -Dsaucelabs.user=YOUR-SAUCELABS-USER 049 * -Dsaucelabs.key=YOUR-SAUCELABS-KEY -Dsaucelabs.browser.version=22 -Dsaucelabs.platform=linux -Dsaucelabs.browser=ff 050 * -Dremote.public.user=admin -Drice.version=42222 051 * }</pre> 052 * </p><p> 053 * To make use of SauceLabsWebDriverHelper, call its {@see #setUp} with the Test Class and Test Name and retrieve the configured 054 * WebDriver using {@see #getDriver} You'll also need to call {@see #getSessionId} which you'll pass into the {@see #tearDown} 055 * (along with the tests passed state). 056 * </p><p> 057 * In your test setUp: 058 * </p><p> 059 * <pre> 060 * {@code 061 * SauceLabsWebDriverHelper saucelabs = new SauceLabsWebDriverHelper(); 062 * saucelabs.setUp(className, testName); 063 * driver = saucelabs.getDriver(); 064 * } 065 * </pre> 066 * </p><p> 067 * In your test tearDown: 068 * <pre> 069 * {@code 070 * SauceLabsWebDriverHelper.tearDown(passed, sessionId); 071 * } 072 * </pre> 073 * </p><p> 074 * Also includes the <a href="">Sauce JUnit</a> helper classes, which will use the Sauce REST API to mark the 075 * Sauce Job as passed/failed. 076 * </p><p> 077 * In order to use {@link SauceOnDemandTestWatcher} the {@link SauceOnDemandSessionIdProvider} interface is implemented. 078 * </p> 079 * @author Kuali Rice Team (rice.collab@kuali.org) 080 */ 081public class SauceLabsWebDriverHelper implements SauceOnDemandSessionIdProvider { 082 083 /** 084 * <p> 085 * Use Saucelabs flag, <b>required</b>. 086 * </p><p> 087 * For ease of disabling saucelabs without having to remove other saucelabs property settings, if not present saucelabs 088 * will not be used. 089 * </p><p> 090 * -Dremote.driver.saucelabs 091 * </p> 092 */ 093 public static final String REMOTE_DRIVER_SAUCELABS_PROPERTY = "remote.driver.saucelabs"; 094 095 /** 096 * <p> 097 * Saucelabs browser, default is Firefox. 098 * </p><p> 099 * See <a href="https://saucelabs.com/docs/platforms">Saucelabs Resources</a> 100 * <ul> 101 * <li>ff = Firefox</li> 102 * <li>ie = Internet Explorer</li> 103 * <li>chrome = Google Chrome</li> 104 * <li>opera = Opera</li> 105 * <li>android = Android</li> 106 * <li>safari = Safari</li> 107 * <li>ipad = IPad</li> 108 * <li>iphone = IPhone</li> 109 * </ul></p><p> 110 * -Dsaucelabs.browser= 111 * <p> 112 */ 113 public static final String SAUCE_BROWSER_PROPERTY = "saucelabs.browser"; 114 115 /** 116 * <p> 117 * Suacelabs build displayed as saucelabs build, default is unknown. 118 * </p><p> 119 * -Drice.version= 120 * </p> 121 */ 122 public static final String SAUCE_BUILD_PROPERTY = "rice.version"; 123 124 /** 125 * <p> 126 * Create a unix shell script to download saucelab resources, default is false. 127 * </p><p> 128 * Note - saucelabs history only goes back so far, if you run enough tests the resources will no longer 129 * be available for downloading. 130 * </p><p> 131 * -Dsaucelabs.download.script=false 132 * </p> 133 */ 134 public static final String SAUCE_DOWNLOAD_SCRIPT_PROPERTY = "saucelabs.download.scripts"; 135 136 /** 137 * <p> 138 * Saucelabs idle timeout in seconds, default is 180. 139 * </p><p> 140 * -Dsaucelabs.idle.timeout.seconds= 141 * </p> 142 */ 143 public static final String SAUCE_IDLE_TIMEOUT_SECONDS_PROPERTY = "saucelabs.idle.timeout.seconds"; 144 145 /** 146 * <p> 147 * Saucelabs key, <b>required</b>. 148 * </p><p> 149 * -Dsaucelabs.key= 150 * </p> 151 */ 152 public static final String SAUCE_KEY_PROPERTY = "saucelabs.key"; 153 154 /** 155 * <p> 156 * Saucelabs max duration in seconds, default is 480. 157 * </p><p> 158 * -Dsaucelabs.max.duration.seconds= 159 * </p> 160 */ 161 public static final String SAUCE_MAX_DURATION_SECONDS_PROPERTY = "saucelabs.max.duration.seconds"; 162 163 /** 164 * <p> 165 * Saucelabs platform (OS) replace spaces with underscores, default is Linux. 166 * </p><p> 167 * See <a href="https://saucelabs.com/docs/platforms">Saucelabs Resources</a> 168 * </p><p> 169 * -Dsaucelabs.platform= 170 * </p> 171 */ 172 public static final String SAUCE_PLATFORM_PROPERTY = "saucelabs.platform"; 173 174 /** 175 * <p> 176 * Saucelabs ignore security domains in IE, which can introduce flakiness, default is true. 177 * </p><p> 178 * See <a href="http://code.google.com/p/selenium/wiki/FrequentlyAskedQuestions#Q:_The_does_not_work_well_on_Vista._How_do_I_get_it_to_work_as_e">InternetExplorerDriver FAQ</a> 179 * </p><p> 180 * -Dsaucelabs.ie.ignore.domains=false 181 * </p> 182 */ 183 public static final String SAUCE_IE_INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS_PROPERTY = "saucelabs.ie.ignore.domains"; 184 185 /** 186 * <p> 187 * Saucelabs popup disable setting, default is false (not disabled). 188 * </p><p> 189 * See <a href="https://saucelabs.com/docs/additional-config#disable-popup-handler">DISABLE POPUP HANDLER</a> 190 * </p><p> 191 * -Dsaucelabs.pop.disable= 192 * </p> 193 */ 194 public static final String SAUCE_POPUP_PROPERTY = "saucelabs.pop.disable"; 195 196 /** 197 * <p> 198 * Saucelabs share setting, default is public restricted. 199 * </p><p> 200 * See <a href="https://saucelabs.com/docs/additional-config#sharing">Job Sharing</a>. Valid values are: public, public restricted, team, share, and private. 201 * </p><p> 202 * -Dsaucelabs.share= 203 * </p> 204 */ 205 public static final String SAUCE_SHARE_PROPERTY = "saucelabs.share"; 206 207 /** 208 * <p> 209 * Saucelabs user, <b>required</b>. 210 * </p><p> 211 * -Dsaucelabs.user= 212 * </p> 213 */ 214 public static final String SAUCE_USER_PROPERTY = "saucelabs.user"; 215 216 /** 217 * <p> 218 * Saucelabs browser Version, <b>required</b>. 219 * </p><p> 220 * See <a href="https://saucelabs.com/docs/platforms">Saucelabs Resources</a> 0 or null is current version of <b>Chrome</b>. 221 * If using a browser other than Chrome this must be set else an Exception will be thrown. 222 * </p><p> 223 * -Dsaucelabs.version= 224 * </p> 225 */ 226 public static final String SAUCE_VERSION_PROPERTY = "saucelabs.browser.version"; 227 228 /** 229 * <p> 230 * Saucelabs REST API delay in milliseconds, default is 5000. 231 * </p><p> 232 * -Dsaucelabs.rest.api.delay.ms= 233 * </p> 234 */ 235 public static final String SAUCE_REST_API_DELAY_MS = "saucelabs.rest.api.delay.ms"; 236 237 /** 238 * <p> 239 * Constructs a {@link SauceOnDemandAuthentication} instance using the supplied user name/access key. 240 * </p><p> 241 * To use the authentication supplied by environment variables or from an external file, use the no-arg 242 * {@link SauceOnDemandAuthentication} constructor. 243 * </p><p> 244 * {@see #SAUCE_USER_PROPERTY} {@see #SAUCE_KEY_PROPERTY} 245 * </p> 246 */ 247 public SauceOnDemandAuthentication authentication = new SauceOnDemandAuthentication(System.getProperty(SAUCE_USER_PROPERTY), System.getProperty(SAUCE_KEY_PROPERTY)); 248 249 private WebDriver driver; 250 251 private String sessionId; 252 253 /** 254 * <p> 255 * Saucelabs setUp. 256 * </p><p> 257 * Creates a {@link org.openqa.selenium.remote.RemoteWebDriver} instance with the DesiredCapabilities as configured 258 * using the JVM arguments described as SAUCE_ Constants in this class. After setUp the WebDriver can be accessed via 259 * {@see #getDriver}. You'll also need {@see #getSessionId} for when you call {@see #tearDown} 260 * </p> 261 * 262 * @param className class name of the test being setup as a String 263 * @param testName test name of the test being setup as a String 264 * @throws Exception 265 */ 266 public void setUp(String className, String testName) throws Exception { 267 if (System.getProperty(REMOTE_DRIVER_SAUCELABS_PROPERTY) == null) { // dup guard so WebDriverUtils doesn't have to be used. 268 return; 269 } 270 271 if (System.getProperty(SAUCE_USER_PROPERTY) == null || System.getProperty(SAUCE_KEY_PROPERTY) == null) { 272 Assert.fail("-D" + SAUCE_USER_PROPERTY + " and -D" + SAUCE_KEY_PROPERTY + " must be set to saucelabs user and access key."); 273 } 274 275 DesiredCapabilities capabilities = null; 276 if ("ff".equalsIgnoreCase(System.getProperty(SAUCE_BROWSER_PROPERTY))) { 277 capabilities = DesiredCapabilities.firefox(); 278 } else if ("ie".equalsIgnoreCase(System.getProperty(SAUCE_BROWSER_PROPERTY))) { 279 capabilities = DesiredCapabilities.internetExplorer(); 280 capabilities.setCapability(InternetExplorerDriver.INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS, 281 System.getProperty(SAUCE_IE_INTRODUCE_FLAKINESS_BY_IGNORING_SECURITY_DOMAINS_PROPERTY, "true")); 282 } else if ("chrome".equalsIgnoreCase(System.getProperty(SAUCE_BROWSER_PROPERTY))) { 283 capabilities = DesiredCapabilities.chrome(); 284 } else if ("opera".equalsIgnoreCase(System.getProperty(SAUCE_BROWSER_PROPERTY))) { 285 capabilities = DesiredCapabilities.opera(); 286 } else if ("android".equalsIgnoreCase(System.getProperty(SAUCE_BROWSER_PROPERTY))) { 287 capabilities = DesiredCapabilities.android(); 288 } else if ("safari".equalsIgnoreCase(System.getProperty(SAUCE_BROWSER_PROPERTY))) { 289 capabilities = DesiredCapabilities.safari(); 290 } else if ("ipad".equalsIgnoreCase(System.getProperty(SAUCE_BROWSER_PROPERTY))) { 291 capabilities = DesiredCapabilities.ipad(); 292 } else if ("iphone".equalsIgnoreCase(System.getProperty(SAUCE_BROWSER_PROPERTY))) { 293 capabilities = DesiredCapabilities.iphone(); 294 } else { 295 capabilities = DesiredCapabilities.firefox(); 296 } 297 298 String version = System.getProperty(SAUCE_VERSION_PROPERTY); 299 if (version == null || "0".equals(version)) { // Blank or 0 leaves version blank for use with chrome 300 301 if (!"chrome".equalsIgnoreCase(System.getProperty(SAUCE_BROWSER_PROPERTY))) { 302 throw new RuntimeException("Blank or 0 version for a browser not chrome " + System.getProperty(SAUCE_BROWSER_PROPERTY)); 303 } 304 305 capabilities.setCapability("version", ""); // saucelabs requires blank for chrome (latest version) 306 } else { 307 capabilities.setCapability("version", version); // saucelabs requires blank for chrome (latest version) 308 } 309 310 capabilities.setCapability("platform", System.getProperty(SAUCE_PLATFORM_PROPERTY, Platform.UNIX.toString()).replaceAll("_", " ")); 311 capabilities.setCapability("idle-timeout", Integer.parseInt(System.getProperty(SAUCE_IDLE_TIMEOUT_SECONDS_PROPERTY, "180"))); 312 capabilities.setCapability("max-duration", Integer.parseInt(System.getProperty(SAUCE_MAX_DURATION_SECONDS_PROPERTY, "480"))); 313 capabilities.setCapability("name", className + "." + testName + "-" + AutomatedFunctionalTestUtils.DTS); 314 capabilities.setCapability("disable-popup-handler", System.getProperty(SAUCE_POPUP_PROPERTY, "false")); 315 capabilities.setCapability("public", System.getProperty(SAUCE_SHARE_PROPERTY, "public restricted")); 316 317 System.out.println("Requesting Saucelabs RemoteWebDriver with DesiredCapabilities of " + capabilities.toString()); 318 319 this.driver = new RemoteWebDriver( 320 new URL("http://" + authentication.getUsername() + ":" + authentication.getAccessKey() + "@ondemand.saucelabs.com:80/wd/hub"), 321 capabilities); 322 this.sessionId = ((RemoteWebDriver)driver).getSessionId().toString(); 323 } 324 325 private void downloadResults(String className, String testName) { 326 if ("true".equals(System.getProperty(SAUCE_DOWNLOAD_SCRIPT_PROPERTY, "false"))) { 327 try { 328 String dir = determineSaveDir(className, testName); 329 String resources = "mkdir " + dir + " ; cd " + dir + " ; \n" 330 + curlSaveResourceString(className, testName, "selenium-server.log") + " ; \n" 331 + curlSaveResourceString(className, testName, "video.flv") + " ; \n" 332 // + wgetnSaveResourceString(className, testName) + " ; \n" 333 + "cd ../\n"; 334 System.out.println(resources); 335 writeFile("SauceLabsResources" + dir + ".sh", resources); 336 337// downloadResource(dir, "selenium-server.log"); 338// downloadResource(dir, "video.flv"); 339 } catch (Exception e) { 340 System.out.println("Exception while writing SauceLabsResources.sh " + e.getMessage()); 341 System.out.println(curlSaveResourceString(className, testName, "selenium-server.log")); 342 System.out.println(curlSaveResourceString(className, testName, "video.flv")); 343 // System.out.println(curlSaveResourceString(className, testName, "XXXXscreenshot.png (where XXXX is a number between 0000 and 9999)")); // TODO 344 } 345 } 346 } 347 348 /* the curl command works, this doesn't, what's wrong? 349 private void downloadResource(String dir, String resource) throws IOException { 350 File file = new File(dir + resource); 351 if(!file.exists()) { 352 file.createNewFile(); 353 } 354 FileOutputStream fileOutputStream = new FileOutputStream(file); 355 CloseableHttpClient httpclient = HttpClients.custom().build(); 356 try { 357 String userCredentials = "username:password"; 358 String basicAuth = "Basic " + new String(new Base64().encode(userCredentials.getBytes())); 359 HttpGet httpget = new HttpGet(resourceUrl(resource)); 360 httpget.setHeader("Authorization", basicAuth); 361 httpget.setHeader("Content-Type", "text/html"); 362 CloseableHttpResponse response = httpclient.execute(httpget); 363 try { 364 fileOutputStream.write(EntityUtils.toByteArray(response.getEntity())); 365 fileOutputStream.close(); 366 } finally { 367 response.close(); 368 } 369 } finally { 370 httpclient.close(); 371 } 372 } 373 */ 374 375 /** 376 * <p> 377 * Saucelabs tearDown, flag the tests as passed or failed. 378 * </p><p> 379 * Uses the SauceREST API to register a test as passed or failed. {@see #SAUCE_REST_API_DELAY_MS} 380 * </p> 381 * 382 * @param passed true if passed, falsed if failed, as a boolean 383 * @param sessionId saucelabs test session id, as a String 384 * @throws Exception 385 */ 386 public void tearDown(boolean passed, String sessionId, String className, String testName) throws Exception { 387 if (sessionId != null && System.getProperty(REMOTE_DRIVER_SAUCELABS_PROPERTY) != null) { // dup guard so WebDriverUtils doesn't have to be used 388 SauceREST client = new SauceREST(System.getProperty(SauceLabsWebDriverHelper.SAUCE_USER_PROPERTY), 389 System.getProperty(SauceLabsWebDriverHelper.SAUCE_KEY_PROPERTY)); 390 /* Using a map of udpates: 391 * (http://saucelabs.com/docs/sauce-ondemand#alternative-annotation-methods) 392 */ 393 Map<String, Object> updates = new HashMap<String, Object>(); 394 updates.put("passed", passed); 395 updates.put("build", System.getProperty(SAUCE_BUILD_PROPERTY, "unknown")); 396 client.updateJobInfo(sessionId, updates); 397 398 if (passed) { 399 client.jobPassed(sessionId); 400 } else { 401 client.jobFailed(sessionId); 402 } 403 404 // give the client message a chance to get processed on saucelabs side 405 Thread.sleep(Integer.parseInt(System.getProperty(SAUCE_REST_API_DELAY_MS, "5000"))); 406 407 downloadResults(className, testName); 408 } 409 } 410 411 private String curlSaveResourceString(String className, String testName, String resource) { 412 return "curl -o " + deriveResourceBaseNames(className, testName, resource) + " -u " + authentication 413 .getUsername() + ":" + authentication.getAccessKey() + " " + resourceUrl(resource); 414 } 415 416 private String resourceUrl(String resource) { 417 return "http://saucelabs.com/rest/" + authentication.getUsername() + "/jobs/" + sessionId + "/results/" + resource; 418 } 419 420 private String deriveResourceBaseNames(String className, String testName, String resource) { 421 return className + "." + testName + "-" 422 + System.getProperty(SAUCE_PLATFORM_PROPERTY, Platform.UNIX.toString()) + "-" 423 + System.getProperty(SAUCE_BROWSER_PROPERTY) + "-" 424 + System.getProperty(SAUCE_VERSION_PROPERTY) + "-" 425 + System.getProperty(WebDriverUtils.REMOTE_PUBLIC_USER_PROPERTY, "admin") + "-" 426 + System.getProperty(SAUCE_BUILD_PROPERTY, "unknown_build") + "-" 427 + AutomatedFunctionalTestUtils.DTS + "-" 428 + resource; 429 } 430 431 /** 432 * <p> 433 * Returns the (RemoteWebDriver) driver. 434 * </p> 435 * 436 * @return WebDriver 437 */ 438 public WebDriver getDriver() { 439 return driver; 440 } 441 442 @Override 443 public String getSessionId() { 444 return sessionId; 445 } 446 447 // Seems like sceenshot downloading has changed, this doesn't work anymore 448 private String wgetnSaveResourceString(String className, String testName) { 449 String dir = determineSaveDir(className, testName); 450 // http://www.jwz.org/hacks/wgetn 451 return "wgetn https://saucelabs.com/" + sessionId + "/%04dscreenshot.png 0 50"; 452 } 453 454 private String determineSaveDir(String className, String testName) { 455 String dir = deriveResourceBaseNames(className, testName, ""); 456 dir = dir.substring(0, dir.length() -1); 457 return dir; 458 } 459 460 private void writeFile(String fileName, String content) throws IOException { 461 File file = new File(fileName); 462 463 if (!file.exists()) { 464 file.createNewFile(); 465 } 466 467 FileWriter fw = new FileWriter(file.getAbsoluteFile()); 468 BufferedWriter bw = new BufferedWriter(fw); 469 bw.write(content); 470 bw.flush(); 471 bw.close(); 472 } 473}