View Javadoc
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 }