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}