Repository: seleniumkit/gridrouter Branch: master Commit: 13aec297eac9 Files: 135 Total size: 180.9 KB Directory structure: gitextract_k296bzti/ ├── .gitignore ├── AUTHORS ├── LICENSE ├── README.md ├── ci/ │ └── jenkins.groovy ├── config/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── ru/ │ │ │ └── qatools/ │ │ │ └── gridrouter/ │ │ │ └── config/ │ │ │ ├── GridRouterException.java │ │ │ ├── HostSelectionStrategy.java │ │ │ ├── RandomHostSelectionStrategy.java │ │ │ ├── RegionWithCount.java │ │ │ ├── SequentialHostSelectionStrategy.java │ │ │ ├── VersionWithCount.java │ │ │ ├── WithBrowserVersionFind.java │ │ │ ├── WithCopy.java │ │ │ ├── WithCount.java │ │ │ ├── WithRoute.java │ │ │ ├── WithRoutesMap.java │ │ │ ├── WithVersionFind.java │ │ │ └── WithXmlView.java │ │ └── resources/ │ │ └── xsd/ │ │ ├── bindings.xjb │ │ └── config.xsd │ └── test/ │ └── java/ │ └── ru/ │ └── qatools/ │ └── gridrouter/ │ └── config/ │ └── RandomHostSelectionStrategyTest.java ├── pom.xml ├── proxy/ │ ├── pom.xml │ └── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── ru/ │ │ │ └── qatools/ │ │ │ └── gridrouter/ │ │ │ ├── ConfigRepository.java │ │ │ ├── ConfigRepositoryXml.java │ │ │ ├── JsonWireUtils.java │ │ │ ├── PingServlet.java │ │ │ ├── ProxyServlet.java │ │ │ ├── QuotaServlet.java │ │ │ ├── RequestUtils.java │ │ │ ├── RouteServlet.java │ │ │ ├── SessionStorageEvictionScheduler.java │ │ │ ├── SpringHttpServlet.java │ │ │ ├── StatsServlet.java │ │ │ ├── caps/ │ │ │ │ ├── AppiumCapabilityProcessor.java │ │ │ │ ├── CapabilityProcessor.java │ │ │ │ ├── CapabilityProcessorFactory.java │ │ │ │ ├── DummyCapabilityProcessor.java │ │ │ │ └── IECapabilityProcessor.java │ │ │ ├── json/ │ │ │ │ ├── Describable.java │ │ │ │ ├── JsonFormatter.java │ │ │ │ ├── JsonMessageFactory.java │ │ │ │ ├── JsonWithAnyProperties.java │ │ │ │ ├── WithErrorMessage.java │ │ │ │ └── WithJsonView.java │ │ │ └── sessions/ │ │ │ ├── AvailableBrowserCheckExeption.java │ │ │ ├── AvailableBrowsersChecker.java │ │ │ ├── BrowserVersion.java │ │ │ ├── BrowsersCountMap.java │ │ │ ├── GridRouterUserStats.java │ │ │ ├── MemoryStatsCounter.java │ │ │ ├── SkipAvailableBrowsersChecker.java │ │ │ ├── StatsCounter.java │ │ │ ├── WaitAvailableBrowserTimeoutException.java │ │ │ └── WaitAvailableBrowsersChecker.java │ │ ├── resources/ │ │ │ ├── META-INF/ │ │ │ │ └── spring/ │ │ │ │ └── application-context.xml │ │ │ ├── application.properties │ │ │ ├── log4j.properties │ │ │ └── xsd/ │ │ │ ├── json.xjb │ │ │ └── json.xsd │ │ └── webapp/ │ │ └── WEB-INF/ │ │ └── web.xml │ └── test/ │ ├── java/ │ │ └── ru/ │ │ └── qatools/ │ │ └── gridrouter/ │ │ ├── CommandDecodingTest.java │ │ ├── JsonWireUtilsTest.java │ │ ├── PingServletTest.java │ │ ├── ProxyServletExceptionsWithHubTest.java │ │ ├── ProxyServletExceptionsWithoutHubTest.java │ │ ├── ProxyServletTest.java │ │ ├── ProxyServletWithBrokenAndOkHubsTest.java │ │ ├── ProxyServletWithBrokenHubTest.java │ │ ├── ProxyServletWithOneHubTest.java │ │ ├── ProxyServletWithTwoHubsTest.java │ │ ├── ProxyServletWithoutHubTest.java │ │ ├── QuotaReloadTest.java │ │ ├── QuotaServletTest.java │ │ ├── RegionsTest.java │ │ ├── RouteServletTest.java │ │ ├── StatsServletTest.java │ │ ├── caps/ │ │ │ ├── AppiumCapabilityProcessorTest.java │ │ │ ├── CapabilityProcessorFactoryTest.java │ │ │ └── IECapabilityProcessorTest.java │ │ ├── json/ │ │ │ └── JsonMessageTest.java │ │ ├── sessions/ │ │ │ ├── MemoryStatsCounterTest.java │ │ │ └── WaitAvailableBrowsersCheckerTest.java │ │ └── utils/ │ │ ├── FindElementCallback.java │ │ ├── GridRouterRule.java │ │ ├── HttpUtils.java │ │ ├── HubEmulator.java │ │ ├── HubEmulatorRule.java │ │ ├── JettyRule.java │ │ ├── JsonUtils.java │ │ ├── MatcherUtils.java │ │ ├── QuotaUtils.java │ │ ├── RememberUrlCallback.java │ │ ├── SocketUtil.java │ │ └── TestConfigRepository.java │ └── resources/ │ ├── META-INF/ │ │ └── spring/ │ │ └── test-application-context.xml │ ├── application.properties │ ├── log4j.properties │ └── quota/ │ ├── user1.xml │ ├── user2.xml │ └── user3.xml └── testing/ ├── group_vars/ │ └── all.yml ├── ping-local-gridrouter.sh ├── roles/ │ ├── start/ │ │ ├── files/ │ │ │ └── gridrouter/ │ │ │ ├── conf/ │ │ │ │ ├── application.properties │ │ │ │ ├── quota/ │ │ │ │ │ └── selenium.xml │ │ │ │ └── users.properties │ │ │ └── webapps/ │ │ │ └── ROOT.xml │ │ └── tasks/ │ │ ├── before.yml │ │ ├── main.yml │ │ ├── start-gridrouter.yml │ │ └── start-selenium.yml │ ├── stop/ │ │ └── tasks/ │ │ ├── before.yml │ │ ├── main.yml │ │ ├── stop-gridrouter.yml │ │ └── stop-selenium.yml │ └── test/ │ ├── files/ │ │ ├── java/ │ │ │ ├── pom.xml │ │ │ ├── run.sh │ │ │ └── src/ │ │ │ └── test/ │ │ │ └── java/ │ │ │ └── SeleniumTest.java │ │ ├── js/ │ │ │ ├── config.json │ │ │ ├── fixtures/ │ │ │ │ └── big-script.js │ │ │ ├── package.json │ │ │ ├── run.sh │ │ │ └── test/ │ │ │ ├── selenium-test-sync.js │ │ │ └── selenium-test-wd.js │ │ └── python/ │ │ ├── requirements.txt │ │ ├── run.sh │ │ └── src/ │ │ └── test_selenium.py │ └── tasks/ │ ├── after.yml │ ├── before.yml │ ├── main.yml │ └── run-tests.yml ├── start.yml ├── stop.yml └── test.yml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # IDEA files .idea *.iml # Maven files target # Docker compose files compose/war # Npm modules node_modules ================================================ FILE: AUTHORS ================================================ The following authors have created the source code of "Selenium Grid Router" published and distributed by YANDEX LLC as the owner: * Alexander Andryashin * Dmitry Baev * Artem Eroshenko * Innokenty Shuvalov * Ivan Krutov ================================================ FILE: LICENSE ================================================ (C) YANDEX LLC, 2015 The Source Code called "Selenium Grid Router" available at https://github.com/seleniumkit/gridrouter is subject to the terms of the Apache License 2.0 (hereinafter referred to as the "License"). The text of the License is the following: Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Selenium Grid Router **Selenium Grid Router** is a lightweight server that routes and proxies [Selenium Wedriver](http://www.seleniumhq.org/projects/webdriver/) requests to multiple Selenium hubs. ## Golang Implementation There is a smaller and faster Golang implementation of this server. See https://github.com/aerokube/ggr for more details. ## What is this for If you're frequently using Selenium for running your tests in browsers you may notice that a standard [Selenium Grid](https://github.com/SeleniumHQ/selenium/wiki/Grid2) installation has some faults that can prevent you from using it on large scale: * **Does not support high availability.** Selenium Grid consists of a single entry point **hub** server and multiple **node** processes. Users interact only with hub. That means that if for some reason hub goes down all nodes also become unavailable to user. * **Does not scale well.** Our experience shows that even when running on high-end hardware Selenium hub is able to handle correctly no more than 20-30 nodes. When more nodes are connected hub very often stops responding. * **Does not support authentication and authorization.** Standard Selenium grid hub makes all nodes available for everyone. ## How it works The basic idea is very simple: 1. Define user names and their passwords in a plain text file 2. Distribute a set of running Selenium hubs (aka "hosts") with nodes connected to each one over multiple datacenters 3. For each defined user save hosts to a simple XML configuration file 4. Start multiple instances of Grid Router in different datacenters and load-balance them 5. Work with Grid Router like you do with a regular Selenium hub ## Installation Currently we maintain only Debian packages. To install on Ubuntu ensure that you have Java 8 installed: ``` # add-apt-repository ppa:webupd8team/java # apt-get update # apt-get install oracle-java8-installer ``` Then install Gridrouter itself: ``` # add-apt-repository ppa:yandex-qatools/gridrouter # apt-get update # apt-get install yandex-grid-router # service yandex-grid-router start ``` Configuration files are located in `/etc/grid-router/` directory, XML quota files - by default in `/etc/grid-router/quota/`, log files reside in `/var/log/grid-router/`, binaries are installed to `/usr/share/grid-router`. ## Configuration Two types of configuration files exist: * A plain text file with users and passwords (users.properties) * An XML file with user quota definition (<username>.xml) ### Users list (users.properties) A typical file looks like this: ``` alice:alicePassword, user bob:bobPassword, user ``` As you can see passwords are **NOT** encrypted. This is because we consider quotas as a way to easily limit Selenium browsers consumption and not a restrictive tool. ### User quota definition (<username>.xml) This file has the following format: ```xml ``` What we basically do in this file - we enumerate hub hosts, ports and counts of browsers available on each hub. We also distribute hosts across regions, i.e. we place hosts from different datacenters in different **<region>** tags. The most important thing is to make sure that browser name and browser version have **exactly** the same value as respective Selenium hub does. ### Authentication Grid router is using [BASIC HTTP authentication](https://en.wikipedia.org/wiki/Basic_access_authentication). That means that for the majority of test frameworks connection URL would be: ``` http://username:password@grid-router-host.example.com:4444/wd/hub ``` However some Javascript test frameworks have their own ways to specify connection URL, user name and password. ### Hub selection logic When you request a browser by specifying its name and version **Grid Router** does the following: 1. Searches for the browser in user quota XML and returns error if not found 2. Randomly selects a host from all hosts and tries to obtain browser on that host. Our algorithm also considers browser counts specified in XML for each host so that hosts with more browsers get more connections. 3. If browser was obtained - returns it to the user and proxies all requests in this session to the same host 4. If not - selects a new host **from another region** and tries again. This guarantees that when one datacenter goes down in most of cases we'll obtain browser at worst after the second attempt. 5. After trying all hosts returns error if no browser was obtained ### Hub configuration recommendations Our experience shows that Grid Router works better with a big set of "small" hubs (having no more than 5 connected nodes) than with some "big" hubs. A good idea is to launch small virtual machines (with 1 or 2 virtual CPUs) containing one Selenium hub process 4-5 Selenium node processes that connect to **localhost**. This gives us the following profit: * Because we have more hubs the probability to successfully obtain browser is greater * If each virtual machine has only one browser version installed - it's simpler to increase overall count of available browsers * Hubs with small count of connected nodes perform better ## Development We're using [Docker](https://www.docker.com/) and [Ansible](http://www.ansible.com/) for integration tests so you need to install them on your Mac or Linux. ### Install Boot2docker (dog-nail for Mac users) * Install Ansible: `brew install ansible` * Create an empty inventory file: `touch /usr/local/etc/ansible/hosts` * Adjust Python settings: `echo 'localhost ansible_python_interpreter=/usr/local/bin/python' >> /usr/local/etc/ansible/hosts` * Instally Python from [official website](https://www.python.org/ftp/python/2.7.10/python-2.7.10-macosx10.6.pkg) * Install requests with pip: `pip install requests[security]` * Install docker-py: `pip install -Iv https://pypi.python.org/packages/source/d/docker-py/docker-py-1.1.0.tar.gz` * Run boot2docker: `boot2docker up` * Get Docker VM IP: `boot2docker ip` * Modify `/etc/hosts`: ` boot2docker` * Add certificates information to console: `$(boot2docker shellinit)` * Export correct host name: `export DOCKER_HOST=tcp://boot2docker:2376` ### Running service locally #### Start 1. Build project: `mvn clean package` 2. Start app: `ansible-playbook testing/start.yml` 3. Check that container is running: `docker ps -a` #### Run integration tests ```bash] $ ansible-playbook testing/test.yml ``` #### Stop ```bash $ ansible-playbook testing/stop.yml ``` ================================================ FILE: ci/jenkins.groovy ================================================ def project = 'gridrouter'; def repo = 'seleniumkit/gridrouter' def buildWarJob = mavenJob("${project}_build-war") def e2eTestsJob = job("${project}_e2e-tests") def sonarJob = mavenJob("${project}_sonar") def sonarIncrJob = mavenJob("${project}_sonar-incr") def deployJob = mavenJob("${project}_deploy") def pullRequestJob = multiJob("${project}_pull-reqest_flow") def snapshotJob = multiJob("${project}_snapshot_flow") def releaseJob = mavenJob("${project}_release_flow") buildWarJob.with { label('maven') scm { git { remote { github(repo, 'https', 'github.com') refspec('${GIT_REFSPEC}') } branch('${GIT_COMMIT}') localBranch('master') } } goals('clean package') publishers { archiveArtifacts('proxy/target/*.war') } } e2eTestsJob.with { label('e2e') scm { git { remote { github(repo, 'https', 'github.com') refspec('${GIT_REFSPEC}') } branch('${GIT_COMMIT}') localBranch('master') } } steps { copyArtifacts(buildWarJob.name) { includePatterns('proxy/target/*.war') buildSelector { buildNumber('${WAR_BUILD_NUMBER}') } } shell('ansible-playbook -e "workspace=/tmp/e2e/${JOB_NAME}/${BUILD_NUMBER}" testing/start.yml') shell('ansible-playbook -e "workspace=/tmp/e2e/${JOB_NAME}/${BUILD_NUMBER}" testing/test.yml') shell('ansible-playbook -e "workspace=/tmp/e2e/${JOB_NAME}/${BUILD_NUMBER}" testing/stop.yml') } publishers { archiveJunit('testing/target/surefire-reports/*.xml') } } sonarJob.with { label('maven') scm { git { remote { github(repo, 'https', 'github.com') refspec('${GIT_REFSPEC}') } branch('${GIT_COMMIT}') localBranch('master') } } publishers { sonar() } } sonarIncrJob.with { label('maven') scm { git { remote { github(repo, 'https', 'github.com') refspec('${GIT_REFSPEC}') } branch('${GIT_COMMIT}') localBranch('master') } } configure { it / 'publishers' / 'hudson.plugins.sonar.SonarPublisher' { jdk('(Inherit From Job)') branch() language() jobAdditionalProperties('-Dsonar.analysis.mode=incremental -Dsonar.github.pullRequest=${ghprbPullId} -Dsonar.github.repository=' + repo) settings(class: 'jenkins.mvn.DefaultSettingsProvider') globalSettings(class: 'jenkins.mvn.DefaultGlobalSettingsProvider') usePrivateRepository(false) } } } deployJob.with { label('maven') scm { git { remote { github(repo, 'https', 'github.com') refspec('${GIT_REFSPEC}') } branch('${GIT_COMMIT}') localBranch('master') } } goals('clean deploy') } pullRequestJob.with { label('master') displayName('Grid Router Pull Requests Flow') scm { git { remote { github(repo, 'https', 'github.com') refspec('+refs/pull/*:refs/remotes/origin/pr/*') } branch('${sha1}') } } triggers { pullRequest { orgWhitelist(['seleniumkit']) permitAll() useGitHubHooks() } } steps { phase('Build war file') { job(sonarIncrJob.name) { prop('GIT_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*'); prop('GIT_COMMIT', '\${sha1}'); } job(buildWarJob.name) { prop('GIT_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*'); prop('GIT_COMMIT', '\${sha1}'); } } phase('Run e2e tests', 'UNSTABLE') { job(e2eTestsJob.name) { prop('WAR_BUILD_NUMBER', '\${' + buildWarJob.name.toUpperCase().replace("-", "_") + '_BUILD_NUMBER}'); prop('GIT_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*'); prop('GIT_COMMIT', '\${sha1}'); } } } publishers { aggregateDownstreamTestResults() } } snapshotJob.with { label('default') displayName('Grid Router Snapshot Flow') scm { git { remote { github(repo, 'https', 'github.com') } localBranch('master') branch('master') } } triggers { githubPush() } steps { phase('Build war file') { job(buildWarJob.name) { prop('GIT_COMMIT', '\${GIT_COMMIT}'); prop('GIT_REFSPEC', ''); } } phase('Run e2e tests') { job(e2eTestsJob.name) { prop('WAR_BUILD_NUMBER', '\${' + buildWarJob.name.toUpperCase().replace("-", "_") + '_BUILD_NUMBER}'); prop('GIT_COMMIT', '\${GIT_COMMIT}'); prop('GIT_REFSPEC', ''); } job(sonarJob.name) { prop('GIT_COMMIT', '\${GIT_COMMIT}'); prop('GIT_REFSPEC', ''); } } phase('Deploy war') { job(deployJob.name) { prop('GIT_COMMIT', '\${GIT_COMMIT}'); prop('GIT_REFSPEC', ''); } } } publishers { aggregateDownstreamTestResults() } } releaseJob.with { label('maven') displayName('Grid Router Release Flow') scm { git { remote { github(repo, 'https', 'github.com') } localBranch('master') branch('master') } } goals('clean deploy') wrappers { mavenRelease { releaseGoals('release:clean release:prepare release:perform') dryRunGoals('-DdryRun=true release:prepare') numberOfReleaseBuildsToKeep(10) } } } listView(project) { jobs { regex("${project}_.*_flow") } columns { status() name() lastSuccess() lastFailure() lastDuration() buildButton() } } ================================================ FILE: config/pom.xml ================================================ ru.qatools.seleniumkit gridrouter 1.32-SNAPSHOT 4.0.0 gridrouter-config Selenium Grid Router Config org.jvnet.jaxb2.maven2 maven-jaxb2-plugin commons-io commons-io org.apache.commons commons-lang3 commons-codec commons-codec 1.10 junit junit test org.hamcrest hamcrest-all test ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/GridRouterException.java ================================================ package ru.qatools.gridrouter.config; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class GridRouterException extends RuntimeException { public GridRouterException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/HostSelectionStrategy.java ================================================ package ru.qatools.gridrouter.config; import java.util.List; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface HostSelectionStrategy { Region selectRegion(List allRegions, List unvisitedRegions); Host selectHost(List hosts); } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/RandomHostSelectionStrategy.java ================================================ package ru.qatools.gridrouter.config; import java.util.ArrayList; import java.util.List; import static java.util.Collections.nCopies; import static java.util.Collections.shuffle; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class RandomHostSelectionStrategy implements HostSelectionStrategy { protected T selectRandom(List elements) { List copies = new ArrayList<>(); for (T element : elements) { copies.addAll(nCopies(element.getCount(), element)); } shuffle(copies); return copies.get(0); } @Override public Region selectRegion(List allRegions, List unvisitedRegions) { return selectRandom(unvisitedRegions); } @Override public Host selectHost(List hosts) { return selectRandom(hosts); } } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/RegionWithCount.java ================================================ package ru.qatools.gridrouter.config; import java.util.List; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface RegionWithCount extends WithCount { List getHosts(); @Override default int getCount() { return getHosts().stream().mapToInt(Host::getCount).sum(); } } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/SequentialHostSelectionStrategy.java ================================================ package ru.qatools.gridrouter.config; import java.util.List; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class SequentialHostSelectionStrategy implements HostSelectionStrategy { private int hostIndex; @Override public Region selectRegion(List allRegions, List unvisitedRegions) { return unvisitedRegions.get(0); } @Override public Host selectHost(List hosts) { Host host = hosts.get(hostIndex++ % hosts.size()); hostIndex %= hosts.size(); return host; } } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/VersionWithCount.java ================================================ package ru.qatools.gridrouter.config; import java.util.List; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface VersionWithCount extends WithCount { List getRegions(); @Override default int getCount() { return getRegions().stream().mapToInt(Region::getCount).sum(); } } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/WithBrowserVersionFind.java ================================================ package ru.qatools.gridrouter.config; import java.util.List; import static org.apache.commons.lang3.StringUtils.isEmpty; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface WithBrowserVersionFind { List getBrowsers(); default Browser findBrowser(String name) { return getBrowsers().stream() .filter(b -> b.getName().equals(name)) .findFirst().orElse(null); } default Version find(String browserName, String browserVersion) { Browser browser = findBrowser(browserName); if (browser == null) { return null; } return isEmpty(browserVersion) ? browser.findDefaultVersion() : browser.findVersion(browserVersion); } } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/WithCopy.java ================================================ package ru.qatools.gridrouter.config; import java.util.ArrayList; import java.util.List; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface WithCopy { List getHosts(); String getName(); /** * Creates a copy for the {@link Region} object, which is almost deep: * the hosts itself are not copied although the list is new. * * @return a copy of the object */ default Region copy() { return new Region(new ArrayList<>(getHosts()), getName()); } } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/WithCount.java ================================================ package ru.qatools.gridrouter.config; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface WithCount { int getCount(); } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/WithRoute.java ================================================ package ru.qatools.gridrouter.config; import org.apache.commons.codec.digest.DigestUtils; import java.nio.charset.StandardCharsets; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface WithRoute { default String getAddress() { return getName() + ":" + getPort(); } default String getRoute() { return "http://" + getAddress(); } default String getRouteId() { return DigestUtils.md5Hex(getRoute().getBytes(StandardCharsets.UTF_8)); } String getName(); int getPort(); } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/WithRoutesMap.java ================================================ package ru.qatools.gridrouter.config; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface WithRoutesMap { List getBrowsers(); default Map getRoutesMap() { HashMap routes = new HashMap<>(); getBrowsers().stream() .flatMap(b -> b.getVersions().stream()) .flatMap(v -> v.getRegions().stream()) .flatMap(r -> r.getHosts().stream()) .forEach(h -> routes.put(h.getRouteId(), h.getRoute())); return routes; } } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/WithVersionFind.java ================================================ package ru.qatools.gridrouter.config; import java.util.List; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface WithVersionFind { default Version findDefaultVersion() { return findVersion(getDefaultVersion()); } default Version findVersion(String versionPrefix) { return getVersions().stream() .filter(v -> v.getNumber().startsWith(versionPrefix)) .findFirst().orElse(null); } List getVersions(); String getDefaultVersion(); } ================================================ FILE: config/src/main/java/ru/qatools/gridrouter/config/WithXmlView.java ================================================ package ru.qatools.gridrouter.config; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import java.io.StringWriter; import static java.nio.charset.StandardCharsets.UTF_8; import static javax.xml.bind.Marshaller.JAXB_ENCODING; import static javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT; /** * @author Dmitry Baev charlie@yandex-team.ru */ public interface WithXmlView { default String toXml() { try { JAXBContext context = JAXBContext.newInstance(getClass()); Marshaller marshaller = context.createMarshaller(); marshaller.setProperty(JAXB_ENCODING, UTF_8.toString()); marshaller.setProperty(JAXB_FORMATTED_OUTPUT, true); StringWriter writer = new StringWriter(); marshaller.marshal(this, writer); return writer.toString(); } catch (JAXBException e) { throw new GridRouterException("Unable to marshall bean", e); } } } ================================================ FILE: config/src/main/resources/xsd/bindings.xjb ================================================ ru.qatools.gridrouter.config.WithBrowserVersionFind ru.qatools.gridrouter.config.WithXmlView ru.qatools.gridrouter.config.WithRoutesMap ru.qatools.gridrouter.config.WithVersionFind ru.qatools.gridrouter.config.VersionWithCount ru.qatools.gridrouter.config.WithCopy ru.qatools.gridrouter.config.RegionWithCount ru.qatools.gridrouter.config.WithRoute ru.qatools.gridrouter.config.WithCount ================================================ FILE: config/src/main/resources/xsd/config.xsd ================================================ ================================================ FILE: config/src/test/java/ru/qatools/gridrouter/config/RandomHostSelectionStrategyTest.java ================================================ package ru.qatools.gridrouter.config; import org.hamcrest.Matcher; import org.junit.Test; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Optional; import java.util.UUID; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.both; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.lessThan; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class RandomHostSelectionStrategyTest { private static final double ALLOWED_DEVIATION = 0.01; @Test @SuppressWarnings("ArraysAsListWithZeroOrOneArgument") public void testRandomness() { int entriesCount = 5000000; int keysCount = 10; Host host1 = new Host("host_1", 4444, keysCount - 1); List hosts = new ArrayList<>(keysCount); hosts.add(host1); int i = keysCount; while (i --> 1) { hosts.add(newHost()); } HashMap appearances = new HashMap<>(keysCount, entriesCount / keysCount); RandomHostSelectionStrategy strategy = new RandomHostSelectionStrategy(); i = entriesCount; while (i-- > 0) { Host host = strategy.selectRandom(hosts); appearances.put(host, Optional.ofNullable(appearances.get(host)).orElse(0) + 1); } assertThat(appearances.remove(host1), isAround(entriesCount / 2)); for (int count : appearances.values()) { assertThat(count, isAround(entriesCount / 2 / (keysCount - 1))); } } private static Host newHost() { return new Host(UUID.randomUUID().toString(), 4444, 1); } private static Matcher isAround(int count) { return both(greaterThan( (int) (count * (1 - ALLOWED_DEVIATION)) )).and(lessThan( (int) (count * (1 + ALLOWED_DEVIATION)) )); } } ================================================ FILE: pom.xml ================================================ 4.0.0 org.sonatype.oss oss-parent 9 ru.qatools.seleniumkit gridrouter 1.32-SNAPSHOT pom config proxy Selenium Grid Router UTF-8 1.8 4.1.6.RELEASE 9.3.6.v20151106 2.6.0-rc3 1.7.7 Yandex http://yandex.ru/ The Apache Software License, Version 2.0 http://www.apache.org/licenses/LICENSE-2.0.txt repo scm:git:git@github.com:seleniumkit/gridrouter.git scm:git:git@github.com:seleniumkit/gridrouter.git https://github.com/seleniumkit/gridrouter HEAD GitHub Issues https://github.com/seleniumkit/gridrouter/issues Jenkins http://ci.qatools.ru/ commons-io commons-io 2.4 org.apache.commons commons-lang3 3.4 junit junit 4.12 org.hamcrest hamcrest-all 1.3 org.apache.maven.plugins maven-compiler-plugin 3.3 ${java.version} ${java.version} org.apache.maven.plugins maven-javadoc-plugin 2.10.3 -Xdoclint:none org.apache.maven.plugins maven-source-plugin 2.4 jar org.apache.maven.plugins maven-release-plugin 2.5.2 @{project.version} org.jvnet.jaxb2.maven2 maven-jaxb2-plugin 0.12.3 generate src/main/resources/xsd src/main/resources/xsd true true true -Xinheritance -Xvalue-constructor org.jvnet.jaxb2_commons jaxb2-basics 0.9.4 org.jvnet.jaxb2_commons jaxb2-value-constructor 3.0 ================================================ FILE: proxy/pom.xml ================================================ ru.qatools.seleniumkit gridrouter 1.32-SNAPSHOT 4.0.0 gridrouter-proxy Selenium Grid Router Proxy war org.apache.maven.plugins maven-war-plugin true org.jvnet.jaxb2.maven2 maven-jaxb2-plugin ru.qatools.seleniumkit gridrouter-config ${project.version} ru.yandex.qatools.beanloader beanloader 2.1 commons-io commons-io org.apache.commons commons-lang3 org.springframework spring-web ${spring.version} org.eclipse.jetty jetty-servlet ${jetty.version} org.eclipse.jetty jetty-proxy ${jetty.version} org.eclipse.jetty jetty-security ${jetty.version} org.eclipse.jetty jetty-annotations ${jetty.version} test com.fasterxml.jackson.core jackson-databind ${jackson.version} com.fasterxml.jackson.core jackson-annotations ${jackson.version} org.apache.httpcomponents httpclient 4.5.2 org.slf4j slf4j-api ${slf4j.version} org.slf4j slf4j-log4j12 ${slf4j.version} junit junit test org.hamcrest hamcrest-all test org.mockito mockito-all 1.9.5 test org.mock-server mockserver-netty 3.9.17 test logback-classic ch.qos.logback org.json json 20140107 test org.seleniumhq.selenium selenium-java 2.53.0 test xml-apis xml-apis 1.4.01 test ru.yandex.qatools.matchers matcher-decorators 1.1 test org.springframework spring-test ${spring.version} test ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/ConfigRepository.java ================================================ package ru.qatools.gridrouter; import ru.qatools.gridrouter.config.Browser; import ru.qatools.gridrouter.config.Browsers; import ru.qatools.gridrouter.config.Version; import ru.qatools.gridrouter.json.JsonCapabilities; import java.util.HashMap; import java.util.Map; /** * @author Ilya Sadykov */ public interface ConfigRepository { Map getQuotaMap(); String getRoute(String routeId); default Version findVersion(String user, JsonCapabilities caps) { final Browsers browsers = getQuotaMap().get(user); return browsers != null ? browsers.find(caps.getBrowserName(), caps.getVersion()) : null; } default Map getBrowsersCountMap(String user) { HashMap countMap = new HashMap<>(); for (Browser browser : getQuotaMap().get(user).getBrowsers()) { for (Version version : browser.getVersions()) { countMap.put(browser.getName() + ":" + version.getNumber(), version.getCount()); } } return countMap; } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/ConfigRepositoryXml.java ================================================ package ru.qatools.gridrouter; import org.apache.commons.io.FilenameUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import ru.qatools.beanloader.BeanChangeListener; import ru.qatools.beanloader.BeanLoader; import ru.qatools.beanloader.BeanWatcher; import ru.qatools.gridrouter.config.Browsers; import javax.annotation.PostConstruct; import java.io.File; import java.io.IOException; import java.nio.file.Path; import java.util.HashMap; import java.util.Map; /** * @author Alexander Andyashin aandryashin@yandex-team.ru * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class ConfigRepositoryXml implements ConfigRepository, BeanChangeListener { private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRepositoryXml.class); private static final String XML_GLOB = "*.xml"; @Value("${grid.config.quota.directory}") private File quotaDirectory; @Value("${grid.config.quota.hotReload}") private boolean quotaHotReload; private Map userBrowsers = new HashMap<>(); private Map routes = new HashMap<>(); @PostConstruct public void init() { try { if (quotaHotReload) { LOGGER.debug("Starting quota watcher"); BeanWatcher.watchFor(Browsers.class, quotaDirectory.toPath(), XML_GLOB, this); } else { LOGGER.debug("Loading quota configuration"); BeanLoader.loadAll(Browsers.class, quotaDirectory.toPath(), XML_GLOB, this); } } catch (IOException e) { LOGGER.error("Quota configuration loading failed", e); } } @Override public void beanChanged(Path filename, Browsers browsers) { if (browsers == null) { LOGGER.info("Configuration file [{}] was deleted. " + "It is not purged from the running gridrouter process though.", filename); } else { LOGGER.info("Loading quota configuration file [{}]", filename); String user = FilenameUtils.getBaseName(filename.toString()); userBrowsers.put(user, browsers); routes.putAll(browsers.getRoutesMap()); LOGGER.info("Loaded quota configuration for [{}] from [{}]: \n\n{}", user, filename, browsers.toXml()); } } @Override public Map getQuotaMap() { return userBrowsers; } @Override public String getRoute(String routeId) { return routes.get(routeId); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/JsonWireUtils.java ================================================ package ru.qatools.gridrouter; import org.apache.http.client.utils.URIBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.http.HttpServletRequest; import java.io.UnsupportedEncodingException; import java.net.URISyntaxException; import java.net.URLDecoder; import static java.nio.charset.StandardCharsets.UTF_8; import static org.springframework.http.HttpMethod.DELETE; /** * @author Alexander Andyashin aandryashin@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru * @author Dmitry Baev charlie@yandex-team.ru * @author Artem Eroshenko eroshenkoam@yandex-team.ru */ public final class JsonWireUtils { private static final Logger LOGGER = LoggerFactory.getLogger(JsonWireUtils.class); public static final String WD_HUB_SESSION = "/wd/hub/session/"; public static final int SESSION_HASH_LENGTH = 32; private JsonWireUtils() { } public static boolean isUriValid(String uri) { return uri.length() > getUriPrefixLength(); } public static boolean isSessionDeleteRequest(HttpServletRequest request, String command) { return DELETE.name().equalsIgnoreCase(request.getMethod()) && !command.contains("/"); } public static String getSessionHash(String uri) { return uri.substring(WD_HUB_SESSION.length(), getUriPrefixLength()); } public static String getFullSessionId(String uri) { String tail = uri.substring(WD_HUB_SESSION.length()); int end = tail.indexOf('/'); if (end < 0) { return tail; } return tail.substring(0, end); } public static int getUriPrefixLength() { return WD_HUB_SESSION.length() + SESSION_HASH_LENGTH; } public static String redirectionUrl(String host, String command) throws URISyntaxException { return new URIBuilder(host).setPath(WD_HUB_SESSION + command).build().toString(); } public static String getCommand(String uri) { String encodedCommand = uri.substring(getUriPrefixLength()); try { return URLDecoder.decode(encodedCommand, UTF_8.name()); } catch (UnsupportedEncodingException e) { LOGGER.error("[UNABLE_TO_DECODE_COMMAND] - could not decode command: {}", encodedCommand, e); return encodedCommand; } } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/PingServlet.java ================================================ package ru.qatools.gridrouter; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; /** * @author Alexander Andyashin aandryashin@yandex-team.ru * @author Artem Eroshenko eroshenkoam@yandex-team.ru * @author Dmitry Baev charlie@yandex-team.ru */ @WebServlet(urlPatterns = {"/ping"}, asyncSupported = true) public class PingServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try (PrintWriter writer = resp.getWriter()) { writer.print("OK"); writer.flush(); } } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/ProxyServlet.java ================================================ package ru.qatools.gridrouter; import org.apache.commons.io.IOUtils; import org.eclipse.jetty.client.api.Request; import org.eclipse.jetty.client.util.StringContentProvider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import ru.qatools.gridrouter.json.JsonMessage; import ru.qatools.gridrouter.json.JsonMessageFactory; import ru.qatools.gridrouter.sessions.StatsCounter; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.annotation.WebInitParam; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import static java.nio.charset.StandardCharsets.UTF_8; import static org.springframework.web.context.support.SpringBeanAutowiringSupport.processInjectionBasedOnServletContext; import static ru.qatools.gridrouter.JsonWireUtils.*; import static ru.qatools.gridrouter.RequestUtils.getRemoteHost; /** * @author Alexander Andyashin aandryashin@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru * @author Dmitry Baev charlie@yandex-team.ru * @author Artem Eroshenko eroshenkoam@yandex-team.ru */ @WebServlet( urlPatterns = {WD_HUB_SESSION + "*"}, asyncSupported = true, initParams = { @WebInitParam(name = "timeout", value = "300000"), @WebInitParam(name = "idleTimeout", value = "300000") } ) public class ProxyServlet extends org.eclipse.jetty.proxy.ProxyServlet { private static final Logger LOGGER = LoggerFactory.getLogger(ProxyServlet.class); @Autowired private transient ConfigRepository config; @Autowired private transient StatsCounter statsCounter; @Override public void init(ServletConfig config) throws ServletException { super.init(config); processInjectionBasedOnServletContext(this, config.getServletContext()); } @Override protected void sendProxyRequest( HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest) { try { Request request = getRequestWithoutSessionId(clientRequest, proxyRequest); super.sendProxyRequest(clientRequest, proxyResponse, request); } catch (Exception exception) { LOGGER.error("[REQUEST_READ_FAILURE] [{}] - could not read client request, proxying request as is", clientRequest.getRemoteHost(), exception); super.sendProxyRequest(clientRequest, proxyResponse, proxyRequest); } } @Override protected String rewriteTarget(HttpServletRequest request) { String uri = request.getRequestURI(); String remoteHost = getRemoteHost(request); if (!isUriValid(uri)) { LOGGER.warn("[INVALID_SESSION_HASH] [{}] - request uri is {}", remoteHost, uri); return null; } String route = config.getRoute(getSessionHash(uri)); String command = getCommand(uri); if (route == null) { LOGGER.error("[ROUTE_NOT_FOUND] [{}] - request uri is {}", remoteHost, uri); return null; } if (isSessionDeleteRequest(request, command)) { LOGGER.info("[SESSION_DELETED] [{}] [{}] [{}]", remoteHost, route, command); statsCounter.deleteSession(getFullSessionId(uri), route); } else { statsCounter.updateSession(getFullSessionId(uri), route); } try { return redirectionUrl(route, command); } catch (Exception exception) { LOGGER.error("[REDIRECTION_URL_ERROR] [{}] - error building redirection uri because of {}\n" + " request uri: {}\n" + " parsed route: {}\n" + " parsed command: {}", remoteHost, exception.toString(), uri, route, command); } return null; } protected Request getRequestWithoutSessionId(HttpServletRequest clientRequest, Request proxyRequest) throws IOException { String content = IOUtils.toString(clientRequest.getInputStream(), UTF_8); if (!content.isEmpty()) { String remoteHost = getRemoteHost(clientRequest); content = removeSessionIdSafe(content, remoteHost); } return proxyRequest.content( new StringContentProvider(clientRequest.getContentType(), content, UTF_8)); } private String removeSessionIdSafe(String content, String remoteHost) { try { JsonMessage message = JsonMessageFactory.from(content); message.setSessionId(null); return message.toJson(); } catch (IOException exception) { LOGGER.error("[UNABLE_TO_REMOVE_SESSION_ID] [{}] - could not create proxy request without session id, " + "proxying request as is. Request content is: {}", remoteHost, content, exception); } return content; } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/QuotaServlet.java ================================================ package ru.qatools.gridrouter; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import javax.servlet.ServletException; import javax.servlet.annotation.HttpConstraint; import javax.servlet.annotation.ServletSecurity; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import static java.nio.charset.StandardCharsets.UTF_8; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import static ru.qatools.gridrouter.json.JsonFormatter.toJson; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ @WebServlet(urlPatterns = {"/quota"}, asyncSupported = true) @ServletSecurity(value = @HttpConstraint(rolesAllowed = {"user"})) public class QuotaServlet extends SpringHttpServlet { @Autowired private transient ConfigRepository config; @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setStatus(SC_OK); resp.setContentType(APPLICATION_JSON_VALUE); try (OutputStream output = resp.getOutputStream()) { String jsonResponse = toJson(config.getBrowsersCountMap(req.getRemoteUser())); IOUtils.write(jsonResponse, output, UTF_8); } } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/RequestUtils.java ================================================ package ru.qatools.gridrouter; import javax.servlet.http.HttpServletRequest; public final class RequestUtils { private static final String X_FORWARDED_FOR = "X-Forwarded-For"; public static String getRemoteHost(HttpServletRequest request) { String remoteHost = request.getHeader(X_FORWARDED_FOR); if (remoteHost == null) { return request.getRemoteHost(); } return remoteHost; } private RequestUtils(){} } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/RouteServlet.java ================================================ package ru.qatools.gridrouter; import com.fasterxml.jackson.core.JsonProcessingException; import org.apache.commons.io.IOUtils; import org.apache.http.HttpResponse; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpPost; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.LaxRedirectStrategy; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import ru.qatools.gridrouter.caps.CapabilityProcessorFactory; import ru.qatools.gridrouter.config.Host; import ru.qatools.gridrouter.config.HostSelectionStrategy; import ru.qatools.gridrouter.config.Region; import ru.qatools.gridrouter.config.Version; import ru.qatools.gridrouter.json.JsonCapabilities; import ru.qatools.gridrouter.json.JsonMessage; import ru.qatools.gridrouter.json.JsonMessageFactory; import ru.qatools.gridrouter.sessions.AvailableBrowserCheckExeption; import ru.qatools.gridrouter.sessions.AvailableBrowsersChecker; import ru.qatools.gridrouter.sessions.StatsCounter; import javax.servlet.ServletException; import javax.servlet.annotation.HttpConstraint; import javax.servlet.annotation.ServletSecurity; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import java.time.Instant; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; import static java.lang.String.format; import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.stream.Collectors.toList; import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.apache.http.HttpHeaders.ACCEPT; import static org.apache.http.entity.ContentType.APPLICATION_JSON; import static ru.qatools.gridrouter.RequestUtils.getRemoteHost; /** * @author Alexander Andyashin aandryashin@yandex-team.ru * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru * @author Artem Eroshenko eroshenkoam@yandex-team.ru */ @WebServlet(urlPatterns = {"/wd/hub/session"}, asyncSupported = true) @ServletSecurity(value = @HttpConstraint(rolesAllowed = {"user"})) public class RouteServlet extends SpringHttpServlet { private static final Logger LOGGER = LoggerFactory.getLogger(RouteServlet.class); private static final String ROUTE_TIMEOUT_CAPABILITY = "grid.router.route.timeout.seconds"; private static final int MAX_ROUTE_TIMEOUT_SECONDS = 300; @Autowired private transient ConfigRepository config; @Autowired private transient HostSelectionStrategy hostSelectionStrategy; @Autowired private transient StatsCounter statsCounter; @Autowired private transient CapabilityProcessorFactory capabilityProcessorFactory; @Autowired private transient AvailableBrowsersChecker avblBrowsersChecker; @Value("${grid.router.route.timeout.seconds:120}") private int routeTimeout; private AtomicLong requestCounter = new AtomicLong(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); JsonMessage message = JsonMessageFactory.from(request.getInputStream()); long requestId = requestCounter.getAndIncrement(); int routeTimeout = getRouteTimeout(request.getRemoteUser(), message); AtomicBoolean terminated = new AtomicBoolean(false); executor.submit(getRouteCallable(request, message, response, requestId, routeTimeout, terminated)); executor.shutdown(); try { executor.awaitTermination(routeTimeout, TimeUnit.SECONDS); terminated.set(true); } catch (InterruptedException e) { executor.shutdownNow(); } replyWithError("Timed out when searching for valid host", response); } private Callable getRouteCallable(HttpServletRequest request, JsonMessage message, HttpServletResponse response, long requestId, int routeTimeout, AtomicBoolean terminated) { return () -> { route(request, message, response, requestId, routeTimeout, terminated); return null; }; } private int getRouteTimeout(String user, JsonMessage message) { JsonCapabilities caps = message.getDesiredCapabilities(); try { if (caps.any().containsKey(ROUTE_TIMEOUT_CAPABILITY)) { Integer desiredRouteTimeout = Integer.valueOf(String.valueOf(caps.any().get(ROUTE_TIMEOUT_CAPABILITY))); routeTimeout = (desiredRouteTimeout < MAX_ROUTE_TIMEOUT_SECONDS) ? desiredRouteTimeout : MAX_ROUTE_TIMEOUT_SECONDS; LOGGER.warn("[{}] [INVALID_ROUTE_TIMEOUT] [{}]", user, desiredRouteTimeout); } } catch (NumberFormatException ignored) { } return routeTimeout; } private void route(HttpServletRequest request, JsonMessage message, HttpServletResponse response, long requestId, int routeTimeout, AtomicBoolean terminated) throws IOException { long initialSeconds = Instant.now().getEpochSecond(); JsonCapabilities caps = message.getDesiredCapabilities(); String user = request.getRemoteUser(); String remoteHost = getRemoteHost(request); String browser = caps.describe(); Version actualVersion = config.findVersion(user, caps); if (actualVersion == null) { LOGGER.warn("[{}] [UNSUPPORTED_BROWSER] [{}] [{}] [{}]", requestId, user, remoteHost, browser); replyWithError(format("Cannot find %s capabilities on any available node", caps.describe()), response); return; } caps.setVersion(actualVersion.getNumber()); capabilityProcessorFactory.getProcessor(caps).process(caps); List allRegions = actualVersion.getRegions() .stream().map(Region::copy).collect(toList()); List unvisitedRegions = new ArrayList<>(allRegions); int attempt = 0; JsonMessage hubMessage = null; try (CloseableHttpClient client = newHttpClient(routeTimeout * 1000)) { if (actualVersion.getPermittedCount() != null) { avblBrowsersChecker.ensureFreeBrowsersAvailable(user, remoteHost, browser, actualVersion); } while (!allRegions.isEmpty() && !terminated.get()) { attempt++; Region currentRegion = hostSelectionStrategy.selectRegion(allRegions, unvisitedRegions); Host host = hostSelectionStrategy.selectHost(currentRegion.getHosts()); String route = host.getRoute(); try { LOGGER.info("[{}] [SESSION_ATTEMPTED] [{}] [{}] [{}] [{}] [{}]", requestId, user, remoteHost, browser, route, attempt); String target = route + request.getRequestURI(); HttpResponse hubResponse = client.execute(post(target, message)); hubMessage = JsonMessageFactory.from(hubResponse.getEntity().getContent()); if (hubResponse.getStatusLine().getStatusCode() == SC_OK) { String sessionId = hubMessage.getSessionId(); hubMessage.setSessionId(host.getRouteId() + sessionId); replyWithOk(hubMessage, response); long createdDurationSeconds = Instant.now().getEpochSecond() - initialSeconds; LOGGER.info("[{}] [{}] [SESSION_CREATED] [{}] [{}] [{}] [{}] [{}] [{}]", requestId, createdDurationSeconds, user, remoteHost, browser, route, sessionId, attempt); statsCounter.startSession(hubMessage.getSessionId(), user, caps.getBrowserName(), actualVersion.getNumber(), route); return; } LOGGER.warn("[{}] [SESSION_FAILED] [{}] [{}] [{}] [{}] - {}", requestId, user, remoteHost, browser, route, hubMessage.getErrorMessage()); } catch (JsonProcessingException exception) { LOGGER.error("[{}] [BAD_HUB_JSON] [{}] [{}] [{}] [{}] - {}", "", requestId, user, remoteHost, browser, route, exception.getMessage()); } catch (IOException exception) { LOGGER.error("[{}] [HUB_COMMUNICATION_FAILURE] [{}] [{}] [{}] - {}", requestId, user, remoteHost, browser, route, exception.getMessage()); } currentRegion.getHosts().remove(host); if (currentRegion.getHosts().isEmpty()) { allRegions.remove(currentRegion); } unvisitedRegions.remove(currentRegion); if (unvisitedRegions.isEmpty()) { unvisitedRegions = new ArrayList<>(allRegions); } } } catch (AvailableBrowserCheckExeption e) { LOGGER.error("[{}] [AVAILABLE_BROWSER_CHECK_ERROR] [{}] [{}] [{}] - {}", requestId, user, remoteHost, browser, e.getMessage()); } LOGGER.error("[{}] [SESSION_NOT_CREATED] [{}] [{}] [{}]", requestId, user, remoteHost, browser); if (hubMessage == null) { replyWithError("Cannot create session on any available node", response); } else { replyWithError(hubMessage, response); } } protected void replyWithOk(JsonMessage message, HttpServletResponse response) throws IOException { reply(SC_OK, message, response); } protected void replyWithError(String errorMessage, HttpServletResponse response) throws IOException { replyWithError(JsonMessageFactory.error(13, errorMessage), response); } protected void replyWithError(JsonMessage message, HttpServletResponse response) throws IOException { reply(SC_INTERNAL_SERVER_ERROR, message, response); } protected void reply(int code, JsonMessage message, HttpServletResponse response) throws IOException { response.setStatus(code); response.setContentType(APPLICATION_JSON.toString()); String messageRaw = message.toJson(); response.setContentLength(messageRaw.getBytes(UTF_8).length); try (OutputStream output = response.getOutputStream()) { IOUtils.write(messageRaw, output, UTF_8); } } protected HttpPost post(String target, JsonMessage message) throws IOException { HttpPost method = new HttpPost(target); StringEntity entity = new StringEntity(message.toJson(), APPLICATION_JSON); method.setEntity(entity); method.setHeader(ACCEPT, APPLICATION_JSON.getMimeType()); return method; } protected CloseableHttpClient newHttpClient(int maxTimeout) { return HttpClientBuilder.create().setDefaultRequestConfig( RequestConfig.custom() .setConnectionRequestTimeout(10000) .setConnectTimeout(10000) .setSocketTimeout(maxTimeout) .build() ).setRedirectStrategy(new LaxRedirectStrategy()).disableAutomaticRetries().build(); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/SessionStorageEvictionScheduler.java ================================================ package ru.qatools.gridrouter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.Scheduled; import ru.qatools.gridrouter.sessions.StatsCounter; import java.time.Duration; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ @Configuration @EnableScheduling public class SessionStorageEvictionScheduler { @Value("${grid.router.evict.sessions.timeout.seconds}") private int timeout; @Autowired private StatsCounter statsCounter; @Scheduled(cron = "${grid.router.evict.sessions.cron}") public void expireOldSessions() { statsCounter.expireSessionsOlderThan(Duration.ofSeconds(timeout)); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/SpringHttpServlet.java ================================================ package ru.qatools.gridrouter; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import static org.springframework.web.context.support.SpringBeanAutowiringSupport.processInjectionBasedOnServletContext; /** * @author Ilya Sadykov */ public abstract class SpringHttpServlet extends HttpServlet { @Override public void init(ServletConfig config) throws ServletException { super.init(config); processInjectionBasedOnServletContext(this, config.getServletContext()); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/StatsServlet.java ================================================ package ru.qatools.gridrouter; import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Autowired; import ru.qatools.gridrouter.json.JsonFormatter; import ru.qatools.gridrouter.sessions.StatsCounter; import javax.servlet.ServletException; import javax.servlet.annotation.HttpConstraint; import javax.servlet.annotation.ServletSecurity; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import static java.nio.charset.StandardCharsets.UTF_8; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; /** * @author Dmitry Baev charlie@yandex-team.ru */ @WebServlet(urlPatterns = {"/stats"}, asyncSupported = true) @ServletSecurity(value = @HttpConstraint(rolesAllowed = {"user"})) public class StatsServlet extends SpringHttpServlet { @Autowired private transient StatsCounter statsCounter; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setStatus(SC_OK); response.setContentType(APPLICATION_JSON_VALUE); try (OutputStream output = response.getOutputStream()) { IOUtils.write(JsonFormatter.toJson( statsCounter.getStats(request.getRemoteUser()) ), output, UTF_8); } } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/caps/AppiumCapabilityProcessor.java ================================================ package ru.qatools.gridrouter.caps; import org.springframework.stereotype.Service; import ru.qatools.gridrouter.json.JsonCapabilities; import java.util.Map; /** *

* Sets "keepKeyChains" capability for Mac sessions. *

* * @author Ivan Krutov vania-pooh@yandex-team.ru * */ @SuppressWarnings("JavadocReference") @Service public class AppiumCapabilityProcessor implements CapabilityProcessor { private static final String PLATFORM_NAME = "platformName"; private static final String IOS = "iOS"; @Override public boolean accept(JsonCapabilities caps) { return caps.getBrowserName().isEmpty() && isMac(caps); } @Override public void process(JsonCapabilities caps) { caps.any().put("keepKeyChains", true); } private boolean isMac(JsonCapabilities jsonCapabilities) { Map capsMap = jsonCapabilities.any(); return capsMap.containsKey(PLATFORM_NAME) && String.valueOf(capsMap.get(PLATFORM_NAME)).contains(IOS); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/caps/CapabilityProcessor.java ================================================ package ru.qatools.gridrouter.caps; import ru.qatools.gridrouter.json.JsonCapabilities; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface CapabilityProcessor { boolean accept(JsonCapabilities caps); void process(JsonCapabilities caps); } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/caps/CapabilityProcessorFactory.java ================================================ package ru.qatools.gridrouter.caps; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import ru.qatools.gridrouter.json.JsonCapabilities; import java.util.List; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ @Component public class CapabilityProcessorFactory { @Autowired @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") private List processors; public CapabilityProcessor getProcessor(JsonCapabilities caps) { return processors.stream() .filter(p -> p.accept(caps)) .findFirst() .orElse(new DummyCapabilityProcessor()); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/caps/DummyCapabilityProcessor.java ================================================ package ru.qatools.gridrouter.caps; import ru.qatools.gridrouter.json.JsonCapabilities; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class DummyCapabilityProcessor implements CapabilityProcessor { @Override public boolean accept(JsonCapabilities caps) { throw new UnsupportedOperationException("Method DummyCapabilityProcessor::accept should never be called"); } @Override public void process(JsonCapabilities caps) { } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/caps/IECapabilityProcessor.java ================================================ package ru.qatools.gridrouter.caps; import org.springframework.stereotype.Service; import ru.qatools.gridrouter.json.JsonCapabilities; import ru.qatools.gridrouter.json.Proxy; /** *

* Sets "ie.ensureCleanSession" and "ie.usePerProcessProxy" for all new * internet explorer sessions to ensure clean browser state. *

*

* Apart from that it sets the "proxy" capability to * {@link org.openqa.selenium.Proxy.ProxyType#DIRECT ProxyType.DIRECT} * because explorers tend to reuse the proxy from the previous sessions. *

* * @author Innokenty Shuvalov innokenty@yandex-team.ru */ @SuppressWarnings("JavadocReference") @Service public class IECapabilityProcessor implements CapabilityProcessor { private static final String IE_BROWSER_NAME = "internet explorer"; @Override public boolean accept(JsonCapabilities caps) { return caps.getBrowserName().equals(IE_BROWSER_NAME); } @Override public void process(JsonCapabilities caps) { caps.any().put("ie.ensureCleanSession", true); caps.any().put("ie.usePerProcessProxy", true); if (!caps.any().containsKey("proxy")) { Proxy proxy = new Proxy(); proxy.setProxyType("DIRECT"); caps.any().put("proxy", proxy); } } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/json/Describable.java ================================================ package ru.qatools.gridrouter.json; import static org.apache.commons.lang3.StringUtils.isEmpty; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru * @author Dmitry Baev charlie@yandex-team.ru */ public interface Describable { String getBrowserName(); String getVersion(); default String describe() { return getBrowserName() + (isEmpty(getVersion()) ? "" : "-" + getVersion()); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/json/JsonFormatter.java ================================================ package ru.qatools.gridrouter.json; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class JsonFormatter { public static String toJson(Object o) throws JsonProcessingException { ObjectMapper mapper = new ObjectMapper(); mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); return mapper.writeValueAsString(o); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/json/JsonMessageFactory.java ================================================ package ru.qatools.gridrouter.json; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.io.InputStream; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public final class JsonMessageFactory { JsonMessageFactory() { } public static JsonMessage from(String content) throws IOException { return new ObjectMapper().readValue(content, JsonMessage.class); } public static JsonMessage from(InputStream stream) throws IOException { return new ObjectMapper().readValue(stream, JsonMessage.class); } public static JsonMessage error(int status, String errorMessage) { JsonMessage message = new JsonMessage(); message.setStatus(status); message.setErrorMessage(errorMessage); return message; } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/json/JsonWithAnyProperties.java ================================================ package ru.qatools.gridrouter.json; import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import java.util.HashMap; import java.util.Map; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public abstract class JsonWithAnyProperties { private Map otherProperties = new HashMap<>(); @JsonAnyGetter public Map any() { return otherProperties; } @JsonAnySetter public void set(String name, Object value) { otherProperties.put(name, value); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/json/WithErrorMessage.java ================================================ package ru.qatools.gridrouter.json; import com.fasterxml.jackson.annotation.JsonIgnore; import java.io.IOException; import java.util.Map; import static java.util.Collections.emptyMap; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface WithErrorMessage { String VALUE_KEY = "value"; String MESSAGE_KEY = "message"; String DEFAULT_ERROR_MESSAGE = "no error message was provided from hub"; Map any(); void set(String name, Object value); @JsonIgnore @SuppressWarnings("unchecked") default String getErrorMessage() throws IOException { try { return (String) ((Map) any().getOrDefault(VALUE_KEY, emptyMap())) .getOrDefault(MESSAGE_KEY, DEFAULT_ERROR_MESSAGE); } catch (ClassCastException ignored) { return DEFAULT_ERROR_MESSAGE; } } @JsonIgnore default void setErrorMessage(String message) { JsonValue value = new JsonValue(); value.setMessage(message); set(VALUE_KEY, value); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/json/WithJsonView.java ================================================ package ru.qatools.gridrouter.json; import com.fasterxml.jackson.core.JsonProcessingException; /** * @author Dmitry Baev charlie@yandex-team.ru */ public interface WithJsonView { default String toJson() throws JsonProcessingException { return JsonFormatter.toJson(this); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/AvailableBrowserCheckExeption.java ================================================ package ru.qatools.gridrouter.sessions; /** * @author Ilya Sadykov */ public class AvailableBrowserCheckExeption extends RuntimeException { public AvailableBrowserCheckExeption(String message) { super(message); } public AvailableBrowserCheckExeption(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/AvailableBrowsersChecker.java ================================================ package ru.qatools.gridrouter.sessions; import ru.qatools.gridrouter.config.Version; /** * @author Ilya Sadykov */ public interface AvailableBrowsersChecker { /** * Blocks or throws an exception if there is no browsers available for user */ void ensureFreeBrowsersAvailable(String user, String remoteHost, String browser, Version actualVersion); } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/BrowserVersion.java ================================================ package ru.qatools.gridrouter.sessions; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class BrowserVersion { private final String browser; private final String version; public BrowserVersion(String browser, String version) { this.browser = browser; this.version = version; } public String getBrowser() { return browser; } public String getVersion() { return version; } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/BrowsersCountMap.java ================================================ package ru.qatools.gridrouter.sessions; import java.util.HashMap; import java.util.Map; import java.util.Optional; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class BrowsersCountMap extends HashMap> implements GridRouterUserStats { public void increment(String browser, String version) { putIfAbsent(browser, new HashMap<>()); get(browser).compute(version, (v, count) -> Optional.ofNullable(count).orElse(0) + 1); } public void decrement(BrowserVersion browser) { decrement(browser.getBrowser(), browser.getVersion()); } public void decrement(String browser, String version) { if (!containsKey(browser)) { return; } Map versions = get(browser); if (!versions.containsKey(version)) { return; } int count = versions.get(version) - 1; if (count > 0) { versions.put(version, count); } else { versions.remove(version); } if (versions.isEmpty()) { remove(browser); } } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/GridRouterUserStats.java ================================================ package ru.qatools.gridrouter.sessions; import java.io.Serializable; /** * @author Ilya Sadykov */ public interface GridRouterUserStats extends Serializable { } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/MemoryStatsCounter.java ================================================ package ru.qatools.gridrouter.sessions; import java.time.Duration; import java.time.temporal.Temporal; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import static java.time.ZonedDateTime.now; import static java.util.stream.Collectors.toList; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class MemoryStatsCounter implements StatsCounter { private final Map session2instant = new HashMap<>(); private final Map session2user = new HashMap<>(); private final Map session2browserVersion = new HashMap<>(); private final Map user2browserCount = new HashMap<>(); @Override public synchronized void startSession(String sessionId, String user, String browser, String version, String route) { if (session2instant.put(sessionId, now()) == null) { session2user.put(sessionId, user); session2browserVersion.put(sessionId, new BrowserVersion(browser, version)); user2browserCount.putIfAbsent(user, new BrowsersCountMap()); user2browserCount.get(user).increment(browser, version); } } @Override public void updateSession(String sessionId, String route) { session2instant.replace(sessionId, now()); } @Override public synchronized void deleteSession(String sessionId, String route) { if (session2instant.remove(sessionId) != null) { String user = session2user.remove(sessionId); BrowserVersion browser = session2browserVersion.remove(sessionId); user2browserCount.get(user).decrement(browser); } } @Override public void expireSessionsOlderThan(Duration duration) { List sessions2delete = session2instant.entrySet().stream() .filter(e -> duration.compareTo(Duration.between(e.getValue(), now())) < 0) .map(Map.Entry::getKey) .collect(toList()); sessions2delete.stream().forEach(this::deleteSession); } @Override public Set getActiveSessions() { return session2instant.keySet(); } @Override public synchronized BrowsersCountMap getStats(String user) { return user2browserCount.getOrDefault(user, new BrowsersCountMap()); } @Override public int getSessionsCountForUser(String user) { return user2browserCount.getOrDefault(user, new BrowsersCountMap()).values() .parallelStream().flatMapToInt(version -> version.values().stream().mapToInt(Integer::intValue)) .sum(); } @Override public int getSessionsCountForUserAndBrowser(String user, String browser, String version) { return user2browserCount.getOrDefault(user, new BrowsersCountMap()).entrySet() .parallelStream().filter(entry -> entry.getKey().equals(browser)) .flatMapToInt(entry -> entry.getValue().entrySet().parallelStream() .filter(ver -> ver.getKey().equals(version)).mapToInt(Map.Entry::getValue) ).sum(); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/SkipAvailableBrowsersChecker.java ================================================ package ru.qatools.gridrouter.sessions; import ru.qatools.gridrouter.config.Version; /** * @author Ilya Sadykov */ public class SkipAvailableBrowsersChecker implements AvailableBrowsersChecker { @Override public void ensureFreeBrowsersAvailable(String user, String remoteHost, String browser, Version actualVersion) { // do nothing } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/StatsCounter.java ================================================ package ru.qatools.gridrouter.sessions; import java.time.Duration; import java.util.Set; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public interface StatsCounter { default void startSession(String sessionId, String user, String browser, String version) { startSession(sessionId, user, browser, version, null); } default void updateSession(String sessionId) { updateSession(sessionId, null); } default void deleteSession(String sessionId) { deleteSession(sessionId, null); } void startSession(String sessionId, String user, String browser, String version, String route); default void updateSession(String sessionId, String route) { } void deleteSession(String sessionId, String route); void expireSessionsOlderThan(Duration duration); Set getActiveSessions(); GridRouterUserStats getStats(String user); int getSessionsCountForUser(String user); int getSessionsCountForUserAndBrowser(String user, String browser, String version); } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/WaitAvailableBrowserTimeoutException.java ================================================ package ru.qatools.gridrouter.sessions; /** * @author Ilya Sadykov */ public class WaitAvailableBrowserTimeoutException extends AvailableBrowserCheckExeption { public WaitAvailableBrowserTimeoutException(String message) { super(message); } public WaitAvailableBrowserTimeoutException(String message, Throwable cause) { super(message, cause); } } ================================================ FILE: proxy/src/main/java/ru/qatools/gridrouter/sessions/WaitAvailableBrowsersChecker.java ================================================ package ru.qatools.gridrouter.sessions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import ru.qatools.gridrouter.config.Version; import java.time.Duration; import java.time.temporal.Temporal; import static java.lang.String.format; import static java.time.ZonedDateTime.now; import static java.util.UUID.randomUUID; import static java.util.concurrent.TimeUnit.SECONDS; /** * @author Ilya Sadykov */ public class WaitAvailableBrowsersChecker implements AvailableBrowsersChecker { private static final Logger LOGGER = LoggerFactory.getLogger(WaitAvailableBrowsersChecker.class); @Value("${grid.router.queue.interval.seconds}") protected int queueWaitInterval; @Autowired protected StatsCounter statsCounter; @Value("${grid.router.queue.timeout.seconds}") protected int queueTimeout; public WaitAvailableBrowsersChecker() { } public WaitAvailableBrowsersChecker(int queueTimeout, int queueWaitInterval, StatsCounter statsCounter) { this.queueTimeout = queueTimeout; this.queueWaitInterval = queueWaitInterval; this.statsCounter = statsCounter; } @Override public void ensureFreeBrowsersAvailable(String user, String remoteHost, String browser, Version version) { int waitAttempt = 0; final String requestId = randomUUID().toString(); final Temporal waitingStarted = now(); final Duration maxWait = Duration.ofSeconds(queueTimeout); while (maxWait.compareTo(Duration.between(waitingStarted, now())) > 0 && (countSessions(user, browser, version)) >= version.getPermittedCount()) { try { onWait(user, browser, version, requestId, waitAttempt); Thread.sleep(SECONDS.toMillis(queueWaitInterval)); } catch (InterruptedException e) { LOGGER.error("Failed to sleep thread", e); } if (maxWait.compareTo(Duration.between(waitingStarted, now())) < 0) { onWaitTimeout(user, browser, version, requestId, waitAttempt); } } onWaitFinished(user, browser, version, requestId, waitAttempt); } protected void onWaitTimeout(String user, String browser, Version version, String requestId, int waitAttempt) { throw new WaitAvailableBrowserTimeoutException( format("Waiting for available browser of %s %s timed out for %s after %s attempts", browser, version.getNumber(), user, waitAttempt)); } protected void onWait(String user, String browser, Version version, String requestId, int waitAttempt) { LOGGER.info("[SESSION_WAIT_AVAILABLE_BROWSER] [{}] [{}] [{}] [{}] [{}]", user, browser, version.getNumber(), version.getPermittedCount(), ++waitAttempt); } protected void onWaitFinished(String user, String browser, Version version, String requestId, int waitAttempt) { LOGGER.info("[SESSION_WAIT_FINISHED] [{}] [{}] [{}] [{}] [{}]", user, browser, version.getNumber(), version.getPermittedCount(), ++waitAttempt); } protected int countSessions(String user, String browser, Version actualVersion) { return statsCounter.getSessionsCountForUserAndBrowser(user, browser, actualVersion.getNumber()); } } ================================================ FILE: proxy/src/main/resources/META-INF/spring/application-context.xml ================================================ classpath:application.properties ================================================ FILE: proxy/src/main/resources/application.properties ================================================ grid.config.quota.directory=classpath:quota grid.router.quota.repository=ru.qatools.gridrouter.ConfigRepositoryXml grid.config.quota.hotReload=true grid.router.evict.sessions.cron=0 * * * * * grid.router.evict.sessions.timeout.seconds=120 grid.router.route.timeout.seconds=120 grid.router.queue.timeout.seconds=120 grid.router.queue.interval.seconds=5 grid.router.host.selection.strategy=ru.qatools.gridrouter.config.RandomHostSelectionStrategy grid.router.stats.counter=ru.qatools.gridrouter.sessions.MemoryStatsCounter grid.router.available.browsers.checker=ru.qatools.gridrouter.sessions.SkipAvailableBrowsersChecker ================================================ FILE: proxy/src/main/resources/log4j.properties ================================================ # suppress inspection "UnusedProperty" for whole file log4j.rootLogger=INFO, out # CONSOLE appender not used by default log4j.appender.out=org.apache.log4j.ConsoleAppender log4j.appender.out.layout=org.apache.log4j.PatternLayout log4j.appender.out.layout.ConversionPattern=%d [%-10.10t] %-5p %-20.20c{1} - %m%n log4j.throwableRenderer=org.apache.log4j.EnhancedThrowableRenderer ================================================ FILE: proxy/src/main/resources/xsd/json.xjb ================================================ ru.qatools.gridrouter.json.JsonWithAnyProperties ru.qatools.gridrouter.json.WithJsonView ru.qatools.gridrouter.json.WithErrorMessage ru.qatools.gridrouter.json.Describable ================================================ FILE: proxy/src/main/resources/xsd/json.xsd ================================================ ================================================ FILE: proxy/src/main/webapp/WEB-INF/web.xml ================================================ contextConfigLocation classpath:META-INF/spring/*application-context.xml org.springframework.web.context.ContextLoaderListener default org.eclipse.jetty.servlet.DefaultServlet dirAllowed false BASIC Selenium Grid Router ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/CommandDecodingTest.java ================================================ package ru.qatools.gridrouter; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.net.URLEncoder; import java.util.Arrays; import java.util.Collection; import static java.nio.charset.StandardCharsets.UTF_8; import static org.hamcrest.Matchers.endsWith; import static org.junit.Assert.assertThat; /** * @author Artem Eroshenko eroshenkoam@yandex-team.ru */ @RunWith(Parameterized.class) public class CommandDecodingTest { public static final String SUFFIX = "http://host.com/wd/hub/session/8dec71ede39ad9ff3"; public static final String POSTFIX = "b3fbc03311bdc45282358f1-f09c-4c44-8057-4b82f4a53002/element/id/"; public String requestUri; public String elementId; public CommandDecodingTest(String elementId) throws Exception { this.requestUri = String.format("%s%s%s", SUFFIX, POSTFIX, URLEncoder.encode(elementId, UTF_8.name())); this.elementId = elementId; } @Parameterized.Parameters public static Collection getData() { return Arrays.asList( new Object[]{"text_???"}, new Object[]{"text_&_not_text"} ); } @Test public void testOutput() throws Exception { assertThat(JsonWireUtils.getCommand(requestUri), endsWith(elementId)); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/JsonWireUtilsTest.java ================================================ package ru.qatools.gridrouter; import org.junit.Test; import java.nio.charset.StandardCharsets; import static java.util.UUID.randomUUID; import static org.apache.commons.codec.digest.DigestUtils.md5Hex; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static ru.qatools.gridrouter.JsonWireUtils.WD_HUB_SESSION; import static ru.qatools.gridrouter.JsonWireUtils.getFullSessionId; import static ru.qatools.gridrouter.JsonWireUtils.getSessionHash; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class JsonWireUtilsTest { @Test public void testGetSessionHash() { String routeHash = md5Hex("hubAddress".getBytes(StandardCharsets.UTF_8)); assertThat(getSessionHash(sessionRequest(routeHash, randomUUID().toString(), "")), is(equalTo(routeHash))); assertThat(getSessionHash(sessionRequest(routeHash, randomUUID().toString(), "dhgdhg")), is(equalTo(routeHash))); assertThat(getSessionHash(sessionRequest(routeHash, randomUUID().toString(), "dh/gdh/")), is(equalTo(routeHash))); assertThat(getSessionHash(sessionRequest(routeHash, randomUUID().toString(), "dh/gdh/g")), is(equalTo(routeHash))); } @Test public void testGetFullSessionId() { String routeHash = md5Hex("hubAddress".getBytes(StandardCharsets.UTF_8)); String sessionId = randomUUID().toString(); String expected = routeHash + sessionId; assertThat(getFullSessionId(sessionRequest(routeHash, sessionId, "")), is(equalTo(expected))); assertThat(getFullSessionId(sessionRequest(routeHash, sessionId, "sfgsds")), is(equalTo(expected))); assertThat(getFullSessionId(sessionRequest(routeHash, sessionId, "sfg/sds/")), is(equalTo(expected))); assertThat(getFullSessionId(sessionRequest(routeHash, sessionId, "sfg/sds/adfad")), is(equalTo(expected))); } public String sessionRequest(String routeHash, String sessionId, String sessionCommand) { if (!sessionCommand.isEmpty()) { sessionCommand = "/".concat(sessionCommand); } return WD_HUB_SESSION + routeHash + sessionId + sessionCommand; } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/PingServletTest.java ================================================ package ru.qatools.gridrouter; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClientBuilder; import org.junit.Rule; import org.junit.Test; import ru.qatools.gridrouter.utils.GridRouterRule; import java.io.IOException; import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class PingServletTest { @Rule public GridRouterRule gridRouter = new GridRouterRule(); @Test public void testPingWithAuth() throws IOException { assertThat(executeSimpleGet(gridRouter.baseUrlWithAuth + "/ping"), equalTo(SC_OK)); } public static int executeSimpleGet(String url) throws IOException { return HttpClientBuilder .create().build() .execute(new HttpGet(url)) .getStatusLine() .getStatusCode(); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/ProxyServletExceptionsWithHubTest.java ================================================ package ru.qatools.gridrouter; import org.junit.After; import org.junit.Rule; import ru.qatools.gridrouter.utils.HubEmulatorRule; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class ProxyServletExceptionsWithHubTest extends ProxyServletExceptionsWithoutHubTest { @Rule public HubEmulatorRule hub = new HubEmulatorRule( 8081); @After public void tearDown() { hub.verify().totalRequestsCountIs(0); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/ProxyServletExceptionsWithoutHubTest.java ================================================ package ru.qatools.gridrouter; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.UnsupportedCommandException; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import ru.qatools.gridrouter.utils.GridRouterRule; import static org.openqa.selenium.remote.DesiredCapabilities.chrome; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static ru.qatools.gridrouter.utils.GridRouterRule.hubUrl; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class ProxyServletExceptionsWithoutHubTest { @Rule public GridRouterRule gridRouter = new GridRouterRule(); @Test(expected = UnsupportedCommandException.class) public void testProxyWithWrongAuth() { new RemoteWebDriver(hubUrl(gridRouter.baseUrlWrongPassword), firefox()); } @Test(expected = UnsupportedCommandException.class) public void testProxyWithoutAuth() { new RemoteWebDriver(hubUrl(gridRouter.baseUrl), firefox()); } @Test(expected = WebDriverException.class) public void testProxyWithNotSupportedBrowser() { new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), chrome()); } @Test(expected = WebDriverException.class) public void testProxyWithNotSupportedVersion() { DesiredCapabilities caps = firefox(); caps.setVersion("1"); new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), caps); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/ProxyServletTest.java ================================================ package ru.qatools.gridrouter; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.By; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.WebElement; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import org.openqa.selenium.remote.RemoteWebElement; import ru.qatools.gridrouter.utils.GridRouterRule; import java.net.URL; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.openqa.selenium.Platform.ANY; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public abstract class ProxyServletTest { @Rule public GridRouterRule gridRouter = new GridRouterRule(); private final URL url; public ProxyServletTest(String user) { url = GridRouterRule.hubUrl(gridRouter.baseUrl(user)); } protected final URL getUrl() { return url; } @Test public void testSpecifyingBrowserVersion() { DesiredCapabilities caps = firefox(); caps.setVersion("32"); new RemoteWebDriver(getUrl(), caps); } @Test public void testSessionIdDoesNotChange() { RemoteWebDriver driver = new RemoteWebDriver(getUrl(), firefox()); String sessionId = driver.getSessionId().toString(); driver.getCurrentUrl(); driver.get("some url"); assertThat(driver.getSessionId().toString(), is(equalTo(sessionId))); driver.getCurrentUrl(); assertThat(driver.getSessionId().toString(), is(equalTo(sessionId))); } @Test public void testSessionIdChangesForANewBrowser() { RemoteWebDriver driver1 = new RemoteWebDriver(getUrl(), firefox()); String sessionId1 = driver1.getSessionId().toString(); RemoteWebDriver driver2 = new RemoteWebDriver(getUrl(), firefox()); String sessionId2 = driver2.getSessionId().toString(); assertThat(sessionId1, is(not(equalTo(sessionId2)))); } @Test public void testQuit() { RemoteWebDriver driver = new RemoteWebDriver(getUrl(), firefox()); driver.quit(); } @Test public void testSendRequestParams() { RemoteWebDriver driver = new RemoteWebDriver(getUrl(), firefox()); String url = "some url"; driver.getCurrentUrl(); driver.get(url); assertThat(driver.getCurrentUrl(), is(url)); } @Test public void testFindElement() { RemoteWebDriver driver = new RemoteWebDriver(getUrl(), firefox()); driver.getCurrentUrl(); String selector = "//lol[foo='bar']"; WebElement element = driver.findElement(By.xpath(selector)); assertThat( ((RemoteWebElement) element).getId(), is(String.valueOf(selector.hashCode())) ); } @Test public void testNullVersion() throws Exception { String browserName = "other"; try { new RemoteWebDriver(getUrl(), new DesiredCapabilities(browserName, null, ANY)); } catch (WebDriverException e) { assertThat(e.getMessage(), startsWith("Cannot find " + browserName + " capabilities on any available node")); } } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithBrokenAndOkHubsTest.java ================================================ package ru.qatools.gridrouter; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.remote.RemoteWebDriver; import ru.qatools.gridrouter.utils.GridRouterRule; import ru.qatools.gridrouter.utils.HubEmulatorRule; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static ru.qatools.gridrouter.utils.GridRouterRule.USER_2; import static ru.qatools.gridrouter.utils.GridRouterRule.hubUrl; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class ProxyServletWithBrokenAndOkHubsTest { @Rule public GridRouterRule gridRouter = new GridRouterRule(); @Rule public HubEmulatorRule hub1 = new HubEmulatorRule(8081, hub -> hub.emulate().newSessionFailures(1)); @Rule public HubEmulatorRule hub2 = new HubEmulatorRule(8082, hub -> hub.emulate().newSessions(1)); @Test public void testFailingHubIsSkipped() { new RemoteWebDriver(hubUrl(gridRouter.baseUrl(USER_2)), firefox()); hub1.verify().totalRequestsCountIs(1); hub1.verify().totalRequestsCountIs(1); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithBrokenHubTest.java ================================================ package ru.qatools.gridrouter; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.RemoteWebDriver; import ru.qatools.gridrouter.utils.GridRouterRule; import ru.qatools.gridrouter.utils.HubEmulatorRule; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class ProxyServletWithBrokenHubTest { @ClassRule public static GridRouterRule gridRouter = new GridRouterRule(); @Rule public HubEmulatorRule hub = new HubEmulatorRule( 8081, hub -> hub.emulate().newSessionFailures(1)); @Test(expected = WebDriverException.class) public void testFailingHubIsSkipped() { new RemoteWebDriver(GridRouterRule.hubUrl(gridRouter.baseUrlWithAuth), firefox()); hub.verify().totalRequestsCountIs(1); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithOneHubTest.java ================================================ package ru.qatools.gridrouter; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.remote.RemoteWebDriver; import ru.qatools.gridrouter.utils.HubEmulatorRule; import static org.hamcrest.MatcherAssert.assertThat; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static ru.qatools.gridrouter.utils.GridRouterRule.USER_1; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class ProxyServletWithOneHubTest extends ProxyServletTest { @Rule public HubEmulatorRule hub = new HubEmulatorRule( 8081, hub -> hub.emulate().newSessions(1) ); public ProxyServletWithOneHubTest() throws Exception { super(USER_1); } @Test public void testSessionIdsHaveACommonPrefix() { hub.emulate().newSessions(1); RemoteWebDriver driver1 = new RemoteWebDriver(getUrl(), firefox()); String sessionId1 = driver1.getSessionId().toString(); RemoteWebDriver driver2 = new RemoteWebDriver(getUrl(), firefox()); String sessionId2 = driver2.getSessionId().toString(); assertThat("sessionIds should have the same prefix", sessionId1.regionMatches(0, sessionId2, 0, 30)); hub.verify().totalRequestsCountIs(2); } @Test @Override public void testSpecifyingBrowserVersion() { super.testSpecifyingBrowserVersion(); hub.verify().totalRequestsCountIs(1); } @Test @Override public void testSessionIdDoesNotChange() { hub.emulate().navigation(); super.testSessionIdDoesNotChange(); hub.verify().totalRequestsCountIs(4); } @Test @Override public void testSessionIdChangesForANewBrowser() { hub.emulate().newSessions(1); super.testSessionIdChangesForANewBrowser(); hub.verify().totalRequestsCountIs(2); } @Test @Override public void testQuit() { hub.emulate().quit(); super.testQuit(); hub.verify().newSessionRequestsCountIs(1) .quitRequestsCountIs(1); } @Override public void testSendRequestParams() { hub.emulate().navigation(); super.testSendRequestParams(); hub.verify().totalRequestsCountIs(4); } @Override public void testFindElement() { hub.emulate().navigation().findElement(); super.testFindElement(); hub.verify().totalRequestsCountIs(3); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithTwoHubsTest.java ================================================ package ru.qatools.gridrouter; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.remote.RemoteWebDriver; import ru.qatools.gridrouter.utils.HubEmulatorRule; import static org.hamcrest.MatcherAssert.assertThat; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static ru.qatools.gridrouter.utils.GridRouterRule.USER_2; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class ProxyServletWithTwoHubsTest extends ProxyServletTest { @Rule public HubEmulatorRule hub1 = new HubEmulatorRule( 8081, hub -> hub.emulate().newSessions(1)); @Rule public HubEmulatorRule hub2 = new HubEmulatorRule( 8082, hub -> hub.emulate().newSessions(1)); public ProxyServletWithTwoHubsTest() throws Exception { super(USER_2); } @Test public void testSessionIdsHaveNoCommonPrefix() { RemoteWebDriver driver1 = new RemoteWebDriver(getUrl(), firefox()); String sessionId1 = driver1.getSessionId().toString(); RemoteWebDriver driver2 = new RemoteWebDriver(getUrl(), firefox()); String sessionId2 = driver2.getSessionId().toString(); assertThat("sessionIds should not have the same prefix", !sessionId1.regionMatches(0, sessionId2, 0, 30)); hub1.verify().totalRequestsCountIs(1); hub2.verify().totalRequestsCountIs(1); } @Override public void testSpecifyingBrowserVersion() { super.testSpecifyingBrowserVersion(); } @Override public void testSessionIdDoesNotChange() { hub1.emulate().navigation(); hub2.emulate().navigation(); super.testSessionIdDoesNotChange(); } @Test @Override public void testSessionIdChangesForANewBrowser() { super.testSessionIdChangesForANewBrowser(); hub1.verify().totalRequestsCountIs(1); hub2.verify().totalRequestsCountIs(1); } @Override public void testQuit() { hub1.emulate().quit(); hub2.emulate().quit(); super.testQuit(); } @Override public void testSendRequestParams() { hub1.emulate().navigation(); hub2.emulate().navigation(); super.testSendRequestParams(); } @Test @Override public void testFindElement() { hub1.emulate().navigation().findElement(); hub2.emulate().navigation().findElement(); super.testFindElement(); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/ProxyServletWithoutHubTest.java ================================================ package ru.qatools.gridrouter; import org.junit.ClassRule; import org.junit.Test; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.RemoteWebDriver; import ru.qatools.gridrouter.utils.GridRouterRule; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class ProxyServletWithoutHubTest { @ClassRule public static GridRouterRule gridRouterRule = new GridRouterRule(); @Test(expected = WebDriverException.class) public void testProxyWithProperAuth() { new RemoteWebDriver(GridRouterRule.hubUrl(gridRouterRule.baseUrlWithAuth), firefox()); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/QuotaReloadTest.java ================================================ package ru.qatools.gridrouter; import org.junit.*; import ru.qatools.gridrouter.utils.GridRouterRule; import ru.qatools.gridrouter.utils.HubEmulatorRule; import static java.util.concurrent.TimeUnit.SECONDS; import static org.hamcrest.MatcherAssert.assertThat; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static ru.qatools.gridrouter.utils.GridRouterRule.USER_1; import static ru.qatools.gridrouter.utils.GridRouterRule.USER_4; import static ru.qatools.gridrouter.utils.MatcherUtils.canObtain; import static ru.qatools.gridrouter.utils.QuotaUtils.*; import static ru.yandex.qatools.matchers.decorators.MatcherDecorators.should; import static ru.yandex.qatools.matchers.decorators.MatcherDecorators.timeoutHasExpired; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ @Ignore public class QuotaReloadTest { public static final int HUB_PORT_2 = 8082; @Rule public GridRouterRule gridRouter = new GridRouterRule(); @Rule public HubEmulatorRule hub2 = new HubEmulatorRule( HUB_PORT_2, hub -> hub.emulate().newSessions(1)); @Test public void testQuotaIsReloadedOnFileChange() throws Exception { replacePortInQuotaFile(USER_1, hub2.getPort()); assertThat(USER_1, should(canObtain(gridRouter, firefox())) .whileWaitingUntil(timeoutHasExpired(SECONDS.toMillis(60)) .withPollingInterval(SECONDS.toMillis(3)))); } @Test public void testNewQuotaFileIsLoaded() throws Exception { copyQuotaFile(USER_1, USER_4, 0, 0, hub2.getPort()); assertThat(USER_4, should(canObtain(gridRouter, firefox())) .whileWaitingUntil(timeoutHasExpired(SECONDS.toMillis(60)) .withPollingInterval(SECONDS.toMillis(3)))); } @After public void tearDown() { hub2.verify().newSessionRequestsCountIs(1); hub2.verify().totalRequestsCountIs(1); } @AfterClass public static void restoreQuotaFiles() throws Exception { replacePortInQuotaFile(USER_1, 8081); deleteQuotaFile(USER_4); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/QuotaServletTest.java ================================================ package ru.qatools.gridrouter; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClientBuilder; import org.junit.ClassRule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; import ru.qatools.gridrouter.utils.GridRouterRule; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static ru.qatools.gridrouter.utils.GridRouterRule.*; /** * TODO add test for user with different browsers and different versions * @author Dmitry Baev charlie@yandex-team.ru */ @RunWith(Parameterized.class) public class QuotaServletTest { @ClassRule public static GridRouterRule gridRouter = new GridRouterRule(); @Parameters(name = "{0}") public static Collection data() { return Arrays.asList(new Object[][]{ {USER_1, 1}, {USER_2, 4}, {USER_3, 8}, }); } private final String user; private final int browsersCount; public QuotaServletTest(String user, int browsersCount) { this.user = user; this.browsersCount = browsersCount; } @Test public void testQuota() throws IOException { Map quota = executeSimpleGet(gridRouter.baseUrl(user) + "/quota"); assertThat(quota.size(), is(1)); assertThat(quota.get("firefox:32.0"), is(browsersCount)); } public static Map executeSimpleGet(String url) throws IOException { CloseableHttpResponse execute = HttpClientBuilder .create().build() .execute(new HttpGet(url)); InputStream content = execute.getEntity().getContent(); //noinspection unchecked return new ObjectMapper().readValue(content, HashMap.class); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/RegionsTest.java ================================================ package ru.qatools.gridrouter; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.RemoteWebDriver; import ru.qatools.gridrouter.utils.GridRouterRule; import ru.qatools.gridrouter.utils.HubEmulatorRule; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static ru.qatools.gridrouter.utils.GridRouterRule.*; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class RegionsTest { @ClassRule public static GridRouterRule gridRouter = new GridRouterRule(); @Rule public HubEmulatorRule hub1 = new HubEmulatorRule( 8081); @Rule public HubEmulatorRule hub2 = new HubEmulatorRule( 8082); @Rule public HubEmulatorRule hub3 = new HubEmulatorRule( 8083); @Test public void testRegionIsChangedAfterFailedTry() { hub3.emulate().newSessions(1); new RemoteWebDriver(hubUrl(gridRouter.baseUrl(USER_3)), firefox()); hub1.verify().newSessionRequestsCountIs(1); hub2.verify().newSessionRequestsCountIs(0); hub3.verify().newSessionRequestsCountIs(1); } @Test public void testAllHostsAreTriedExactlyOnceInTheEnd() { getWebDriverSafe(USER_3); hub1.verify().newSessionRequestsCountIs(1); hub2.verify().newSessionRequestsCountIs(1); hub3.verify().newSessionRequestsCountIs(1); } @Test public void testConfigIsImmutableBetweenRequests() { // note here user1 is used for simplicity getWebDriverSafe(USER_1); hub1.verify().newSessionRequestsCountIs(1); getWebDriverSafe(USER_1); hub1.verify().newSessionRequestsCountIs(2); } private static void getWebDriverSafe(String user) { try { new RemoteWebDriver(hubUrl(gridRouter.baseUrl(user)), firefox()); } catch (WebDriverException ignored) { } } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/RouteServletTest.java ================================================ package ru.qatools.gridrouter; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.WebDriverException; import org.openqa.selenium.remote.RemoteWebDriver; import ru.qatools.gridrouter.utils.GridRouterRule; import ru.qatools.gridrouter.utils.HubEmulatorRule; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static ru.qatools.gridrouter.utils.GridRouterRule.USER_3; import static ru.qatools.gridrouter.utils.GridRouterRule.hubUrl; public class RouteServletTest { @ClassRule public static GridRouterRule gridRouter = new GridRouterRule(); @Rule public HubEmulatorRule hub = new HubEmulatorRule( 8081); @Test(expected = WebDriverException.class, timeout = 10 * 1000) public void testRouteTimeout() { hub.emulate().newSessionFreeze(30); new RemoteWebDriver(hubUrl(gridRouter.baseUrl(USER_3)), firefox()); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/StatsServletTest.java ================================================ package ru.qatools.gridrouter; import org.junit.Rule; import org.junit.Test; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.RemoteWebDriver; import ru.qatools.gridrouter.sessions.BrowsersCountMap; import ru.qatools.gridrouter.utils.GridRouterRule; import ru.qatools.gridrouter.utils.HubEmulatorRule; import java.io.IOException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static ru.qatools.gridrouter.utils.GridRouterRule.*; import static ru.qatools.gridrouter.utils.HttpUtils.executeSimpleGet; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class StatsServletTest { @Rule public GridRouterRule gridRouter = new GridRouterRule(); @Rule public HubEmulatorRule hub = new HubEmulatorRule(8081); @Test public void testStats() throws IOException { assertThat(getActual(USER_1), is(empty())); hub.emulate().newSessions(1); hub.emulate().quit(); WebDriver driver = new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), firefox()); assertThat(getActual(USER_1), is(newCountMap("firefox", "32.0"))); driver.quit(); assertThat(getActual(USER_1), is(empty())); } @Test public void testStatsForDifferentUsers() throws IOException { hub.emulate().newSessions(1); new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), firefox()); assertThat(getActual(USER_1), is(newCountMap("firefox", "32.0"))); assertThat(getActual(USER_2), is(empty())); } @Test public void testEvictionOfOldSession() throws Exception { hub.emulate().newSessions(1); new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), firefox()); Thread.sleep(1000); assertThat(getActual(USER_1), is(newCountMap("firefox", "32.0"))); Thread.sleep(6000); assertThat(getActual(USER_1), is(empty())); } @Test public void testActiveSessionIsNotEvicted() throws Exception { hub.emulate().newSessions(1).navigation(); WebDriver driver = new RemoteWebDriver(hubUrl(gridRouter.baseUrlWithAuth), firefox()); for (int i = 0; i < 3; i++) { Thread.sleep(2000); driver.getCurrentUrl(); driver.get("http://yandex.ru"); } assertThat(getActual(USER_1), is(newCountMap("firefox", "32.0"))); } private BrowsersCountMap getActual(String user) throws IOException { return executeSimpleGet(gridRouter.baseUrl(user) + "/stats", BrowsersCountMap.class); } private BrowsersCountMap newCountMap(String browser, String version) { BrowsersCountMap expected = new BrowsersCountMap(); expected.increment(browser, version); return expected; } private BrowsersCountMap empty() { return new BrowsersCountMap(); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/caps/AppiumCapabilityProcessorTest.java ================================================ package ru.qatools.gridrouter.caps; import org.junit.Before; import org.junit.Test; import org.openqa.selenium.Platform; import org.openqa.selenium.remote.DesiredCapabilities; import ru.qatools.gridrouter.json.JsonCapabilities; import ru.qatools.gridrouter.utils.JsonUtils; import java.io.IOException; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertThat; public class AppiumCapabilityProcessorTest { private CapabilityProcessor processor; @Before public void setUp() throws Exception { processor = new AppiumCapabilityProcessor(); } @Test public void accept() throws Exception { assertThat(processor.accept(createCapabilities("", "iOS")), is(true)); assertThat(processor.accept(createCapabilities("blabla", "iOS")), is(false)); assertThat(processor.accept(createCapabilities("", "bla")), is(false)); assertThat(processor.accept(createCapabilities("bla", "iOS")), is(false)); } private JsonCapabilities createCapabilities(String browserName, String platformName) throws IOException { DesiredCapabilities desiredCapabilities = new DesiredCapabilities(browserName, "test", Platform.ANY); desiredCapabilities.setCapability("platformName", platformName); return JsonUtils.buildJsonCapabilities(desiredCapabilities); } @Test public void process() throws Exception { JsonCapabilities jsonCapabilities = new JsonCapabilities(); processor.process(jsonCapabilities); assertThat(jsonCapabilities.any().keySet(), contains("keepKeyChains")); assertThat(jsonCapabilities.any().get("keepKeyChains"), equalTo(true)); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/caps/CapabilityProcessorFactoryTest.java ================================================ package ru.qatools.gridrouter.caps; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import ru.qatools.gridrouter.json.JsonCapabilities; import static org.hamcrest.MatcherAssert.assertThat; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static org.openqa.selenium.remote.DesiredCapabilities.internetExplorer; import static ru.qatools.gridrouter.utils.JsonUtils.buildJsonCapabilities; import static org.hamcrest.Matchers.*; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:META-INF/spring/application-context.xml") public class CapabilityProcessorFactoryTest { @Autowired private CapabilityProcessorFactory factory; @Test public void testGetIEProcessor() throws Exception { JsonCapabilities ieCaps = buildJsonCapabilities(internetExplorer()); assertThat(factory.getProcessor(ieCaps), is(instanceOf(IECapabilityProcessor.class))); } @Test public void testGetDummyProcessor() throws Exception { JsonCapabilities firefoxCaps = buildJsonCapabilities(firefox()); assertThat(factory.getProcessor(firefoxCaps), is(instanceOf(DummyCapabilityProcessor.class))); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/caps/IECapabilityProcessorTest.java ================================================ package ru.qatools.gridrouter.caps; import org.json.JSONObject; import org.junit.Before; import org.junit.Test; import org.openqa.selenium.remote.DesiredCapabilities; import ru.qatools.gridrouter.json.JsonCapabilities; import ru.qatools.gridrouter.json.JsonMessage; import ru.qatools.gridrouter.json.Proxy; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static org.openqa.selenium.Proxy.ProxyType.DIRECT; import static org.openqa.selenium.remote.BrowserType.IE; import static org.openqa.selenium.remote.CapabilityType.PROXY; import static org.openqa.selenium.remote.DesiredCapabilities.firefox; import static org.openqa.selenium.remote.DesiredCapabilities.internetExplorer; import static ru.qatools.gridrouter.utils.JsonUtils.buildJsonCapabilities; import static ru.qatools.gridrouter.utils.JsonUtils.buildJsonMessage; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class IECapabilityProcessorTest { private IECapabilityProcessor processor; @Before public void setUp() throws Exception { processor = new IECapabilityProcessor(); } @Test public void testAccept() throws Exception { assertThat(processor.accept(buildJsonCapabilities(internetExplorer())), is(true)); assertThat(processor.accept(buildJsonCapabilities(firefox())), is(false)); } @Test public void testAddProxy() throws Exception { String version = "11"; JsonCapabilities capabilities = buildJsonCapabilities(internetExplorer(), version); processor.process(capabilities); assertThat(capabilities.getBrowserName(), is(equalTo(IE))); assertThat(capabilities.getVersion(), is(equalTo(version))); assertThat(capabilities.any().get(PROXY), is(notNullValue())); assertThat(((Proxy) capabilities.any().get(PROXY)).getProxyType(), is(equalTo(DIRECT.name()))); } @Test public void testJsonMarshalling() throws Exception { JsonMessage message = buildJsonMessage(internetExplorer()); processor.process(message.getDesiredCapabilities()); String proxyType = (String) new JSONObject(message.toJson()) .getJSONObject("desiredCapabilities") .getJSONObject("proxy") .get("proxyType"); assertThat(proxyType, is(equalTo(DIRECT.name()))); } @Test public void testExistingProxyIsNotOverridden() throws Exception { DesiredCapabilities caps = internetExplorer(); org.openqa.selenium.Proxy proxy = new org.openqa.selenium.Proxy(); proxy.setHttpProxy(PROXY); caps.setCapability(PROXY, proxy); JsonCapabilities capabilities = buildJsonCapabilities(caps); processor.process(capabilities); assertThat(capabilities.any().get(PROXY), not(instanceOf(Proxy.class))); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/json/JsonMessageTest.java ================================================ package ru.qatools.gridrouter.json; import com.fasterxml.jackson.core.JsonProcessingException; import org.json.JSONObject; import org.junit.Test; import java.io.IOException; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static ru.qatools.gridrouter.json.WithErrorMessage.DEFAULT_ERROR_MESSAGE; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class JsonMessageTest { @Test public void testProperJson() throws IOException { JSONObject jsonObject = new JSONObject(); jsonObject.put("status", 69); jsonObject.put("sessionId", "session id"); jsonObject.put("some other key", "some other value"); JSONObject capabilitiesObject = new JSONObject(); capabilitiesObject.put("browserName", "firefox"); capabilitiesObject.put("version", "32.0"); capabilitiesObject.put("some capability key", "some capability value"); jsonObject.put("desiredCapabilities", capabilitiesObject); JSONObject valueObject = new JSONObject(); valueObject.put("message", "some error message"); valueObject.put("some value key", "some value value"); jsonObject.put("value", valueObject); JsonMessage jsonMessage = JsonMessageFactory.from(jsonObject.toString()); assertThat(jsonMessage.getStatus(), is(69)); assertThat(jsonMessage.getSessionId(), is("session id")); assertThat(jsonMessage.any().get("some other key"), is("some other value")); JsonCapabilities jsonCapabilities = jsonMessage.getDesiredCapabilities(); assertThat(jsonCapabilities.getBrowserName(), is("firefox")); assertThat(jsonCapabilities.getVersion(), is("32.0")); assertThat(jsonCapabilities.any().get("some capability key"), is("some capability value")); assertThat(jsonMessage.getErrorMessage(), is("some error message")); } @Test public void testJsonWithKeysMissing() throws IOException { JSONObject jsonObject = new JSONObject(); jsonObject.put("status", 69); JsonMessage jsonMessage = JsonMessageFactory.from(jsonObject.toString()); assertThat(jsonMessage.getStatus(), is(69)); assertThat(jsonMessage.getSessionId(), is(nullValue())); assertThat(jsonMessage.getDesiredCapabilities(), is(nullValue())); } @Test public void testErrorMessageForNullValue() throws IOException { JSONObject jsonObject = new JSONObject(); JsonMessage jsonMessage = JsonMessageFactory.from(jsonObject.toString()); assertThat(jsonMessage.getErrorMessage(), is(DEFAULT_ERROR_MESSAGE)); } @Test public void testNullErrorMessageForPresentValue() throws IOException { JSONObject jsonObject = new JSONObject(); jsonObject.put("value", new JSONObject()); JsonMessage jsonMessage = JsonMessageFactory.from(jsonObject.toString()); assertThat(jsonMessage.getErrorMessage(), is(DEFAULT_ERROR_MESSAGE)); } @Test public void testValueOfSimpleType() throws IOException { String jsonRaw = "{" + "\"using\":\"xpath\"," + "\"value\":\"//lol[foo='bar']\"" + "}"; JsonMessage jsonMessage = JsonMessageFactory.from(jsonRaw); assertThat(jsonMessage.getSessionId(), is(nullValue())); assertThat(jsonMessage.any().get("value"), is("//lol[foo='bar']")); } @Test public void testJsonView() throws JsonProcessingException { JsonMessage jsonMessage = new JsonMessage(); jsonMessage.setSessionId("session id"); jsonMessage.setStatus(69); JsonCapabilities jsonCapabilities = new JsonCapabilities(); jsonCapabilities.setBrowserName("browser name"); jsonCapabilities.setVersion("browser version"); jsonMessage.setDesiredCapabilities(jsonCapabilities); jsonMessage.set("some key", "some value"); JSONObject jsonObject = new JSONObject(jsonMessage.toJson()); assertThat(jsonObject.getString("sessionId"), is("session id")); assertThat(jsonObject.getInt("status"), is(69)); JSONObject capabilitiesObject = jsonObject.getJSONObject("desiredCapabilities"); assertThat(capabilitiesObject.get("browserName"), is("browser name")); assertThat(capabilitiesObject.get("version"), is("browser version")); assertThat(jsonObject.isNull("value"), is(true)); assertThat(jsonObject.isNull("message"), is(true)); assertThat(jsonObject.isNull("errorMessage"), is(true)); } @Test public void testSettingErrorMessage() throws JsonProcessingException { JsonMessage jsonMessage = JsonMessageFactory.error(69, "some error message"); JSONObject jsonObject = new JSONObject(jsonMessage.toJson()); assertThat(jsonObject.getInt("status"), is(69)); assertThat(jsonObject.getJSONObject("value").getString("message"), is("some error message")); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/sessions/MemoryStatsCounterTest.java ================================================ package ru.qatools.gridrouter.sessions; import com.fasterxml.jackson.core.JsonProcessingException; import org.junit.Before; import org.junit.Test; import java.time.Duration; import java.util.HashSet; import java.util.Set; import static java.time.Duration.ZERO; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; import static ru.qatools.gridrouter.json.JsonFormatter.toJson; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class MemoryStatsCounterTest { private MemoryStatsCounter storage; @Before public void setUp() throws Exception { storage = new MemoryStatsCounter(); } @Test public void testEmptyStorage() throws Exception { assertThat(countJsonFor("user"), is("{}")); assertThat(expiredSessions(ZERO), is(empty())); assertThat(expiredSessions(Duration.ofDays(1)), is(empty())); } @Test public void testAddSession() throws Exception { storage.startSession("session1", "user", "firefox", "33"); storage.startSession("session2", "user", "firefox", "33"); storage.startSession("session3", "user", "firefox", "33"); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":3}}")); assertThat(storage.getSessionsCountForUser("user"), is(3)); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "33"), is(3)); storage.startSession("session1", "user", "firefox", "33"); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":3}}")); assertThat(storage.getSessionsCountForUser("user"), is(3)); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "33"), is(3)); } @Test public void testDifferentBrowsers() throws Exception { storage.startSession("session1", "user", "chrome", "33"); storage.startSession("session2", "user", "firefox", "33"); storage.startSession("session3", "user", "firefox", "33"); assertThat(countJsonFor("user"), is("{\"chrome\":{\"33\":1},\"firefox\":{\"33\":2}}")); assertThat(storage.getSessionsCountForUser("user"), is(3)); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "33"), is(2)); assertThat(storage.getSessionsCountForUserAndBrowser("user", "chrome", "33"), is(1)); } @Test public void testDifferentVersions() throws Exception { storage.startSession("session1", "user", "firefox", "33"); storage.startSession("session2", "user", "firefox", "34"); storage.startSession("session3", "user", "firefox", "34"); storage.startSession("session4", "user", "firefox", "firefox"); storage.startSession("session5", "user", "firefox", "firefox"); storage.startSession("session6", "user", "firefox", "firefox"); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":1,\"34\":2,\"firefox\":3}}")); assertThat(storage.getSessionsCountForUser("user"), is(6)); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "33"), is(1)); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "34"), is(2)); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "firefox"), is(3)); } @Test public void testRemoveExistingSession() throws Exception { storage.startSession("session1", "user", "firefox", "33"); storage.startSession("session2", "user", "firefox", "33"); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":2}}")); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "33"), is(2)); storage.deleteSession("session1"); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":1}}")); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "33"), is(1)); storage.deleteSession("session1"); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":1}}")); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "33"), is(1)); storage.deleteSession("session2"); assertThat(countJsonFor("user"), is("{}")); assertThat(storage.getSessionsCountForUserAndBrowser("user", "firefox", "33"), is(0)); } @Test public void testRemoveNotExistingSession() throws Exception { storage.deleteSession("session1"); storage.startSession("session1", "user", "firefox", "33"); storage.startSession("session2", "user", "firefox", "33"); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":2}}")); storage.deleteSession("session4"); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":2}}")); } @Test public void testMultipleUsers() throws Exception { storage.startSession("session1", "user1", "firefox", "33"); storage.startSession("session2", "user2", "firefox", "33"); storage.startSession("session3", "user2", "firefox", "33"); assertThat(countJsonFor("user1"), is("{\"firefox\":{\"33\":1}}")); assertThat(countJsonFor("user2"), is("{\"firefox\":{\"33\":2}}")); storage.deleteSession("session1"); storage.deleteSession("session2"); assertThat(countJsonFor("user1"), is("{}")); assertThat(countJsonFor("user2"), is("{\"firefox\":{\"33\":1}}")); } @Test public void testNewSessionsAreNotExpired() throws Exception { storage.startSession("session1", "user", "firefox", "33"); storage.startSession("session2", "user", "firefox", "33"); assertThat(expiredSessions(1000), is(empty())); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":2}}")); } @Test public void testOldSessionsAreExpired() throws Exception { storage.startSession("session1", "user", "firefox", "33"); storage.startSession("session2", "user", "firefox", "33"); Thread.sleep(500); storage.startSession("session3", "user", "firefox", "33"); assertThat(expiredSessions(250), containsInAnyOrder("session1", "session2")); assertThat(countJsonFor("user"), is("{\"firefox\":{\"33\":1}}")); Thread.sleep(500); assertThat(expiredSessions(250), contains("session3")); assertThat(countJsonFor("user"), is("{}")); } @Test public void testUpdateExistingSession() throws Exception { storage.startSession("session1", "user", "firefox", "33"); Thread.sleep(500); storage.updateSession("session1"); assertThat(expiredSessions(250), is(empty())); } @Test public void testMultipleUsersExpiration() throws Exception { storage.startSession("session1", "user1", "firefox", "33"); Thread.sleep(500); storage.startSession("session2", "user2", "firefox", "33"); assertThat(expiredSessions(250), contains("session1")); assertThat(countJsonFor("user1"), is("{}")); assertThat(countJsonFor("user2"), is("{\"firefox\":{\"33\":1}}")); } private String countJsonFor(String user) throws JsonProcessingException { return toJson(storage.getStats(user)); } public Set expiredSessions(int millis) { return expiredSessions(Duration.ofMillis(millis)); } public Set expiredSessions(Duration duration) { final Set removedSessionIds = new HashSet<>(storage.getActiveSessions()); storage.expireSessionsOlderThan(duration); removedSessionIds.removeAll(storage.getActiveSessions()); return removedSessionIds; } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/sessions/WaitAvailableBrowsersCheckerTest.java ================================================ package ru.qatools.gridrouter.sessions; import org.junit.Before; import org.junit.Test; import ru.qatools.gridrouter.config.Version; import java.time.Duration; import java.time.temporal.Temporal; import static java.time.ZonedDateTime.now; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.lessThan; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.*; /** * @author Ilya Sadykov */ public class WaitAvailableBrowsersCheckerTest { WaitAvailableBrowsersChecker checker; Version version; StatsCounter counter; @Before public void setUp() throws Exception { counter = mock(StatsCounter.class); checker = new WaitAvailableBrowsersChecker(3, 1, counter); version = new Version(); version.setPermittedCount(10); version.setNumber("33"); when(counter.getSessionsCountForUserAndBrowser(eq("user"), eq("firefox"), eq("33"))).thenReturn(10); } @Test public void testWaitAvailableBrowsersChecker() throws Exception { Temporal started = now(); try { checker.ensureFreeBrowsersAvailable("user", "host", "firefox", version); } catch (WaitAvailableBrowserTimeoutException e) { // do nothing } verify(counter, times(3)).getSessionsCountForUserAndBrowser(eq("user"), eq("firefox"), eq("33")); assertThat(Duration.between(started, now()).toMillis(), greaterThanOrEqualTo(3000L)); } @Test(expected = WaitAvailableBrowserTimeoutException.class) public void testWaitAvailableBrowsersTimeout() throws Exception { checker.ensureFreeBrowsersAvailable("user", "host", "firefox", version); } @Test public void testNoWaitAvailableBrowser() throws Exception { when(counter.getSessionsCountForUserAndBrowser(eq("user"), eq("firefox"), eq("33"))).thenReturn(5); Temporal started = now(); checker.ensureFreeBrowsersAvailable("user", "host", "firefox", version); verify(counter, times(1)).getSessionsCountForUserAndBrowser(eq("user"), eq("firefox"), eq("33")); assertThat(Duration.between(started, now()).toMillis(), lessThan(1000L)); verifyNoMoreInteractions(counter); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/FindElementCallback.java ================================================ package ru.qatools.gridrouter.utils; import org.json.JSONObject; import org.mockserver.mock.action.ExpectationCallback; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import static org.mockserver.model.HttpResponse.response; /** * Sets the element id (according to protocol specification) * to the hashcode of the selector. This way we can check that * the selector was passed through proxy correctly. * * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class FindElementCallback implements ExpectationCallback { @Override public HttpResponse handle(HttpRequest httpRequest) { JSONObject jsonObject = new JSONObject(httpRequest.getBodyAsString()); String selector = jsonObject.get("value").toString(); JSONObject responce = new JSONObject(); responce.put("status", 0); JSONObject value = new JSONObject(); value.put("ELEMENT", selector.hashCode()); responce.put("value", value); return response(responce.toString()).withStatusCode(500); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/GridRouterRule.java ================================================ package ru.qatools.gridrouter.utils; import org.eclipse.jetty.security.HashLoginService; import org.eclipse.jetty.util.security.Password; import java.net.MalformedURLException; import java.net.URL; import static java.util.UUID.randomUUID; import static ru.qatools.gridrouter.utils.SocketUtil.findFreePort; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class GridRouterRule extends JettyRule { public static final String USER_1 = "user1"; public static final String USER_2 = "user2"; public static final String USER_3 = "user3"; public static final String USER_4 = "user4"; public static final String PASSWORD = "password"; public static final String ROLE = "user"; public final String baseUrl; public final String baseUrlWithAuth; public final String baseUrlWrongPassword; public GridRouterRule() { super( "/", "src/main/webapp", "target/classes", findFreePort(), new HashLoginService() {{ setName("Selenium Grid Router"); putUser(USER_1, new Password(PASSWORD), new String[]{ROLE}); putUser(USER_2, new Password(PASSWORD), new String[]{ROLE}); putUser(USER_3, new Password(PASSWORD), new String[]{ROLE}); putUser(USER_4, new Password(PASSWORD), new String[]{ROLE}); }} ); baseUrl = "http://localhost:" + getPort(); baseUrlWithAuth = baseUrl(USER_1); baseUrlWrongPassword = baseUrl(USER_1, randomUUID().toString()); } public static URL hubUrl(String baseUrl) { try { return new URL(baseUrl + "/wd/hub"); } catch (MalformedURLException e) { throw new RuntimeException(e); } } public String baseUrl(String user) { return baseUrl(user, PASSWORD); } public String baseUrl(String user, String password) { return String.format("http://%s:%s@localhost:%d", user, password, getPort()); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/HttpUtils.java ================================================ package ru.qatools.gridrouter.utils; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.HttpClientBuilder; import java.io.IOException; import java.io.InputStream; /** * @author Dmitry Baev charlie@yandex-team.ru * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public final class HttpUtils { private HttpUtils() { } public static T executeSimpleGet(String url, Class clazz) throws IOException { CloseableHttpResponse execute = HttpClientBuilder .create().build() .execute(new HttpGet(url)); InputStream content = execute.getEntity().getContent(); return new ObjectMapper().readValue(content, clazz); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/HubEmulator.java ================================================ package ru.qatools.gridrouter.utils; import org.json.JSONObject; import org.mockserver.integration.ClientAndServer; import org.mockserver.matchers.Times; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import java.util.concurrent.TimeUnit; import static java.util.UUID.randomUUID; import static org.mockserver.integration.ClientAndServer.startClientAndServer; import static org.mockserver.matchers.Times.once; import static org.mockserver.model.HttpCallback.callback; import static org.mockserver.model.HttpRequest.request; import static org.mockserver.model.HttpResponse.response; import static org.mockserver.verify.VerificationTimes.exactly; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class HubEmulator { private static final String WD_HUB_SESSION = "/wd/hub/session"; private static final String SESSION_ID_REGEX = "[-a-zA-Z0-9]{36}"; private ClientAndServer hub; public HubEmulator(int hubPort) { hub = startClientAndServer(hubPort); } public HubEmulations emulate() { return new HubEmulations(); } public HubVerifications verify() { return new HubVerifications(); } public void stop() { hub.stop(); } public class HubEmulations { public HubEmulations newSessions(int sessionsCount) { for (int i = 0; i < sessionsCount; i++) { hub.when(newSessionRequest(), once()).respond(newSessionSuccessful()); } return this; } public HubEmulations newSessionFailures(int times) { return newSessionFailures(Times.exactly(times)); } public HubEmulations newSessionFailures(Times times) { hub.when(newSessionRequest(), times).respond(newSessionFailed()); return this; } public HubEmulations newSessionFreeze(int seconds) { hub.when(newSessionRequest(), once()).respond( response() .withDelay(TimeUnit.SECONDS, seconds) .withStatusCode(500) ); return this; } public HubEmulations navigation() { hub.when(sessionRequest("url")) .callback(callback().withCallbackClass( RememberUrlCallback.class.getCanonicalName())); return this; } public HubEmulations findElement() { hub.when(sessionRequest("element").withMethod("POST")) .callback(callback().withCallbackClass( FindElementCallback.class.getCanonicalName())); return this; } public HubEmulations quit() { hub.when(sessionQuitRequest()).respond(emptyResponse()); return this; } } public class HubVerifications { public HubVerifications newSessionRequestsCountIs(int sessionsCount) { hub.verify(newSessionRequest(), exactly(sessionsCount)); return this; } public HubVerifications quitRequestsCountIs(int times) { hub.verify(sessionQuitRequest(), exactly(times)); return this; } public HubVerifications totalRequestsCountIs(int times) { hub.verify(request(".*"), exactly(times)); return this; } } private static HttpRequest newSessionRequest() { return request(WD_HUB_SESSION).withMethod("POST"); } private static HttpRequest sessionRequest(String handler) { return request(WD_HUB_SESSION + "/" + SESSION_ID_REGEX + "/" + handler); } private static HttpRequest sessionQuitRequest() { return request(WD_HUB_SESSION +"/.*").withMethod("DELETE"); } private HttpResponse emptyResponse() { JSONObject json = new JSONObject(); json.put("value", new JSONObject()); return response(json.toString()); } private static HttpResponse newSessionSuccessful() { JSONObject json = new JSONObject(); json.put("value", new JSONObject()); json.put("sessionId", randomUUID()); return response(json.toString()); } private static HttpResponse newSessionFailed() { JSONObject json = new JSONObject(); json.put("status", 6); JSONObject value = new JSONObject(); value.put("message", "unable to start browser"); json.put("value", value); return response(json.toString()).withStatusCode(500); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/HubEmulatorRule.java ================================================ package ru.qatools.gridrouter.utils; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.function.Consumer; import static ru.qatools.gridrouter.utils.SocketUtil.findFreePort; import static ru.qatools.gridrouter.utils.TestConfigRepository.changePort; import static ru.qatools.gridrouter.utils.TestConfigRepository.resetConfig; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public class HubEmulatorRule extends TestWatcher { static final Logger LOGGER = LoggerFactory.getLogger(HubEmulatorRule.class); private int fromPort; private int port; private HubEmulator hub; public HubEmulatorRule(int fromPort) { this(fromPort, hub -> { }); } public HubEmulatorRule(int fromPort, Consumer initializer) { this.fromPort = fromPort; port = findFreePort(); LOGGER.info("Selected new free port {}, starting emulator...", port); hub = new HubEmulator(port); if (initializer != null) { LOGGER.info("Running initializer..."); try { initializer.accept(hub); } catch (Exception e) { throw new RuntimeException(e); } } LOGGER.info("Waiting for config initialization..."); changePort(fromPort, port); } @Override protected void finished(Description description) { resetConfig(); hub.stop(); } public HubEmulator.HubEmulations emulate() { return hub.emulate(); } public HubEmulator.HubVerifications verify() { return hub.verify(); } public int getPort() { return port; } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/JettyRule.java ================================================ package ru.qatools.gridrouter.utils; import org.eclipse.jetty.annotations.AnnotationConfiguration; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.webapp.Configuration; import org.eclipse.jetty.webapp.WebAppContext; import org.eclipse.jetty.webapp.WebInfConfiguration; import org.eclipse.jetty.webapp.WebXmlConfiguration; import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; public class JettyRule implements TestRule { private final String contextPath; private final String classPath; private final String warPath; private final int port; private Server server; private Object[] beans; public JettyRule(String contextPath, String warPath, String classPath, int port, Object... beans) { this.contextPath = contextPath; this.classPath = classPath; this.warPath = warPath; this.port = port; this.beans = beans; } @Override public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { before(); try { base.evaluate(); } finally { after(); } } }; } protected void before() throws Exception { WebAppContext context = new WebAppContext(); context.setResourceBase(warPath); context.setExtraClasspath(classPath); context.setContextPath(contextPath); context.setParentLoaderPriority(true); context.setConfigurations(new Configuration[]{ new AnnotationConfiguration(), new WebXmlConfiguration(), new WebInfConfiguration() }); server = new Server(port); server.setHandler(context); for (Object bean : beans) { server.addBean(bean); } server.start(); } protected void after() throws Exception { server.stop(); } public int getPort() { return port; } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/JsonUtils.java ================================================ package ru.qatools.gridrouter.utils; import org.json.JSONObject; import org.openqa.selenium.remote.DesiredCapabilities; import ru.qatools.gridrouter.json.JsonCapabilities; import ru.qatools.gridrouter.json.JsonMessage; import ru.qatools.gridrouter.json.JsonMessageFactory; import java.io.IOException; import java.util.Map; import static org.openqa.selenium.remote.CapabilityType.BROWSER_NAME; import static org.openqa.selenium.remote.CapabilityType.PLATFORM; import static org.openqa.selenium.remote.CapabilityType.PROXY; import static org.openqa.selenium.remote.CapabilityType.VERSION; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public final class JsonUtils { private JsonUtils() { } public static JsonCapabilities buildJsonCapabilities(DesiredCapabilities capabilities) throws IOException { return buildJsonMessage(capabilities).getDesiredCapabilities(); } public static JsonCapabilities buildJsonCapabilities(DesiredCapabilities capabilities, String version) throws IOException { capabilities.setVersion(version); return buildJsonMessage(capabilities).getDesiredCapabilities(); } public static JsonMessage buildJsonMessage(DesiredCapabilities capabilities) throws IOException { JSONObject capabilitiesObject = new JSONObject(); Map capabilitiesMap = capabilities.asMap(); capabilitiesMap.keySet().forEach(k -> capabilitiesObject.put(k, capabilitiesMap.get(k))); JSONObject jsonObject = new JSONObject(); jsonObject.put("desiredCapabilities", capabilitiesObject); return JsonMessageFactory.from(jsonObject.toString()); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/MatcherUtils.java ================================================ package ru.qatools.gridrouter.utils; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.hamcrest.TypeSafeMatcher; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import static ru.qatools.gridrouter.utils.GridRouterRule.hubUrl; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public final class MatcherUtils { private MatcherUtils() { } /** * Creates a matcher that tries to obtain a browser * for a user that it is matched against. * * @return A matcher instance that creates a new webdriver * on {@link Matcher#matches(Object) matches()} method invocation. * * @param browser capabilities for the browser to obtain */ public static Matcher canObtain(final GridRouterRule gridRouter, final DesiredCapabilities browser) { return new TypeSafeMatcher() { private Exception exception; @Override protected boolean matchesSafely(String user) { try { new RemoteWebDriver(hubUrl(gridRouter.baseUrl(user)), browser); return true; } catch (Exception e) { exception = e; } return false; } @Override public void describeTo(Description description) { description.appendText("not able to obtain browser because of ") .appendValue(exception.toString()); } }; } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/QuotaUtils.java ================================================ package ru.qatools.gridrouter.utils; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SerializationUtils; import ru.qatools.gridrouter.config.Browsers; import javax.xml.bind.JAXB; import java.io.File; import java.io.StringWriter; import static java.lang.ClassLoader.getSystemResource; import static ru.qatools.gridrouter.utils.GridRouterRule.USER_1; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru */ public final class QuotaUtils { public static final String QUOTA_FILE_PATTERN = getSystemResource("quota/" + USER_1 + ".xml").getPath().replace(USER_1, "%s"); private QuotaUtils() { } public static void replacePortInQuotaFile(String user, int port) { replacePortInQuotaFile(user, 0, 0, port); } public static void replacePortInQuotaFile(String user, int regionNum, int hostNum, int port) { copyQuotaFile(user, user, regionNum, hostNum, port); } public static void copyQuotaFile(String srcUser, String dstUser, int regionNum, int hostNum, int withHubPort) { Browsers browsers = getQuotaFor(srcUser); setPort(browsers, regionNum, hostNum, withHubPort); writeQuotaFor(dstUser, browsers); } public static Browsers getQuotaFor(String user) { File quotaFile = getQuotaFile(user); Browsers browsersOriginal = JAXB.unmarshal(quotaFile, Browsers.class); return SerializationUtils.clone(browsersOriginal); } public static synchronized void writeQuotaFor(String user, Browsers browsers) { try { //workaround to write the whole file at once StringWriter xml = new StringWriter(); JAXB.marshal(browsers, xml); final File fileToWrite = getQuotaFile(user); final File tmpFile = File.createTempFile(user, "xml"); FileUtils.write(tmpFile, xml.toString()); FileUtils.copyFile(tmpFile, fileToWrite); FileUtils.deleteQuietly(tmpFile); } catch (Exception e) { throw new RuntimeException(e); } } public static File getQuotaFile(String user) { return new File(String.format(QUOTA_FILE_PATTERN, user)); } @SuppressWarnings("ResultOfMethodCallIgnored") public static void deleteQuotaFile(String user) { getQuotaFile(user).delete(); } public static void setPort(Browsers browsers, int regionNum, int hostNumber, int port) { browsers.getBrowsers().get(0) .getVersions().get(0) .getRegions().get(regionNum) .getHosts().get(hostNumber) .setPort(port); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/RememberUrlCallback.java ================================================ package ru.qatools.gridrouter.utils; import org.json.JSONObject; import org.mockserver.mock.action.ExpectationCallback; import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; import static org.mockserver.model.HttpResponse.response; /** * @author Innokenty Shuvalov innokenty@yandex-team.ru * @author Dmitry Baev charlie@yandex-team.ru */ public class RememberUrlCallback implements ExpectationCallback { private static String currentUrl = "{\"value\":\"\"}"; @Override public HttpResponse handle(HttpRequest httpRequest) { if (httpRequest.getMethod().toString().contains("POST")) { JSONObject jsonObject = new JSONObject(httpRequest.getBodyAsString()); currentUrl = jsonObject.get("url").toString(); return response(); } else if (httpRequest.getMethod().toString().contains("GET")) { return response(currentUrl); } return response("invalid request!").withStatusCode(400); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/SocketUtil.java ================================================ package ru.qatools.gridrouter.utils; import java.io.IOException; import java.net.ServerSocket; public enum SocketUtil { ; /** * Returns a free port number on localhost. * Heavily inspired from org.eclipse.jdt.launching.SocketUtil (to avoid a dependency to JDT just because of this). * Slightly improved with close() missing in JDT. And throws exception instead of returning -1. * * @return a free port number on localhost * @throws IllegalStateException if unable to find a free port */ public static int findFreePort() { ServerSocket socket = null; try { socket = new ServerSocket(0); socket.setReuseAddress(true); int port = socket.getLocalPort(); try { socket.close(); } catch (IOException ignored) { // Ignore IOException on close() } return port; } catch (IOException ignored) { // Ignore IOException on open } finally { if (socket != null) { try { socket.close(); } catch (IOException ignored) { // Ignore IOException on close() } } } throw new IllegalStateException("Could not find a free TCP/IP port to start embedded Jetty HTTP Server on"); } } ================================================ FILE: proxy/src/test/java/ru/qatools/gridrouter/utils/TestConfigRepository.java ================================================ package ru.qatools.gridrouter.utils; import org.apache.commons.io.FilenameUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import ru.qatools.beanloader.BeanLoader; import ru.qatools.gridrouter.ConfigRepository; import ru.qatools.gridrouter.config.Browsers; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import static java.util.Collections.unmodifiableMap; import static javax.xml.bind.JAXB.marshal; import static javax.xml.bind.JAXB.unmarshal; /** * @author Ilya Sadykov */ public class TestConfigRepository implements ConfigRepository { protected static final String XML_GLOB = "*.xml"; private static final Logger LOGGER = LoggerFactory.getLogger(TestConfigRepository.class); private static Map initialBrowsers = new HashMap<>(); private static Map initialRoutes = new HashMap<>(); private static Map userBrowsers = new HashMap<>(); private static Map routes = new HashMap<>(); static { try { final Path quotaDir = Paths.get(TestConfigRepository.class.getClassLoader().getResource("quota").toURI()); LOGGER.debug("Loading quota configuration"); initialBrowsers = new HashMap<>(); initialRoutes = new HashMap<>(); BeanLoader.loadAll(Browsers.class, quotaDir, XML_GLOB, (path, quota) -> { String user = FilenameUtils.getBaseName(path.toString()); initialBrowsers.put(user, quota); initialRoutes.putAll(quota.getRoutesMap()); }); initialBrowsers = unmodifiableMap(initialBrowsers); initialRoutes = unmodifiableMap(initialRoutes); resetConfig(); } catch (IOException | URISyntaxException e) { LOGGER.error("Quota configuration loading failed", e); } } private static Browsers copy(Browsers quota) { StringWriter writer = new StringWriter(); marshal(quota, writer); return unmarshal(new StringReader(writer.toString()), Browsers.class); } public static synchronized void resetConfig() { userBrowsers.clear(); initialBrowsers.entrySet().forEach(e -> { userBrowsers.put(e.getKey(), copy(e.getValue())); }); routes.clear(); routes.putAll(initialRoutes); } public static synchronized void changePort(int from, int to) { userBrowsers.keySet().forEach(quotaName -> userBrowsers.get(quotaName).getBrowsers().forEach(browser -> browser.getVersions().forEach(version -> version.getRegions().forEach(region -> region.getHosts().forEach(host -> { if (host.getPort() == from) { LOGGER.info("Changing port of {} from {} to {} for user {}", host, from, to, quotaName); host.setPort(to); routes.putAll(userBrowsers.get(quotaName).getRoutesMap()); } }))))); } @Override public Map getQuotaMap() { return userBrowsers; } @Override public String getRoute(String routeId) { return routes.get(routeId); } } ================================================ FILE: proxy/src/test/resources/META-INF/spring/test-application-context.xml ================================================ ================================================ FILE: proxy/src/test/resources/application.properties ================================================ grid.config.quota.directory=classpath:quota grid.router.quota.repository=ru.qatools.gridrouter.utils.TestConfigRepository grid.config.quota.hotReload=true grid.router.evict.sessions.cron=* * * * * * grid.router.evict.sessions.timeout.seconds=5 grid.router.route.timeout.seconds=5 grid.router.host.selection.strategy=ru.qatools.gridrouter.config.RandomHostSelectionStrategy grid.router.stats.counter=ru.qatools.gridrouter.sessions.MemoryStatsCounter grid.router.available.browsers.checker=ru.qatools.gridrouter.sessions.SkipAvailableBrowsersChecker ================================================ FILE: proxy/src/test/resources/log4j.properties ================================================ # suppress inspection "UnusedProperty" for whole file log4j.rootLogger=INFO, out # CONSOLE appender not used by default log4j.appender.out=org.apache.log4j.ConsoleAppender log4j.appender.out.layout=org.apache.log4j.PatternLayout log4j.appender.out.layout.ConversionPattern=%d %-5p %c - %m%n log4j.throwableRenderer=org.apache.log4j.EnhancedThrowableRenderer log4j.logger.org.mockserver.mockserver.MockServerHandler=WARN log4j.logger.ru.qatools.gridrouter=DEBUG ================================================ FILE: proxy/src/test/resources/quota/user1.xml ================================================ ================================================ FILE: proxy/src/test/resources/quota/user2.xml ================================================ ================================================ FILE: proxy/src/test/resources/quota/user3.xml ================================================ ================================================ FILE: testing/group_vars/all.yml ================================================ workspace: "{{ ansible_env.PWD }}/target" ================================================ FILE: testing/ping-local-gridrouter.sh ================================================ #!/usr/bin/env bash curl http://boot2docker:$(docker ps | grep jetty | sed 's/.*0.0.0.0://' | sed 's/->.*//')/ping && echo ================================================ FILE: testing/roles/start/files/gridrouter/conf/application.properties ================================================ # suppress inspection "UnusedProperty" for whole file grid.config.quota.directory=classpath:quota grid.config.quota.hotReload=true grid.router.evict.sessions.cron=0 * * * * * grid.router.evict.sessions.timeout.seconds=120 ================================================ FILE: testing/roles/start/files/gridrouter/conf/quota/selenium.xml ================================================ ================================================ FILE: testing/roles/start/files/gridrouter/conf/users.properties ================================================ selenium:selenium, user ================================================ FILE: testing/roles/start/files/gridrouter/webapps/ROOT.xml ================================================ / /etc/gridrouter/war/ROOT.war /etc/gridrouter/conf/ Selenium Grid Router /etc/gridrouter/conf/users.properties ================================================ FILE: testing/roles/start/tasks/before.yml ================================================ - debug: msg="workspace {{ workspace }}" ================================================ FILE: testing/roles/start/tasks/main.yml ================================================ --- - include: before.yml - include: start-selenium.yml version="2.46.0" browser="firefox" - include: start-selenium.yml version="2.46.0" browser="chrome" - include: start-gridrouter.yml ================================================ FILE: testing/roles/start/tasks/start-gridrouter.yml ================================================ - name: copy configuration files copy: src=gridrouter dest={{ workspace }} - shell: "ls -d {{ ansible_env.PWD }}/../proxy/target/*.war" register: war_path - file: path={{ war_path.stdout }} state=file - file: path={{ workspace }}/gridrouter/war/ state=directory - copy: src={{ war_path.stdout }} dest={{ workspace }}/gridrouter/war/ROOT.war - name: start jetty with gridrouter docker: name: gridrouter image: jetty:9.3.2 expose: - "8080" ports: - "8080" links: - "chrome" - "firefox" volumes: - "{{ workspace }}/gridrouter/webapps:/var/lib/jetty/webapps" - "{{ workspace }}/gridrouter/conf:/etc/gridrouter/conf" - "{{ workspace }}/gridrouter/war:/etc/gridrouter/war" state: started ================================================ FILE: testing/roles/start/tasks/start-selenium.yml ================================================ - name: start selenium standalone with {{ browser }} docker: name: "{{ browser }}" image: selenium/standalone-{{ browser }}:{{ version }} expose: - "4444" state: started ================================================ FILE: testing/roles/stop/tasks/before.yml ================================================ - debug: msg="workspace {{ workspace }}" ================================================ FILE: testing/roles/stop/tasks/main.yml ================================================ --- - include: before.yml - include: stop-selenium.yml version="2.46.0" browser="firefox" - include: stop-selenium.yml version="2.46.0" browser="chrome" - include: stop-gridrouter.yml ================================================ FILE: testing/roles/stop/tasks/stop-gridrouter.yml ================================================ - name: stop jetty with gridrouter docker: name: gridrouter image: jetty:9.3.0-jre8 state: absent - name: delete workspace file: path={{ workspace }} state=absent ignore_errors: yes ================================================ FILE: testing/roles/stop/tasks/stop-selenium.yml ================================================ - name: stop selenium standalone with {{ browser }} docker: name: "{{ browser }}" image: selenium/standalone-{{ browser }}:{{ version }} expose: - "4444" state: absent ================================================ FILE: testing/roles/test/files/java/pom.xml ================================================ 4.0.0 ru.qatools.seleniumkit gridrouter-e2e-java 1.0-SNAPSHOT junit junit 4.11 test org.seleniumhq.selenium selenium-java 2.46.0 test ================================================ FILE: testing/roles/test/files/java/run.sh ================================================ #! /bin/sh mvn -f /code/pom.xml test ================================================ FILE: testing/roles/test/files/java/src/test/java/SeleniumTest.java ================================================ import org.junit.Test; import org.openqa.selenium.WebDriver; import org.openqa.selenium.remote.DesiredCapabilities; import org.openqa.selenium.remote.RemoteWebDriver; import java.net.URL; import static org.hamcrest.CoreMatchers.equalTo; import static org.junit.Assert.assertThat; public class SeleniumTest { @Test public void testConnection() throws Exception { URL url = new URL("http://selenium:selenium@gridrouter:8080/wd/hub"); DesiredCapabilities capabilities = DesiredCapabilities.firefox(); capabilities.setVersion("38.0"); WebDriver driver = new RemoteWebDriver(url, capabilities); driver.get("http://www.yandex.ru"); assertThat(driver.getTitle(), equalTo("Яндекс")); } } ================================================ FILE: testing/roles/test/files/js/config.json ================================================ { "baseUrl": "http://selenium:selenium@gridrouter:8080/wd/hub" } ================================================ FILE: testing/roles/test/files/js/fixtures/big-script.js ================================================ function prepareScreenshotUnsafe(selectors, opts) { var rect = getCaptureRect(selectors); if (rect.error) { return rect; } var viewportHeight = window.innerHeight || document.documentElement.clientHeight, viewportWidth = window.innerWidth || document.documentElement.clientWidth, documentHeight = document.documentElement.scrollHeight, documentWidth = document.documentElement.scrollWidth, coverage, viewPort = new Rect({ left: util.getScrollLeft(), top: util.getScrollTop(), width: viewportWidth, height: viewportHeight }); if (!viewPort.rectInside(rect)) { window.scrollTo(rect.left, rect.top); } if (opts.coverage) { coverage = require('./gemini.coverage').collectCoverage(rect); } return { viewportOffset: { top: util.getScrollTop(), left: util.getScrollLeft() }, captureArea: rect.serialize(), ignoreAreas: findIgnoreAreas(opts.ignoreSelectors), viewportHeight: Math.round(viewportHeight), documentHeight: Math.round(documentHeight), documentWidth: Math.round(documentWidth), coverage: coverage, canHaveCaret: isEditable(document.activeElement) }; } function getElementCaptureRect(element) { var pseudo = [':before', ':after'], css = window.getComputedStyle(element), clientRect = rect.getAbsoluteClientRect(element); if (isHidden(css, clientRect)) { return null; } var elementRect = getExtRect(css, clientRect); util.each(pseudo, function(pseudoEl) { css = window.getComputedStyle(element, pseudoEl); elementRect = elementRect.merge(getExtRect(css, clientRect)); }); return elementRect; } function getExtRect(css, clientRect) { var shadows = parseBoxShadow(css.boxShadow), outline = parseInt(css.outlineWidth, 10); if (isNaN(outline)) { outline = 0; } return adjustRect(clientRect, shadows, outline); } function parseBoxShadow(value) { value = value || ''; var regex = /[-+]?\d*\.?\d+px/g, values = value.split(','), results = [], match; util.each(values, function(value) { if ((match = value.match(regex))) { results.push({ offsetX: parseFloat(match[0]), offsetY: parseFloat(match[1]) || 0, blurRadius: parseFloat(match[2]) || 0, spreadRadius: parseFloat(match[3]) || 0, inset: value.indexOf('inset') !== -1 }); } }); return results; } function adjustRect(rect, shadows, outline) { var shadowRect = calculateShadowRect(rect, shadows), outlineRect = calculateOutlineRect(rect, outline); return shadowRect.merge(outlineRect); } function calculateOutlineRect(rect, outline) { return new Rect({ top: Math.max(0, rect.top - outline), left: Math.max(0, rect.left - outline), bottom: rect.bottom + outline, right: rect.right + outline }); } function calculateShadowRect(rect, shadows) { var extent = calculateShadowExtent(shadows); return new Rect({ left: Math.max(0, rect.left + extent.left), top: Math.max(0, rect.top + extent.top), width: rect.width - extent.left + extent.right, height: rect.height - extent.top + extent.bottom }); } function calculateShadowExtent(shadows) { var result = {top: 0, left: 0, right: 0, bottom: 0}; util.each(shadows, function(shadow) { if (shadow.inset) { //skip inset shadows return; } var blurAndSpread = shadow.spreadRadius + shadow.blurRadius; result.left = Math.min(shadow.offsetX - blurAndSpread, result.left); result.right = Math.max(shadow.offsetX + blurAndSpread, result.right); result.top = Math.min(shadow.offsetY - blurAndSpread, result.top); result.bottom = Math.max(shadow.offsetY + blurAndSpread, result.bottom); }); return result; } function isEditable(element) { if (!element) { return false; } return /^(input|textarea)$/i.test(element.tagName) || element.isContentEditable; } return 'ok'; ================================================ FILE: testing/roles/test/files/js/package.json ================================================ { "name": "wd-test", "version": "1.0.0", "description": "webdriver test", "main": "index.js", "directories": { "test": "test" }, "devDependencies": { "mocha": "^2.2.5", "wd": "^0.3.12", "webdriver-http-sync": "^1.1.0" }, "scripts": { "test": "mocha --reporter xunit --reporter-options output=target/surefire-reports/js-selenium-test.xml" }, "author": "", "license": "ISC" } ================================================ FILE: testing/roles/test/files/js/run.sh ================================================ #! /bin/sh cd /code npm install npm test ================================================ FILE: testing/roles/test/files/js/test/selenium-test-sync.js ================================================ var assert = require('assert'), config = require('../config.json'), WebDriver = require('webdriver-http-sync'); describe('"webdriver-http-sync" selenium client for js', function () { it('should work', function () { this.timeout(60000); var driver = new WebDriver(config.baseUrl, { browserName: 'firefox', version: '38' }); driver.navigateTo('http://yandex.ru'); var title = driver.getPageTitle(); assert.equal(title, 'Яндекс'); driver.close(); }); }); ================================================ FILE: testing/roles/test/files/js/test/selenium-test-wd.js ================================================ 'use strict'; var fs = require('fs'), assert = require('assert'), config = require('../config.json'), wd = require('wd'); describe('"wd" selenium client for js', function () { var driver; beforeEach(function() { this.timeout(60000); return driver = wd.promiseChainRemote(config.baseUrl) .init({ browserName: 'chrome', version: '43' }) .get('http://yandex.ru'); }); it('should work', function () { return driver.title().then(function (title) { assert.equal(title, 'Яндекс'); }) .fin(function () { return driver.quit(); }); }); it('should push and evaluate big scripts', function() { return driver.execute(fs.readFileSync(__dirname + '/../fixtures/big-script.js', 'utf-8')).then(function(result) { assert.equal(result, 'ok'); }).fin(function() { return driver.quit(); }); }) }); ================================================ FILE: testing/roles/test/files/python/requirements.txt ================================================ selenium==2.46.0 pytest==2.7.1 ================================================ FILE: testing/roles/test/files/python/run.sh ================================================ #! /bin/sh pip install -r /code/requirements.txt py.test --junitxml=/code/target/surefire-reports/test_selenium.xml -q /code/src/test_selenium.py ================================================ FILE: testing/roles/test/files/python/src/test_selenium.py ================================================ from selenium import webdriver from selenium.webdriver.common.desired_capabilities import DesiredCapabilities class TestSelenium: def test_selenium(self): driver = webdriver.Remote( command_executor='http://selenium:selenium@gridrouter:8080/wd/hub', desired_capabilities=DesiredCapabilities.CHROME) driver.get('http://www.yandex.ru') assert driver.title != '' driver.close() ================================================ FILE: testing/roles/test/tasks/after.yml ================================================ - name: "copy report files" copy: src={{ workspace }}/report/ dest={{ ansible_env.PWD }}/target/surefire-reports ================================================ FILE: testing/roles/test/tasks/before.yml ================================================ - debug: msg="workspace {{ workspace }}" ================================================ FILE: testing/roles/test/tasks/main.yml ================================================ --- - include: before.yml - include: run-tests.yml language="python" image="python:2.7" - include: run-tests.yml language="java" image="maven:3.3-jdk-8" - include: after.yml ================================================ FILE: testing/roles/test/tasks/run-tests.yml ================================================ - name: "{{ language }} gathering facts" set_fact: name: "{{ language }}_tests" project: "{{ workspace }}/{{ language }}" - name: "{{ language }} | copy project files" copy: src={{ language }} dest={{ workspace }} - name: "{{ language }} | grant script execution privileges" file: path={{ workspace }}/{{ language }}/run.sh mode=u+x - name: "{{ language }} | start container" docker: name: "{{ name}}" image: "{{ image }}" command: /code/run.sh links: - "gridrouter" volumes: - "{{ project }}:/code" - "{{ workspace }}/report:/code/target/surefire-reports" state: started - name: "{{ language }} | wait until tests complete" command: "docker wait {{ name }}" register: tests_result - name: "{{ language }} | delete container" docker: name: "{{ name}}" image: "{{ image }}" state: absent - name: "{{ language }} | delete project" file: path={{ project }} state=absent ignore_errors: yes ================================================ FILE: testing/start.yml ================================================ --- - hosts: 127.0.0.1 connection: local roles: - start ================================================ FILE: testing/stop.yml ================================================ --- - hosts: 127.0.0.1 connection: local roles: - stop ================================================ FILE: testing/test.yml ================================================ --- - hosts: 127.0.0.1 connection: local roles: - test