1 /**
2 * Copyright 2005-2015 The Kuali Foundation
3 *
4 * Licensed under the Educational Community License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.opensource.org/licenses/ecl2.php
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package org.kuali.rice.testtools.selenium;
17
18 import com.saucelabs.common.SauceOnDemandAuthentication;
19 import com.saucelabs.common.SauceOnDemandSessionIdProvider;
20 import com.saucelabs.junit.SauceOnDemandTestWatcher;
21 import com.saucelabs.saucerest.SauceREST;
22 import org.junit.Assert;
23 import org.openqa.selenium.Platform;
24 import org.openqa.selenium.WebDriver;
25 import org.openqa.selenium.ie.InternetExplorerDriver;
26 import org.openqa.selenium.remote.DesiredCapabilities;
27 import org.openqa.selenium.remote.RemoteWebDriver;
28
29 import java.io.BufferedWriter;
30 import java.io.File;
31 import java.io.FileWriter;
32 import java.io.IOException;
33 import java.net.URL;
34 import java.util.HashMap;
35 import java.util.Map;
36
37 /**
38 * <p>
39 * Helper class for {@link org.openqa.selenium.remote.RemoteWebDriver} when writing Selenium tests making use of Saucelabs
40 * or <a href="http://saucelabs.com/ondemand">Sauce OnDemand</a>.
41 * </p><p>
42 * Saucelabs properties need to be set as JVM arguments. See the SAUCE_ Constants defined below. Required <b>saucelabs</b>
43 * parameters are: {@see #REMOTE_DRIVER_SAUCELABS_PROPERTY} (master use saucelabs flag), {@see #SAUCE_USER_PROPERTY},
44 * {@see #SAUCE_KEY_PROPERTY}, {@see #SAUCE_VERSION_PROPERTY}.
45 * </p><p>
46 * An example:
47 * <pre>{@code
48 * -Dremote.public.url=env14.rice.kuali.org -Dremote.driver.saucelabs -Dsaucelabs.user=YOUR-SAUCELABS-USER
49 * -Dsaucelabs.key=YOUR-SAUCELABS-KEY -Dsaucelabs.browser.version=31 -Dsaucelabs.platform=Linux -Dsaucelabs.browser=ff
50 * -Dremote.public.user=admin -Drice.version=48058
51 * }</pre>
52 * </p><p>
53 * To make use of SauceLabsWebDriverHelper, call its {@see #setUp} with the Test Class and Test Name and retrieve the configured
54 * WebDriver using {@see #getDriver} You'll also need to call {@see #getSessionId} which you'll pass into the {@see #tearDown}
55 * (along with the tests passed state).
56 * </p><p>
57 * In your test setUp:
58 * </p><p>
59 * <pre>
60 * {@code
61 * SauceLabsWebDriverHelper saucelabs = new SauceLabsWebDriverHelper();
62 * saucelabs.setUp(className, testName);
63 * driver = saucelabs.getDriver();
64 * }
65 * </pre>
66 * </p><p>
67 * In your test tearDown:
68 * <pre>
69 * {@code
70 * SauceLabsWebDriverHelper.tearDown(passed, sessionId);
71 * }
72 * </pre>
73 * </p><p>
74 * Also includes the <a href="">Sauce JUnit</a> helper classes, which will use the Sauce REST API to mark the
75 * Sauce Job as passed/failed.
76 * </p><p>
77 * In order to use {@link SauceOnDemandTestWatcher} the {@link SauceOnDemandSessionIdProvider} interface is implemented.
78 * </p>
79 * @author Kuali Rice Team (rice.collab@kuali.org)
80 */
81 public class SauceLabsWebDriverHelper implements SauceOnDemandSessionIdProvider {
82
83 /**
84 * <p>
85 * Use Saucelabs flag, <b>required</b>.
86 * </p><p>
87 * For ease of disabling saucelabs without having to remove other saucelabs property settings, if not present saucelabs
88 * will not be used.
89 * </p><p>
90 * -Dremote.driver.saucelabs
91 * </p>
92 */
93 public static final String REMOTE_DRIVER_SAUCELABS_PROPERTY = "remote.driver.saucelabs";
94
95 /**
96 * <p>
97 * Saucelabs browser, default is Firefox.
98 * </p><p>
99 * 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, "600")));
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 System.out.println("SauceLabs job can be viewed at https://saucelabs.com/jobs/" + this.sessionId);
325 }
326
327 private void downloadResults(String className, String testName) {
328 if ("true".equals(System.getProperty(SAUCE_DOWNLOAD_SCRIPT_PROPERTY, "false"))) {
329 try {
330 String dir = determineSaveDir(className, testName);
331 String resources = "mkdir " + dir + " ; cd " + dir + " ; \n"
332 + curlSaveResourceString(className, testName, "selenium-server.log") + " ; \n"
333 + curlSaveResourceString(className, testName, "video.flv") + " ; \n"
334 // + wgetnSaveResourceString(className, testName) + " ; \n"
335 + "cd ../\n";
336 System.out.println(resources);
337 writeFile("SauceLabsResources" + dir + ".sh", resources);
338
339 // downloadResource(dir, "selenium-server.log");
340 // downloadResource(dir, "video.flv");
341 } catch (Exception e) {
342 System.out.println("Exception while writing SauceLabsResources.sh " + e.getMessage());
343 System.out.println(curlSaveResourceString(className, testName, "selenium-server.log"));
344 System.out.println(curlSaveResourceString(className, testName, "video.flv"));
345 // System.out.println(curlSaveResourceString(className, testName, "XXXXscreenshot.png (where XXXX is a number between 0000 and 9999)")); // TODO
346 }
347 }
348 }
349
350 /* the curl command works, this doesn't, what's wrong?
351 private void downloadResource(String dir, String resource) throws IOException {
352 File file = new File(dir + resource);
353 if(!file.exists()) {
354 file.createNewFile();
355 }
356 FileOutputStream fileOutputStream = new FileOutputStream(file);
357 CloseableHttpClient httpclient = HttpClients.custom().build();
358 try {
359 String userCredentials = "username:password";
360 String basicAuth = "Basic " + new String(new Base64().encode(userCredentials.getBytes()));
361 HttpGet httpget = new HttpGet(resourceUrl(resource));
362 httpget.setHeader("Authorization", basicAuth);
363 httpget.setHeader("Content-Type", "text/html");
364 CloseableHttpResponse response = httpclient.execute(httpget);
365 try {
366 fileOutputStream.write(EntityUtils.toByteArray(response.getEntity()));
367 fileOutputStream.close();
368 } finally {
369 response.close();
370 }
371 } finally {
372 httpclient.close();
373 }
374 }
375 */
376
377 /**
378 * <p>
379 * Saucelabs tearDown, flag the tests as passed or failed.
380 * </p><p>
381 * Uses the SauceREST API to register a test as passed or failed. {@see #SAUCE_REST_API_DELAY_MS}
382 * </p>
383 *
384 * @param passed true if passed, falsed if failed, as a boolean
385 * @param sessionId saucelabs test session id, as a String
386 * @throws Exception
387 */
388 public void tearDown(boolean passed, String sessionId, String className, String testName) throws Exception {
389 if (sessionId != null && System.getProperty(REMOTE_DRIVER_SAUCELABS_PROPERTY) != null) { // dup guard so WebDriverUtils doesn't have to be used
390 SauceREST client = new SauceREST(System.getProperty(SauceLabsWebDriverHelper.SAUCE_USER_PROPERTY),
391 System.getProperty(SauceLabsWebDriverHelper.SAUCE_KEY_PROPERTY));
392 /* Using a map of udpates:
393 * (http://saucelabs.com/docs/sauce-ondemand#alternative-annotation-methods)
394 */
395 Map<String, Object> updates = new HashMap<String, Object>();
396 updates.put("passed", passed);
397 updates.put("build", System.getProperty(SAUCE_BUILD_PROPERTY, "unknown"));
398 client.updateJobInfo(sessionId, updates);
399
400 if (passed) {
401 client.jobPassed(sessionId);
402 } else {
403 client.jobFailed(sessionId);
404 }
405
406 // give the client message a chance to get processed on saucelabs side
407 Thread.sleep(Integer.parseInt(System.getProperty(SAUCE_REST_API_DELAY_MS, "5000")));
408
409 downloadResults(className, testName);
410 }
411 }
412
413 private String curlSaveResourceString(String className, String testName, String resource) {
414 return "curl -o " + deriveResourceBaseNames(className, testName, resource) + " -u " + authentication
415 .getUsername() + ":" + authentication.getAccessKey() + " " + resourceUrl(resource);
416 }
417
418 private String resourceUrl(String resource) {
419 return "http://saucelabs.com/rest/" + authentication.getUsername() + "/jobs/" + sessionId + "/results/" + resource;
420 }
421
422 private String deriveResourceBaseNames(String className, String testName, String resource) {
423 return className + "." + testName + "-"
424 + System.getProperty(SAUCE_PLATFORM_PROPERTY, Platform.UNIX.toString()) + "-"
425 + System.getProperty(SAUCE_BROWSER_PROPERTY) + "-"
426 + System.getProperty(SAUCE_VERSION_PROPERTY) + "-"
427 + System.getProperty(WebDriverUtils.REMOTE_PUBLIC_USER_PROPERTY, "admin") + "-"
428 + System.getProperty(SAUCE_BUILD_PROPERTY, "unknown_build") + "-"
429 + AutomatedFunctionalTestUtils.DTS + "-"
430 + resource;
431 }
432
433 /**
434 * <p>
435 * Returns the (RemoteWebDriver) driver.
436 * </p>
437 *
438 * @return WebDriver
439 */
440 public WebDriver getDriver() {
441 return driver;
442 }
443
444 @Override
445 public String getSessionId() {
446 return sessionId;
447 }
448
449 // Seems like sceenshot downloading has changed, this doesn't work anymore
450 private String wgetnSaveResourceString(String className, String testName) {
451 String dir = determineSaveDir(className, testName);
452 // http://www.jwz.org/hacks/wgetn
453 return "wgetn https://saucelabs.com/" + sessionId + "/%04dscreenshot.png 0 50";
454 }
455
456 private String determineSaveDir(String className, String testName) {
457 String dir = deriveResourceBaseNames(className, testName, "");
458 dir = dir.substring(0, dir.length() -1);
459 return dir;
460 }
461
462 private void writeFile(String fileName, String content) throws IOException {
463 File file = new File(fileName);
464
465 if (!file.exists()) {
466 file.createNewFile();
467 }
468
469 FileWriter fw = new FileWriter(file.getAbsoluteFile());
470 BufferedWriter bw = new BufferedWriter(fw);
471 bw.write(content);
472 bw.flush();
473 bw.close();
474 }
475 }