diff --git a/pom.xml b/pom.xml index 9548b28..31c1112 100644 --- a/pom.xml +++ b/pom.xml @@ -110,6 +110,12 @@ commons-exec 1.3 + + junit + junit + 4.12 + test + diff --git a/src/main/java/com/github/markusbernhardt/selenium2library/keywords/BrowserManagement.java b/src/main/java/com/github/markusbernhardt/selenium2library/keywords/BrowserManagement.java index 26537b8..f0dfc41 100644 --- a/src/main/java/com/github/markusbernhardt/selenium2library/keywords/BrowserManagement.java +++ b/src/main/java/com/github/markusbernhardt/selenium2library/keywords/BrowserManagement.java @@ -1,21 +1,18 @@ package com.github.markusbernhardt.selenium2library.keywords; +import com.github.markusbernhardt.selenium2library.RunOnFailureKeywordsAdapter; +import com.github.markusbernhardt.selenium2library.Selenium2LibraryFatalException; +import com.github.markusbernhardt.selenium2library.Selenium2LibraryNonFatalException; +import com.github.markusbernhardt.selenium2library.locators.ElementFinder; +import com.github.markusbernhardt.selenium2library.locators.WindowManager; +import com.github.markusbernhardt.selenium2library.utils.CustomHttpClientFactory; +import com.github.markusbernhardt.selenium2library.utils.Robotframework; +import com.github.markusbernhardt.selenium2library.utils.TimeUtils; +import com.github.markusbernhardt.selenium2library.utils.WebDriverCache; +import com.github.markusbernhardt.selenium2library.utils.WebDriverCache.SessionIdAliasWebDriverTuple; +import com.opera.core.systems.OperaDriver; import io.appium.java_client.ios.IOSDriver; import io.selendroid.client.SelendroidDriver; - -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Field; -import java.net.InetAddress; -import java.net.MalformedURLException; -import java.net.URL; -import java.net.UnknownHostException; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Map.Entry; -import java.util.concurrent.TimeUnit; - import org.apache.http.HttpHost; import org.apache.http.auth.AuthScope; import org.apache.http.auth.NTCredentials; @@ -36,9 +33,11 @@ import org.openqa.selenium.ie.InternetExplorerDriver; import org.openqa.selenium.phantomjs.PhantomJSDriver; import org.openqa.selenium.remote.Augmenter; +import org.openqa.selenium.remote.CommandInfo; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.HttpCommandExecutor; import org.openqa.selenium.remote.RemoteWebDriver; +import org.openqa.selenium.remote.http.HttpClient; import org.openqa.selenium.safari.SafariDriver; import org.robotframework.javalib.annotation.ArgumentNames; import org.robotframework.javalib.annotation.Autowired; @@ -46,15 +45,19 @@ import org.robotframework.javalib.annotation.RobotKeywordOverload; import org.robotframework.javalib.annotation.RobotKeywords; -import com.github.markusbernhardt.selenium2library.RunOnFailureKeywordsAdapter; -import com.github.markusbernhardt.selenium2library.Selenium2LibraryFatalException; -import com.github.markusbernhardt.selenium2library.Selenium2LibraryNonFatalException; -import com.github.markusbernhardt.selenium2library.locators.ElementFinder; -import com.github.markusbernhardt.selenium2library.locators.WindowManager; -import com.github.markusbernhardt.selenium2library.utils.Robotframework; -import com.github.markusbernhardt.selenium2library.utils.WebDriverCache; -import com.github.markusbernhardt.selenium2library.utils.WebDriverCache.SessionIdAliasWebDriverTuple; -import com.opera.core.systems.OperaDriver; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; @SuppressWarnings("deprecation") @RobotKeywords @@ -211,6 +214,16 @@ public String openBrowser(String url, String browserName, String alias, String r return openBrowser(url, browserName, alias, remoteUrl, desiredCapabilities, null); } + @RobotKeywordOverload + public String openBrowser(String url, String browserName, String alias, String remoteUrl, + String desiredCapabilities, String browserOptions) throws Throwable { + // Magic constants '2 minutes' and '3 hours' are defined at HttpClientFactory, and hard-coded by default: + // HttpClientFactory.TIMEOUT_TWO_MINUTES + // HttpClientFactory.TIMEOUT_THREE_HOURS + return openBrowser(url, browserName, alias, remoteUrl, desiredCapabilities, browserOptions, + "2 minutes", "3 hours"); + } + /** * Opens a new browser instance to given URL.
*
@@ -345,7 +358,7 @@ public String openBrowser(String url, String browserName, String alias, String r * href= * "http://selenium-grid.seleniumhq.org/faq.html#i_get_some_strange_errors_when_i_run_multiple_internet_explorer_instances_on_the_same_machine" * >Strange errors with multiple IE instances
- * + * * @param url * The URL to open in the newly created browser instance. * @param browserName @@ -369,18 +382,23 @@ public String openBrowser(String url, String browserName, String alias, String r * >DesiredCapabilities * @param browserOptions * Default=NONE. Extended browser options as JSON structure. + * @param connectionTimeout + * Default=2 minutes. Connection timeout value for (Remote) WebDriver. + * @param socketTimeout + * Default=3 hours. Socket timeout value for (Remote) WebDriver. * @return The index of the newly created browser instance. * @throws Throwable - if anything goes wrong - * + * * @see BrowserManagement#closeAllBrowsers * @see BrowserManagement#closeBrowser * @see BrowserManagement#switchBrowser */ @RobotKeyword @ArgumentNames({ "url", "browserName=firefox", "alias=NONE", "remoteUrl=False", "desiredCapabilities=NONE", - "browserOptions=NONE" }) + "browserOptions=NONE", "connectionTimeout=2 minutes", "socketTimeout=3 hours" }) public String openBrowser(String url, String browserName, String alias, String remoteUrl, - String desiredCapabilities, String browserOptions) throws Throwable { + String desiredCapabilities, String browserOptions, String connectionTimeout, + String socketTimeout) throws Throwable { try { logging.info("browserName: " + browserName); if (remoteUrl != null) { @@ -389,8 +407,10 @@ public String openBrowser(String url, String browserName, String alias, String r } else { logging.info(String.format("Opening browser '%s' to base url '%s'", browserName, url)); } - - WebDriver webDriver = createWebDriver(browserName, desiredCapabilities, remoteUrl, browserOptions); + final int connectionTimeoutMillis = TimeUtils.convertRobotTimeToMillis(connectionTimeout); + final int socketTimeoutMillis = TimeUtils.convertRobotTimeToMillis(socketTimeout); + WebDriver webDriver = createWebDriver(browserName, desiredCapabilities, remoteUrl, browserOptions, + CustomHttpClientFactory.createWithSpecificTimeout(connectionTimeoutMillis, socketTimeoutMillis)); webDriver.get(url); String sessionId = webDriverCache.register(webDriver, alias); logging.debug(String.format("Opened browser with session id %s", sessionId)); @@ -1350,14 +1370,14 @@ protected String getPasswordFromURL(URL url) { } protected WebDriver createWebDriver(String browserName, String desiredCapabilitiesString, String remoteUrlString, - String browserOptions) throws MalformedURLException { + String browserOptions, HttpClient.Factory factory) throws MalformedURLException { browserName = browserName.toLowerCase().replace(" ", ""); DesiredCapabilities desiredCapabilities = createDesiredCapabilities(browserName, desiredCapabilitiesString, browserOptions); WebDriver webDriver; if (remoteUrlString != null && !"False".equals(remoteUrlString)) { - webDriver = createRemoteWebDriver(desiredCapabilities, new URL(remoteUrlString)); + webDriver = createRemoteWebDriver(desiredCapabilities, new URL(remoteUrlString), factory); } else { webDriver = createLocalWebDriver(browserName, desiredCapabilities); } @@ -1404,8 +1424,10 @@ protected WebDriver createLocalWebDriver(String browserName, DesiredCapabilities throw new Selenium2LibraryFatalException(browserName + " is not a supported browser."); } - protected WebDriver createRemoteWebDriver(DesiredCapabilities desiredCapabilities, URL remoteUrl) { - HttpCommandExecutor httpCommandExecutor = new HttpCommandExecutor(remoteUrl); + protected WebDriver createRemoteWebDriver(DesiredCapabilities desiredCapabilities, URL remoteUrl, + HttpClient.Factory factory) { + HttpCommandExecutor httpCommandExecutor = new HttpCommandExecutor(Collections.emptyMap(), + remoteUrl, factory); setRemoteWebDriverProxy(httpCommandExecutor); return new Augmenter().augment(new RemoteWebDriver(httpCommandExecutor, desiredCapabilities)); } diff --git a/src/main/java/com/github/markusbernhardt/selenium2library/utils/CustomHttpClientFactory.java b/src/main/java/com/github/markusbernhardt/selenium2library/utils/CustomHttpClientFactory.java new file mode 100644 index 0000000..5b99c9b --- /dev/null +++ b/src/main/java/com/github/markusbernhardt/selenium2library/utils/CustomHttpClientFactory.java @@ -0,0 +1,61 @@ +package com.github.markusbernhardt.selenium2library.utils; + +import org.apache.http.auth.Credentials; +import org.apache.http.impl.client.CloseableHttpClient; +import org.openqa.selenium.remote.http.HttpClient; +import org.openqa.selenium.remote.internal.ApacheHttpClient; +import org.openqa.selenium.remote.internal.HttpClientFactory; + +import java.net.URL; + +/** + * Provides customized {@link HttpClient.Factory} instances. + * {@link HttpClient} creation is via delegation to {@link ApacheHttpClient.Factory} as default behavior does. + */ +public class CustomHttpClientFactory implements HttpClient.Factory { + private final HttpClient.Factory delegate; + + private CustomHttpClientFactory(HttpClientFactory factory) { + delegate = new ApacheHttpClient.Factory(factory); + } + + @Override + public HttpClient createClient(URL url) { + return delegate.createClient(url); + } + + /** + * Creates a HttpClient.Factory with customized connection and socked timeouts. + * Default timeout values are 2 minutes for connection, and 3 hours for socket as hard-coded in default + * {@link HttpClientFactory} class. + * + * @param connectionTimeout the connection timeout in milliseconds + * @param socketTimeout the socket timeout in milliseconds + * @return a HttpClient.Factory instance that will create + */ + public static HttpClient.Factory createWithSpecificTimeout(int connectionTimeout, int socketTimeout) { + HttpClientFactory factory = new CustomTimeoutHttpClientFactory(connectionTimeout, socketTimeout); + return new CustomHttpClientFactory(factory); + } + + /** + * {@link HttpClientFactory} is using hard-coded timeouts for connection timeout (2m) and socket timeout (3h). + * This behavior is undesired in some cases, when socket timeout keeps the webdriver blocked for 3 hours on too + * early connection attempt to opened port. + */ + private static class CustomTimeoutHttpClientFactory extends HttpClientFactory { + private final int connectionTimeout; + private final int socketTimeout; + + private CustomTimeoutHttpClientFactory(int connectionTimeout, int socketTimeout) { + super(connectionTimeout, socketTimeout); + this.connectionTimeout = connectionTimeout; + this.socketTimeout = socketTimeout; + } + + @Override + public CloseableHttpClient createHttpClient(Credentials credentials) { + return super.createHttpClient(credentials, connectionTimeout, socketTimeout); + } + } +} diff --git a/src/main/java/com/github/markusbernhardt/selenium2library/utils/TimeUtils.java b/src/main/java/com/github/markusbernhardt/selenium2library/utils/TimeUtils.java new file mode 100644 index 0000000..e729c36 --- /dev/null +++ b/src/main/java/com/github/markusbernhardt/selenium2library/utils/TimeUtils.java @@ -0,0 +1,89 @@ +package com.github.markusbernhardt.selenium2library.utils; + +/** + * Utilities to convert Robot Framework time. + */ +public final class TimeUtils { + private static final int SECONDS_TO_MILLISECS = 1000; + private static final int MINUTES_TO_MILLISECS = 60 * SECONDS_TO_MILLISECS; + private static final int HOURS_TO_MILLISECS = 60 * MINUTES_TO_MILLISECS; + private static final int DAYS_TO_MILLISECS = 24 * HOURS_TO_MILLISECS; + + private static final String DAYS_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*d(ays?)?)?"; + private static final String HOURS_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*h(ours?)?)?"; + private static final String MINUTES_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*m(in((ute)?s?)?)?)?"; + private static final String SECONDS_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*s(ec((ond)?s?))?)?"; + private static final String MILLISECONDS_PATTERN = "(\\s*\\d+(\\.\\d+)?\\s*(millis(ec((ond)?s?))?|ms))?"; + private static final String TIME_STRING_PATTERN = "-?(\\s*\\d+(\\.\\d+)?\\s*|" + DAYS_PATTERN + HOURS_PATTERN + + MINUTES_PATTERN + SECONDS_PATTERN + MILLISECONDS_PATTERN + ")"; + + private TimeUtils() { + // this is a utility class + } + + /** + * Converts a Robot Framework time string to milliseconds value. + * See http://robotframework.org/robotframework/latest/libraries/DateTime.html + * + * @param robotTimeString a valid time string + * @return the time in milliseconds + */ + public static int convertRobotTimeToMillis(String robotTimeString) { + int sum = 0; + if (!robotTimeString.matches(TIME_STRING_PATTERN)) { + throw new IllegalArgumentException("Invalid time string " + robotTimeString); + } + String[] values = robotTimeString.replaceAll("(\\d)([dhms])", "$1 $2").split("\\s+"); + try { + if (values.length == 1) { + return Math.round(Float.parseFloat(values[0]) * SECONDS_TO_MILLISECS); + } + final int signum = values[0].startsWith("-") ? -1 : 1; + if (values.length % 2 != 0) { + throw new IllegalArgumentException("Invalid time string " + robotTimeString); + } + for (int i = 0; i < values.length - 1; i+=2) { + final float value = Math.abs(Float.parseFloat(values[i])); + final int multiplier = getMultiplier(values[i + 1]); + sum += signum * Math.round(value * multiplier); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid time string " + robotTimeString, e); + } + return sum; + } + + private static int getMultiplier(String specifier) { + if ("days".equalsIgnoreCase(specifier) + || "day".equalsIgnoreCase(specifier) + || "d".equalsIgnoreCase(specifier)) { + return DAYS_TO_MILLISECS; + } + if ("hours".equalsIgnoreCase(specifier) + || "hour".equalsIgnoreCase(specifier) + || "h".equalsIgnoreCase(specifier)) { + return HOURS_TO_MILLISECS; + } + if ("minutes".equalsIgnoreCase(specifier) + || "minute".equalsIgnoreCase(specifier) + || "mins".equalsIgnoreCase(specifier) + || "min".equalsIgnoreCase(specifier) + || "m".equalsIgnoreCase(specifier)) { + return MINUTES_TO_MILLISECS; + } + if ("seconds".equalsIgnoreCase(specifier) + || "second".equalsIgnoreCase(specifier) + || "secs".equalsIgnoreCase(specifier) + || "sec".equalsIgnoreCase(specifier) + || "s".equalsIgnoreCase(specifier)) { + return SECONDS_TO_MILLISECS; + } + if ("milliseconds".equalsIgnoreCase(specifier) + || "millisecond".equalsIgnoreCase(specifier) + || "millis".equalsIgnoreCase(specifier) + || "ms".equalsIgnoreCase(specifier)) { + return 1; + } + throw new IllegalArgumentException("Invalid time specifier " + specifier); + } +} diff --git a/src/test/java/com/github/markusbernhardt/selenium2library/utils/TimeUtilsTest.java b/src/test/java/com/github/markusbernhardt/selenium2library/utils/TimeUtilsTest.java new file mode 100644 index 0000000..2d8c926 --- /dev/null +++ b/src/test/java/com/github/markusbernhardt/selenium2library/utils/TimeUtilsTest.java @@ -0,0 +1,60 @@ +package com.github.markusbernhardt.selenium2library.utils; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class TimeUtilsTest { + + @Test + public void whenNoSpecifier_ShouldConvertSeconds() { + assertEquals(1000, TimeUtils.convertRobotTimeToMillis("1")); + assertEquals(-2000, TimeUtils.convertRobotTimeToMillis("-2")); + assertEquals(500, TimeUtils.convertRobotTimeToMillis("0.5")); + } + + @Test + public void whenMillisecondsSpecified_ShouldConvertMillisecsonds() { + assertEquals(-10, TimeUtils.convertRobotTimeToMillis("-10ms")); + assertEquals(1, TimeUtils.convertRobotTimeToMillis("0.8 milliseconds")); + assertEquals(1, TimeUtils.convertRobotTimeToMillis("1 millisecond")); + assertEquals(1, TimeUtils.convertRobotTimeToMillis("1 millis")); + } + + @Test + public void whenSecondsSpecified_ShouldConvertSeconds() { + assertEquals(-10000, TimeUtils.convertRobotTimeToMillis("-10seconds")); + assertEquals(800, TimeUtils.convertRobotTimeToMillis("0.8 s")); + assertEquals(1000, TimeUtils.convertRobotTimeToMillis("1 sec")); + assertEquals(2000, TimeUtils.convertRobotTimeToMillis("2second")); + } + + @Test + public void whenMinutesSpecified_ShouldConvertMinutes() { + assertEquals(-600000, TimeUtils.convertRobotTimeToMillis("-10min")); + assertEquals(48000, TimeUtils.convertRobotTimeToMillis("0.8 m")); + assertEquals(60000, TimeUtils.convertRobotTimeToMillis("1 minute")); + assertEquals(120000, TimeUtils.convertRobotTimeToMillis("2minutes")); + } + + @Test + public void whenHoursSpecified_ShouldConvertHours() { + assertEquals(-36000000, TimeUtils.convertRobotTimeToMillis("-10h")); + assertEquals(3600000, TimeUtils.convertRobotTimeToMillis("1 hour")); + assertEquals(5400000, TimeUtils.convertRobotTimeToMillis("1.5 hours")); + } + + @Test + public void whenDaysSpecified_ShouldConvertDays() { + assertEquals(-864000000, TimeUtils.convertRobotTimeToMillis("-10days")); + assertEquals(129600000, TimeUtils.convertRobotTimeToMillis("1.5d")); + assertEquals(86400000, TimeUtils.convertRobotTimeToMillis("1 day")); + } + + @Test + public void whenComplexSpecified_ShouldConvertComplex() { + assertEquals(151264015, TimeUtils.convertRobotTimeToMillis("1.5d 6 hours 1 minute 4 secs 15ms")); + assertEquals(-151264015, TimeUtils.convertRobotTimeToMillis("-1.5d 6 hours 1 minute 4 secs 15ms")); + } + +}