├── .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 /.gitignore: -------------------------------------------------------------------------------- 1 | # IDEA files 2 | .idea 3 | *.iml 4 | 5 | # Maven files 6 | target 7 | 8 | # Docker compose files 9 | compose/war 10 | 11 | # Npm modules 12 | node_modules 13 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | The following authors have created the source code of "Selenium Grid Router" 2 | published and distributed by YANDEX LLC as the owner: 3 | 4 | * Alexander Andryashin 5 | * Dmitry Baev 6 | * Artem Eroshenko 7 | * Innokenty Shuvalov 8 | * Ivan Krutov 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (C) YANDEX LLC, 2015 2 | 3 | The Source Code called "Selenium Grid Router" available at https://github.com/seleniumkit/gridrouter is subject 4 | to the terms of the Apache License 2.0 (hereinafter referred to as the "License"). 5 | The text of the License is the following: 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); 8 | you may not use this file except in compliance with the License. 9 | You may obtain a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, 15 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | See the License for the specific language governing permissions and 17 | limitations under the License. 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selenium Grid Router 2 | 3 | **Selenium Grid Router** is a lightweight server that routes and proxies [Selenium Wedriver](http://www.seleniumhq.org/projects/webdriver/) requests to multiple Selenium hubs. 4 | 5 | ## Golang Implementation 6 | There is a smaller and faster Golang implementation of this server. See https://github.com/aerokube/ggr for more details. 7 | 8 | ## What is this for 9 | 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: 10 | * **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. 11 | * **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. 12 | * **Does not support authentication and authorization.** Standard Selenium grid hub makes all nodes available for everyone. 13 | 14 | ## How it works 15 | The basic idea is very simple: 16 | 17 | 1. Define user names and their passwords in a plain text file 18 | 2. Distribute a set of running Selenium hubs (aka "hosts") with nodes connected to each one over multiple datacenters 19 | 3. For each defined user save hosts to a simple XML configuration file 20 | 4. Start multiple instances of Grid Router in different datacenters and load-balance them 21 | 5. Work with Grid Router like you do with a regular Selenium hub 22 | 23 | ## Installation 24 | 25 | Currently we maintain only Debian packages. To install on Ubuntu ensure that you have Java 8 installed: 26 | ``` 27 | # add-apt-repository ppa:webupd8team/java 28 | # apt-get update 29 | # apt-get install oracle-java8-installer 30 | ``` 31 | Then install Gridrouter itself: 32 | ``` 33 | # add-apt-repository ppa:yandex-qatools/gridrouter 34 | # apt-get update 35 | # apt-get install yandex-grid-router 36 | # service yandex-grid-router start 37 | ``` 38 | 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`. 39 | 40 | ## Configuration 41 | Two types of configuration files exist: 42 | * A plain text file with users and passwords (users.properties) 43 | * An XML file with user quota definition (<username>.xml) 44 | 45 | ### Users list (users.properties) 46 | A typical file looks like this: 47 | ``` 48 | alice:alicePassword, user 49 | bob:bobPassword, user 50 | ``` 51 | 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. 52 | 53 | ### User quota definition (<username>.xml) 54 | This file has the following format: 55 | ```xml 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | ``` 88 | 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. 89 | 90 | ### Authentication 91 | 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: 92 | ``` 93 | http://username:password@grid-router-host.example.com:4444/wd/hub 94 | ``` 95 | However some Javascript test frameworks have their own ways to specify connection URL, user name and password. 96 | 97 | ### Hub selection logic 98 | When you request a browser by specifying its name and version **Grid Router** does the following: 99 | 100 | 1. Searches for the browser in user quota XML and returns error if not found 101 | 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. 102 | 3. If browser was obtained - returns it to the user and proxies all requests in this session to the same host 103 | 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. 104 | 5. After trying all hosts returns error if no browser was obtained 105 | 106 | ### Hub configuration recommendations 107 | 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: 108 | * Because we have more hubs the probability to successfully obtain browser is greater 109 | * If each virtual machine has only one browser version installed - it's simpler to increase overall count of available browsers 110 | * Hubs with small count of connected nodes perform better 111 | 112 | ## Development 113 | 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. 114 | 115 | ### Install Boot2docker (dog-nail for Mac users) 116 | 117 | * Install Ansible: `brew install ansible` 118 | * Create an empty inventory file: `touch /usr/local/etc/ansible/hosts` 119 | * Adjust Python settings: `echo 'localhost ansible_python_interpreter=/usr/local/bin/python' >> /usr/local/etc/ansible/hosts` 120 | * Instally Python from [official website](https://www.python.org/ftp/python/2.7.10/python-2.7.10-macosx10.6.pkg) 121 | * Install requests with pip: `pip install requests[security]` 122 | * Install docker-py: `pip install -Iv https://pypi.python.org/packages/source/d/docker-py/docker-py-1.1.0.tar.gz` 123 | * Run boot2docker: `boot2docker up` 124 | * Get Docker VM IP: `boot2docker ip` 125 | * Modify `/etc/hosts`: ` boot2docker` 126 | * Add certificates information to console: `$(boot2docker shellinit)` 127 | * Export correct host name: `export DOCKER_HOST=tcp://boot2docker:2376` 128 | 129 | ### Running service locally 130 | 131 | #### Start 132 | 133 | 1. Build project: `mvn clean package` 134 | 2. Start app: `ansible-playbook testing/start.yml` 135 | 3. Check that container is running: `docker ps -a` 136 | 137 | #### Run integration tests 138 | 139 | ```bash] 140 | $ ansible-playbook testing/test.yml 141 | ``` 142 | 143 | #### Stop 144 | 145 | ```bash 146 | $ ansible-playbook testing/stop.yml 147 | ``` 148 | -------------------------------------------------------------------------------- /ci/jenkins.groovy: -------------------------------------------------------------------------------- 1 | def project = 'gridrouter'; 2 | def repo = 'seleniumkit/gridrouter' 3 | 4 | def buildWarJob = mavenJob("${project}_build-war") 5 | def e2eTestsJob = job("${project}_e2e-tests") 6 | def sonarJob = mavenJob("${project}_sonar") 7 | def sonarIncrJob = mavenJob("${project}_sonar-incr") 8 | def deployJob = mavenJob("${project}_deploy") 9 | 10 | def pullRequestJob = multiJob("${project}_pull-reqest_flow") 11 | def snapshotJob = multiJob("${project}_snapshot_flow") 12 | def releaseJob = mavenJob("${project}_release_flow") 13 | 14 | buildWarJob.with { 15 | 16 | label('maven') 17 | scm { 18 | git { 19 | remote { 20 | github(repo, 'https', 'github.com') 21 | refspec('${GIT_REFSPEC}') 22 | } 23 | branch('${GIT_COMMIT}') 24 | localBranch('master') 25 | } 26 | } 27 | goals('clean package') 28 | 29 | publishers { 30 | archiveArtifacts('proxy/target/*.war') 31 | } 32 | } 33 | 34 | e2eTestsJob.with { 35 | 36 | label('e2e') 37 | scm { 38 | git { 39 | remote { 40 | github(repo, 'https', 'github.com') 41 | refspec('${GIT_REFSPEC}') 42 | } 43 | branch('${GIT_COMMIT}') 44 | localBranch('master') 45 | } 46 | } 47 | 48 | steps { 49 | copyArtifacts(buildWarJob.name) { 50 | includePatterns('proxy/target/*.war') 51 | buildSelector { 52 | buildNumber('${WAR_BUILD_NUMBER}') 53 | } 54 | } 55 | shell('ansible-playbook -e "workspace=/tmp/e2e/${JOB_NAME}/${BUILD_NUMBER}" testing/start.yml') 56 | shell('ansible-playbook -e "workspace=/tmp/e2e/${JOB_NAME}/${BUILD_NUMBER}" testing/test.yml') 57 | shell('ansible-playbook -e "workspace=/tmp/e2e/${JOB_NAME}/${BUILD_NUMBER}" testing/stop.yml') 58 | } 59 | 60 | publishers { 61 | archiveJunit('testing/target/surefire-reports/*.xml') 62 | } 63 | } 64 | 65 | sonarJob.with { 66 | 67 | label('maven') 68 | scm { 69 | git { 70 | remote { 71 | github(repo, 'https', 'github.com') 72 | refspec('${GIT_REFSPEC}') 73 | } 74 | branch('${GIT_COMMIT}') 75 | localBranch('master') 76 | } 77 | } 78 | 79 | publishers { 80 | sonar() 81 | } 82 | } 83 | 84 | sonarIncrJob.with { 85 | 86 | label('maven') 87 | scm { 88 | git { 89 | remote { 90 | github(repo, 'https', 'github.com') 91 | refspec('${GIT_REFSPEC}') 92 | } 93 | branch('${GIT_COMMIT}') 94 | localBranch('master') 95 | } 96 | } 97 | 98 | configure { 99 | it / 'publishers' / 'hudson.plugins.sonar.SonarPublisher' { 100 | jdk('(Inherit From Job)') 101 | branch() 102 | language() 103 | jobAdditionalProperties('-Dsonar.analysis.mode=incremental -Dsonar.github.pullRequest=${ghprbPullId} -Dsonar.github.repository=' + repo) 104 | settings(class: 'jenkins.mvn.DefaultSettingsProvider') 105 | globalSettings(class: 'jenkins.mvn.DefaultGlobalSettingsProvider') 106 | usePrivateRepository(false) 107 | } 108 | } 109 | } 110 | 111 | deployJob.with { 112 | 113 | label('maven') 114 | 115 | scm { 116 | git { 117 | remote { 118 | github(repo, 'https', 'github.com') 119 | refspec('${GIT_REFSPEC}') 120 | } 121 | branch('${GIT_COMMIT}') 122 | localBranch('master') 123 | } 124 | } 125 | 126 | goals('clean deploy') 127 | 128 | } 129 | 130 | pullRequestJob.with { 131 | 132 | label('master') 133 | displayName('Grid Router Pull Requests Flow') 134 | 135 | scm { 136 | git { 137 | remote { 138 | github(repo, 'https', 'github.com') 139 | refspec('+refs/pull/*:refs/remotes/origin/pr/*') 140 | } 141 | branch('${sha1}') 142 | } 143 | } 144 | 145 | triggers { 146 | pullRequest { 147 | orgWhitelist(['seleniumkit']) 148 | permitAll() 149 | useGitHubHooks() 150 | } 151 | } 152 | 153 | steps { 154 | phase('Build war file') { 155 | job(sonarIncrJob.name) { 156 | prop('GIT_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*'); 157 | prop('GIT_COMMIT', '\${sha1}'); 158 | } 159 | job(buildWarJob.name) { 160 | prop('GIT_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*'); 161 | prop('GIT_COMMIT', '\${sha1}'); 162 | } 163 | } 164 | phase('Run e2e tests', 'UNSTABLE') { 165 | job(e2eTestsJob.name) { 166 | prop('WAR_BUILD_NUMBER', '\${' + buildWarJob.name.toUpperCase().replace("-", "_") + '_BUILD_NUMBER}'); 167 | prop('GIT_REFSPEC', '+refs/pull/*:refs/remotes/origin/pr/*'); 168 | prop('GIT_COMMIT', '\${sha1}'); 169 | } 170 | } 171 | } 172 | 173 | publishers { 174 | aggregateDownstreamTestResults() 175 | } 176 | } 177 | 178 | snapshotJob.with { 179 | 180 | label('default') 181 | displayName('Grid Router Snapshot Flow') 182 | 183 | scm { 184 | git { 185 | remote { 186 | github(repo, 'https', 'github.com') 187 | } 188 | localBranch('master') 189 | branch('master') 190 | } 191 | } 192 | triggers { 193 | githubPush() 194 | } 195 | 196 | steps { 197 | phase('Build war file') { 198 | job(buildWarJob.name) { 199 | prop('GIT_COMMIT', '\${GIT_COMMIT}'); 200 | prop('GIT_REFSPEC', ''); 201 | } 202 | } 203 | phase('Run e2e tests') { 204 | job(e2eTestsJob.name) { 205 | prop('WAR_BUILD_NUMBER', '\${' + buildWarJob.name.toUpperCase().replace("-", "_") + '_BUILD_NUMBER}'); 206 | prop('GIT_COMMIT', '\${GIT_COMMIT}'); 207 | prop('GIT_REFSPEC', ''); 208 | } 209 | job(sonarJob.name) { 210 | prop('GIT_COMMIT', '\${GIT_COMMIT}'); 211 | prop('GIT_REFSPEC', ''); 212 | } 213 | } 214 | phase('Deploy war') { 215 | job(deployJob.name) { 216 | prop('GIT_COMMIT', '\${GIT_COMMIT}'); 217 | prop('GIT_REFSPEC', ''); 218 | } 219 | } 220 | } 221 | 222 | publishers { 223 | aggregateDownstreamTestResults() 224 | } 225 | } 226 | 227 | releaseJob.with { 228 | 229 | label('maven') 230 | displayName('Grid Router Release Flow') 231 | 232 | scm { 233 | git { 234 | remote { 235 | github(repo, 'https', 'github.com') 236 | } 237 | localBranch('master') 238 | branch('master') 239 | } 240 | } 241 | 242 | goals('clean deploy') 243 | 244 | wrappers { 245 | mavenRelease { 246 | releaseGoals('release:clean release:prepare release:perform') 247 | dryRunGoals('-DdryRun=true release:prepare') 248 | numberOfReleaseBuildsToKeep(10) 249 | } 250 | } 251 | } 252 | 253 | listView(project) { 254 | jobs { 255 | regex("${project}_.*_flow") 256 | } 257 | 258 | columns { 259 | status() 260 | name() 261 | lastSuccess() 262 | lastFailure() 263 | lastDuration() 264 | buildButton() 265 | } 266 | } -------------------------------------------------------------------------------- /config/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ru.qatools.seleniumkit 5 | gridrouter 6 | 1.32-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | gridrouter-config 11 | Selenium Grid Router Config 12 | 13 | 14 | 15 | 16 | org.jvnet.jaxb2.maven2 17 | maven-jaxb2-plugin 18 | 19 | 20 | 21 | 22 | 23 | 24 | commons-io 25 | commons-io 26 | 27 | 28 | org.apache.commons 29 | commons-lang3 30 | 31 | 32 | commons-codec 33 | commons-codec 34 | 1.10 35 | 36 | 37 | 38 | 39 | junit 40 | junit 41 | test 42 | 43 | 44 | org.hamcrest 45 | hamcrest-all 46 | test 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/GridRouterException.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | /** 4 | * @author Dmitry Baev charlie@yandex-team.ru 5 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 6 | */ 7 | public class GridRouterException extends RuntimeException { 8 | 9 | public GridRouterException(String message, Throwable cause) { 10 | super(message, cause); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/HostSelectionStrategy.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 7 | */ 8 | public interface HostSelectionStrategy { 9 | 10 | Region selectRegion(List allRegions, List unvisitedRegions); 11 | 12 | Host selectHost(List hosts); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/RandomHostSelectionStrategy.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | import static java.util.Collections.nCopies; 7 | import static java.util.Collections.shuffle; 8 | 9 | /** 10 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 11 | */ 12 | public class RandomHostSelectionStrategy implements HostSelectionStrategy { 13 | 14 | protected T selectRandom(List elements) { 15 | List copies = new ArrayList<>(); 16 | for (T element : elements) { 17 | copies.addAll(nCopies(element.getCount(), element)); 18 | } 19 | shuffle(copies); 20 | return copies.get(0); 21 | } 22 | 23 | @Override 24 | public Region selectRegion(List allRegions, List unvisitedRegions) { 25 | return selectRandom(unvisitedRegions); 26 | } 27 | 28 | @Override 29 | public Host selectHost(List hosts) { 30 | return selectRandom(hosts); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/RegionWithCount.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 7 | */ 8 | public interface RegionWithCount extends WithCount { 9 | 10 | List getHosts(); 11 | 12 | @Override 13 | default int getCount() { 14 | return getHosts().stream().mapToInt(Host::getCount).sum(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/SequentialHostSelectionStrategy.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 7 | */ 8 | public class SequentialHostSelectionStrategy implements HostSelectionStrategy { 9 | 10 | private int hostIndex; 11 | 12 | @Override 13 | public Region selectRegion(List allRegions, List unvisitedRegions) { 14 | return unvisitedRegions.get(0); 15 | } 16 | 17 | @Override 18 | public Host selectHost(List hosts) { 19 | Host host = hosts.get(hostIndex++ % hosts.size()); 20 | hostIndex %= hosts.size(); 21 | return host; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/VersionWithCount.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 7 | */ 8 | public interface VersionWithCount extends WithCount { 9 | 10 | List getRegions(); 11 | 12 | @Override 13 | default int getCount() { 14 | return getRegions().stream().mapToInt(Region::getCount).sum(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/WithBrowserVersionFind.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import java.util.List; 4 | 5 | import static org.apache.commons.lang3.StringUtils.isEmpty; 6 | 7 | /** 8 | * @author Dmitry Baev charlie@yandex-team.ru 9 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 10 | */ 11 | public interface WithBrowserVersionFind { 12 | 13 | List getBrowsers(); 14 | 15 | default Browser findBrowser(String name) { 16 | return getBrowsers().stream() 17 | .filter(b -> b.getName().equals(name)) 18 | .findFirst().orElse(null); 19 | } 20 | 21 | default Version find(String browserName, String browserVersion) { 22 | Browser browser = findBrowser(browserName); 23 | 24 | if (browser == null) { 25 | return null; 26 | } 27 | 28 | return isEmpty(browserVersion) ? 29 | browser.findDefaultVersion() : 30 | browser.findVersion(browserVersion); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/WithCopy.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | 6 | /** 7 | * @author Dmitry Baev charlie@yandex-team.ru 8 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 9 | */ 10 | public interface WithCopy { 11 | 12 | List getHosts(); 13 | 14 | String getName(); 15 | 16 | /** 17 | * Creates a copy for the {@link Region} object, which is almost deep: 18 | * the hosts itself are not copied although the list is new. 19 | * 20 | * @return a copy of the object 21 | */ 22 | default Region copy() { 23 | return new Region(new ArrayList<>(getHosts()), getName()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/WithCount.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | /** 4 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 5 | */ 6 | public interface WithCount { 7 | 8 | int getCount(); 9 | } 10 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/WithRoute.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import org.apache.commons.codec.digest.DigestUtils; 4 | 5 | import java.nio.charset.StandardCharsets; 6 | 7 | /** 8 | * @author Dmitry Baev charlie@yandex-team.ru 9 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 10 | */ 11 | public interface WithRoute { 12 | 13 | default String getAddress() { 14 | return getName() + ":" + getPort(); 15 | } 16 | 17 | default String getRoute() { 18 | return "http://" + getAddress(); 19 | } 20 | 21 | default String getRouteId() { 22 | return DigestUtils.md5Hex(getRoute().getBytes(StandardCharsets.UTF_8)); 23 | } 24 | 25 | String getName(); 26 | 27 | int getPort(); 28 | } 29 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/WithRoutesMap.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import java.util.HashMap; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | /** 8 | * @author Dmitry Baev charlie@yandex-team.ru 9 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 10 | */ 11 | public interface WithRoutesMap { 12 | 13 | List getBrowsers(); 14 | 15 | default Map getRoutesMap() { 16 | HashMap routes = new HashMap<>(); 17 | getBrowsers().stream() 18 | .flatMap(b -> b.getVersions().stream()) 19 | .flatMap(v -> v.getRegions().stream()) 20 | .flatMap(r -> r.getHosts().stream()) 21 | .forEach(h -> routes.put(h.getRouteId(), h.getRoute())); 22 | return routes; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/WithVersionFind.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import java.util.List; 4 | 5 | /** 6 | * @author Dmitry Baev charlie@yandex-team.ru 7 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 8 | */ 9 | public interface WithVersionFind { 10 | 11 | default Version findDefaultVersion() { 12 | return findVersion(getDefaultVersion()); 13 | } 14 | 15 | default Version findVersion(String versionPrefix) { 16 | return getVersions().stream() 17 | .filter(v -> v.getNumber().startsWith(versionPrefix)) 18 | .findFirst().orElse(null); 19 | } 20 | 21 | List getVersions(); 22 | 23 | String getDefaultVersion(); 24 | } 25 | -------------------------------------------------------------------------------- /config/src/main/java/ru/qatools/gridrouter/config/WithXmlView.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import javax.xml.bind.JAXBContext; 4 | import javax.xml.bind.JAXBException; 5 | import javax.xml.bind.Marshaller; 6 | import java.io.StringWriter; 7 | 8 | import static java.nio.charset.StandardCharsets.UTF_8; 9 | import static javax.xml.bind.Marshaller.JAXB_ENCODING; 10 | import static javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT; 11 | 12 | /** 13 | * @author Dmitry Baev charlie@yandex-team.ru 14 | */ 15 | public interface WithXmlView { 16 | 17 | default String toXml() { 18 | try { 19 | JAXBContext context = JAXBContext.newInstance(getClass()); 20 | Marshaller marshaller = context.createMarshaller(); 21 | marshaller.setProperty(JAXB_ENCODING, UTF_8.toString()); 22 | marshaller.setProperty(JAXB_FORMATTED_OUTPUT, true); 23 | StringWriter writer = new StringWriter(); 24 | marshaller.marshal(this, writer); 25 | return writer.toString(); 26 | } catch (JAXBException e) { 27 | throw new GridRouterException("Unable to marshall bean", e); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config/src/main/resources/xsd/bindings.xjb: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ru.qatools.gridrouter.config.WithBrowserVersionFind 15 | ru.qatools.gridrouter.config.WithXmlView 16 | ru.qatools.gridrouter.config.WithRoutesMap 17 | 18 | 19 | 20 | ru.qatools.gridrouter.config.WithVersionFind 21 | 22 | 23 | 24 | ru.qatools.gridrouter.config.VersionWithCount 25 | 26 | 27 | 28 | ru.qatools.gridrouter.config.WithCopy 29 | ru.qatools.gridrouter.config.RegionWithCount 30 | 31 | 32 | 33 | ru.qatools.gridrouter.config.WithRoute 34 | ru.qatools.gridrouter.config.WithCount 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /config/src/main/resources/xsd/config.xsd: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /config/src/test/java/ru/qatools/gridrouter/config/RandomHostSelectionStrategyTest.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.config; 2 | 3 | import org.hamcrest.Matcher; 4 | import org.junit.Test; 5 | 6 | import java.util.ArrayList; 7 | import java.util.HashMap; 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.UUID; 11 | 12 | import static org.hamcrest.MatcherAssert.assertThat; 13 | import static org.hamcrest.Matchers.both; 14 | import static org.hamcrest.Matchers.greaterThan; 15 | import static org.hamcrest.Matchers.lessThan; 16 | 17 | /** 18 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 19 | */ 20 | public class RandomHostSelectionStrategyTest { 21 | 22 | private static final double ALLOWED_DEVIATION = 0.01; 23 | 24 | @Test 25 | @SuppressWarnings("ArraysAsListWithZeroOrOneArgument") 26 | public void testRandomness() { 27 | int entriesCount = 5000000; 28 | int keysCount = 10; 29 | 30 | Host host1 = new Host("host_1", 4444, keysCount - 1); 31 | 32 | List hosts = new ArrayList<>(keysCount); 33 | hosts.add(host1); 34 | 35 | int i = keysCount; 36 | while (i --> 1) { 37 | hosts.add(newHost()); 38 | } 39 | 40 | HashMap appearances = new HashMap<>(keysCount, entriesCount / keysCount); 41 | 42 | RandomHostSelectionStrategy strategy = new RandomHostSelectionStrategy(); 43 | i = entriesCount; 44 | while (i-- > 0) { 45 | Host host = strategy.selectRandom(hosts); 46 | appearances.put(host, Optional.ofNullable(appearances.get(host)).orElse(0) + 1); 47 | } 48 | 49 | assertThat(appearances.remove(host1), isAround(entriesCount / 2)); 50 | 51 | for (int count : appearances.values()) { 52 | assertThat(count, isAround(entriesCount / 2 / (keysCount - 1))); 53 | } 54 | } 55 | 56 | private static Host newHost() { 57 | return new Host(UUID.randomUUID().toString(), 4444, 1); 58 | } 59 | 60 | private static Matcher isAround(int count) { 61 | return both(greaterThan( 62 | (int) (count * (1 - ALLOWED_DEVIATION)) 63 | )).and(lessThan( 64 | (int) (count * (1 + ALLOWED_DEVIATION)) 65 | )); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | 6 | org.sonatype.oss 7 | oss-parent 8 | 9 9 | 10 | 11 | ru.qatools.seleniumkit 12 | gridrouter 13 | 1.32-SNAPSHOT 14 | pom 15 | 16 | 17 | config 18 | proxy 19 | 20 | 21 | Selenium Grid Router 22 | 23 | 24 | UTF-8 25 | 1.8 26 | 27 | 4.1.6.RELEASE 28 | 9.3.6.v20151106 29 | 2.6.0-rc3 30 | 1.7.7 31 | 32 | 33 | 34 | Yandex 35 | http://yandex.ru/ 36 | 37 | 38 | 39 | 40 | The Apache Software License, Version 2.0 41 | http://www.apache.org/licenses/LICENSE-2.0.txt 42 | repo 43 | 44 | 45 | 46 | 47 | scm:git:git@github.com:seleniumkit/gridrouter.git 48 | scm:git:git@github.com:seleniumkit/gridrouter.git 49 | https://github.com/seleniumkit/gridrouter 50 | HEAD 51 | 52 | 53 | 54 | GitHub Issues 55 | https://github.com/seleniumkit/gridrouter/issues 56 | 57 | 58 | 59 | Jenkins 60 | http://ci.qatools.ru/ 61 | 62 | 63 | 64 | 65 | 66 | commons-io 67 | commons-io 68 | 2.4 69 | 70 | 71 | org.apache.commons 72 | commons-lang3 73 | 3.4 74 | 75 | 76 | 77 | junit 78 | junit 79 | 4.12 80 | 81 | 82 | org.hamcrest 83 | hamcrest-all 84 | 1.3 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | org.apache.maven.plugins 93 | maven-compiler-plugin 94 | 3.3 95 | 96 | ${java.version} 97 | ${java.version} 98 | 99 | 100 | 101 | org.apache.maven.plugins 102 | maven-javadoc-plugin 103 | 2.10.3 104 | 105 | -Xdoclint:none 106 | 107 | 108 | 109 | org.apache.maven.plugins 110 | maven-source-plugin 111 | 2.4 112 | 113 | 114 | 115 | jar 116 | 117 | 118 | 119 | 120 | 121 | org.apache.maven.plugins 122 | maven-release-plugin 123 | 2.5.2 124 | 125 | @{project.version} 126 | 127 | 128 | 129 | 130 | 131 | 132 | org.jvnet.jaxb2.maven2 133 | maven-jaxb2-plugin 134 | 0.12.3 135 | 136 | 137 | 138 | generate 139 | 140 | 141 | 142 | 143 | src/main/resources/xsd 144 | src/main/resources/xsd 145 | true 146 | true 147 | true 148 | 149 | -Xinheritance 150 | -Xvalue-constructor 151 | 152 | 153 | 154 | org.jvnet.jaxb2_commons 155 | jaxb2-basics 156 | 0.9.4 157 | 158 | 159 | org.jvnet.jaxb2_commons 160 | jaxb2-value-constructor 161 | 3.0 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | -------------------------------------------------------------------------------- /proxy/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ru.qatools.seleniumkit 5 | gridrouter 6 | 1.32-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | gridrouter-proxy 11 | Selenium Grid Router Proxy 12 | war 13 | 14 | 15 | 16 | 17 | org.apache.maven.plugins 18 | maven-war-plugin 19 | 20 | true 21 | 22 | 23 | 24 | org.jvnet.jaxb2.maven2 25 | maven-jaxb2-plugin 26 | 27 | 28 | 29 | 30 | 31 | 32 | ru.qatools.seleniumkit 33 | gridrouter-config 34 | ${project.version} 35 | 36 | 37 | 38 | ru.yandex.qatools.beanloader 39 | beanloader 40 | 2.1 41 | 42 | 43 | 44 | 45 | commons-io 46 | commons-io 47 | 48 | 49 | org.apache.commons 50 | commons-lang3 51 | 52 | 53 | 54 | 55 | org.springframework 56 | spring-web 57 | ${spring.version} 58 | 59 | 60 | 61 | 62 | org.eclipse.jetty 63 | jetty-servlet 64 | ${jetty.version} 65 | 66 | 67 | org.eclipse.jetty 68 | jetty-proxy 69 | ${jetty.version} 70 | 71 | 72 | org.eclipse.jetty 73 | jetty-security 74 | ${jetty.version} 75 | 76 | 77 | org.eclipse.jetty 78 | jetty-annotations 79 | ${jetty.version} 80 | test 81 | 82 | 83 | 84 | 85 | com.fasterxml.jackson.core 86 | jackson-databind 87 | ${jackson.version} 88 | 89 | 90 | com.fasterxml.jackson.core 91 | jackson-annotations 92 | ${jackson.version} 93 | 94 | 95 | 96 | 97 | org.apache.httpcomponents 98 | httpclient 99 | 4.5.2 100 | 101 | 102 | 103 | 104 | org.slf4j 105 | slf4j-api 106 | ${slf4j.version} 107 | 108 | 109 | org.slf4j 110 | slf4j-log4j12 111 | ${slf4j.version} 112 | 113 | 114 | 115 | 116 | junit 117 | junit 118 | test 119 | 120 | 121 | org.hamcrest 122 | hamcrest-all 123 | test 124 | 125 | 126 | org.mockito 127 | mockito-all 128 | 1.9.5 129 | test 130 | 131 | 132 | org.mock-server 133 | mockserver-netty 134 | 3.9.17 135 | test 136 | 137 | 138 | logback-classic 139 | ch.qos.logback 140 | 141 | 142 | 143 | 144 | org.json 145 | json 146 | 20140107 147 | test 148 | 149 | 150 | org.seleniumhq.selenium 151 | selenium-java 152 | 2.53.0 153 | test 154 | 155 | 156 | xml-apis 157 | xml-apis 158 | 1.4.01 159 | test 160 | 161 | 162 | ru.yandex.qatools.matchers 163 | matcher-decorators 164 | 1.1 165 | test 166 | 167 | 168 | org.springframework 169 | spring-test 170 | ${spring.version} 171 | test 172 | 173 | 174 | 175 | 176 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/ConfigRepository.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import ru.qatools.gridrouter.config.Browser; 4 | import ru.qatools.gridrouter.config.Browsers; 5 | import ru.qatools.gridrouter.config.Version; 6 | import ru.qatools.gridrouter.json.JsonCapabilities; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | /** 12 | * @author Ilya Sadykov 13 | */ 14 | public interface ConfigRepository { 15 | Map getQuotaMap(); 16 | 17 | String getRoute(String routeId); 18 | 19 | default Version findVersion(String user, JsonCapabilities caps) { 20 | final Browsers browsers = getQuotaMap().get(user); 21 | return browsers != null ? browsers.find(caps.getBrowserName(), caps.getVersion()) : null; 22 | } 23 | 24 | default Map getBrowsersCountMap(String user) { 25 | HashMap countMap = new HashMap<>(); 26 | for (Browser browser : getQuotaMap().get(user).getBrowsers()) { 27 | for (Version version : browser.getVersions()) { 28 | countMap.put(browser.getName() + ":" + version.getNumber(), version.getCount()); 29 | } 30 | } 31 | return countMap; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/ConfigRepositoryXml.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import org.apache.commons.io.FilenameUtils; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Value; 7 | import ru.qatools.beanloader.BeanChangeListener; 8 | import ru.qatools.beanloader.BeanLoader; 9 | import ru.qatools.beanloader.BeanWatcher; 10 | import ru.qatools.gridrouter.config.Browsers; 11 | 12 | import javax.annotation.PostConstruct; 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.nio.file.Path; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | /** 20 | * @author Alexander Andyashin aandryashin@yandex-team.ru 21 | * @author Dmitry Baev charlie@yandex-team.ru 22 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 23 | */ 24 | public class ConfigRepositoryXml implements ConfigRepository, BeanChangeListener { 25 | 26 | private static final Logger LOGGER = LoggerFactory.getLogger(ConfigRepositoryXml.class); 27 | 28 | private static final String XML_GLOB = "*.xml"; 29 | 30 | @Value("${grid.config.quota.directory}") 31 | private File quotaDirectory; 32 | 33 | @Value("${grid.config.quota.hotReload}") 34 | private boolean quotaHotReload; 35 | 36 | private Map userBrowsers = new HashMap<>(); 37 | 38 | private Map routes = new HashMap<>(); 39 | 40 | @PostConstruct 41 | public void init() { 42 | try { 43 | if (quotaHotReload) { 44 | LOGGER.debug("Starting quota watcher"); 45 | BeanWatcher.watchFor(Browsers.class, quotaDirectory.toPath(), XML_GLOB, this); 46 | } else { 47 | LOGGER.debug("Loading quota configuration"); 48 | BeanLoader.loadAll(Browsers.class, quotaDirectory.toPath(), XML_GLOB, this); 49 | } 50 | } catch (IOException e) { 51 | LOGGER.error("Quota configuration loading failed", e); 52 | } 53 | } 54 | 55 | @Override 56 | public void beanChanged(Path filename, Browsers browsers) { 57 | if (browsers == null) { 58 | LOGGER.info("Configuration file [{}] was deleted. " 59 | + "It is not purged from the running gridrouter process though.", filename); 60 | } else { 61 | LOGGER.info("Loading quota configuration file [{}]", filename); 62 | String user = FilenameUtils.getBaseName(filename.toString()); 63 | userBrowsers.put(user, browsers); 64 | routes.putAll(browsers.getRoutesMap()); 65 | LOGGER.info("Loaded quota configuration for [{}] from [{}]: \n\n{}", 66 | user, filename, browsers.toXml()); 67 | } 68 | } 69 | 70 | @Override 71 | public Map getQuotaMap() { 72 | return userBrowsers; 73 | } 74 | 75 | @Override 76 | public String getRoute(String routeId) { 77 | return routes.get(routeId); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/JsonWireUtils.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import org.apache.http.client.utils.URIBuilder; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import javax.servlet.http.HttpServletRequest; 8 | import java.io.UnsupportedEncodingException; 9 | import java.net.URISyntaxException; 10 | import java.net.URLDecoder; 11 | 12 | import static java.nio.charset.StandardCharsets.UTF_8; 13 | import static org.springframework.http.HttpMethod.DELETE; 14 | 15 | /** 16 | * @author Alexander Andyashin aandryashin@yandex-team.ru 17 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 18 | * @author Dmitry Baev charlie@yandex-team.ru 19 | * @author Artem Eroshenko eroshenkoam@yandex-team.ru 20 | */ 21 | public final class JsonWireUtils { 22 | 23 | private static final Logger LOGGER = LoggerFactory.getLogger(JsonWireUtils.class); 24 | 25 | public static final String WD_HUB_SESSION = "/wd/hub/session/"; 26 | 27 | public static final int SESSION_HASH_LENGTH = 32; 28 | 29 | private JsonWireUtils() { 30 | } 31 | 32 | public static boolean isUriValid(String uri) { 33 | return uri.length() > getUriPrefixLength(); 34 | } 35 | 36 | public static boolean isSessionDeleteRequest(HttpServletRequest request, String command) { 37 | return DELETE.name().equalsIgnoreCase(request.getMethod()) && !command.contains("/"); 38 | } 39 | 40 | public static String getSessionHash(String uri) { 41 | return uri.substring(WD_HUB_SESSION.length(), getUriPrefixLength()); 42 | } 43 | 44 | public static String getFullSessionId(String uri) { 45 | String tail = uri.substring(WD_HUB_SESSION.length()); 46 | int end = tail.indexOf('/'); 47 | if (end < 0) { 48 | return tail; 49 | } 50 | return tail.substring(0, end); 51 | } 52 | 53 | public static int getUriPrefixLength() { 54 | return WD_HUB_SESSION.length() + SESSION_HASH_LENGTH; 55 | } 56 | 57 | public static String redirectionUrl(String host, String command) throws URISyntaxException { 58 | return new URIBuilder(host).setPath(WD_HUB_SESSION + command).build().toString(); 59 | } 60 | 61 | public static String getCommand(String uri) { 62 | String encodedCommand = uri.substring(getUriPrefixLength()); 63 | try { 64 | return URLDecoder.decode(encodedCommand, UTF_8.name()); 65 | } catch (UnsupportedEncodingException e) { 66 | LOGGER.error("[UNABLE_TO_DECODE_COMMAND] - could not decode command: {}", encodedCommand, e); 67 | return encodedCommand; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/PingServlet.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import javax.servlet.ServletException; 4 | import javax.servlet.annotation.WebServlet; 5 | import javax.servlet.http.HttpServlet; 6 | import javax.servlet.http.HttpServletRequest; 7 | import javax.servlet.http.HttpServletResponse; 8 | import java.io.IOException; 9 | import java.io.PrintWriter; 10 | 11 | /** 12 | * @author Alexander Andyashin aandryashin@yandex-team.ru 13 | * @author Artem Eroshenko eroshenkoam@yandex-team.ru 14 | * @author Dmitry Baev charlie@yandex-team.ru 15 | */ 16 | @WebServlet(urlPatterns = {"/ping"}, asyncSupported = true) 17 | public class PingServlet extends HttpServlet { 18 | 19 | @Override 20 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 21 | try (PrintWriter writer = resp.getWriter()) { 22 | writer.print("OK"); 23 | writer.flush(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/ProxyServlet.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.eclipse.jetty.client.api.Request; 5 | import org.eclipse.jetty.client.util.StringContentProvider; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import ru.qatools.gridrouter.json.JsonMessage; 10 | import ru.qatools.gridrouter.json.JsonMessageFactory; 11 | import ru.qatools.gridrouter.sessions.StatsCounter; 12 | 13 | import javax.servlet.ServletConfig; 14 | import javax.servlet.ServletException; 15 | import javax.servlet.annotation.WebInitParam; 16 | import javax.servlet.annotation.WebServlet; 17 | import javax.servlet.http.HttpServletRequest; 18 | import javax.servlet.http.HttpServletResponse; 19 | import java.io.IOException; 20 | 21 | import static java.nio.charset.StandardCharsets.UTF_8; 22 | import static org.springframework.web.context.support.SpringBeanAutowiringSupport.processInjectionBasedOnServletContext; 23 | import static ru.qatools.gridrouter.JsonWireUtils.*; 24 | import static ru.qatools.gridrouter.RequestUtils.getRemoteHost; 25 | 26 | /** 27 | * @author Alexander Andyashin aandryashin@yandex-team.ru 28 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 29 | * @author Dmitry Baev charlie@yandex-team.ru 30 | * @author Artem Eroshenko eroshenkoam@yandex-team.ru 31 | */ 32 | @WebServlet( 33 | urlPatterns = {WD_HUB_SESSION + "*"}, 34 | asyncSupported = true, 35 | initParams = { 36 | @WebInitParam(name = "timeout", value = "300000"), 37 | @WebInitParam(name = "idleTimeout", value = "300000") 38 | } 39 | ) 40 | public class ProxyServlet extends org.eclipse.jetty.proxy.ProxyServlet { 41 | 42 | private static final Logger LOGGER = LoggerFactory.getLogger(ProxyServlet.class); 43 | 44 | @Autowired 45 | private transient ConfigRepository config; 46 | 47 | @Autowired 48 | private transient StatsCounter statsCounter; 49 | 50 | @Override 51 | public void init(ServletConfig config) throws ServletException { 52 | super.init(config); 53 | processInjectionBasedOnServletContext(this, config.getServletContext()); 54 | } 55 | 56 | @Override 57 | protected void sendProxyRequest( 58 | HttpServletRequest clientRequest, HttpServletResponse proxyResponse, Request proxyRequest) { 59 | try { 60 | Request request = getRequestWithoutSessionId(clientRequest, proxyRequest); 61 | super.sendProxyRequest(clientRequest, proxyResponse, request); 62 | } catch (Exception exception) { 63 | LOGGER.error("[REQUEST_READ_FAILURE] [{}] - could not read client request, proxying request as is", 64 | clientRequest.getRemoteHost(), exception); 65 | super.sendProxyRequest(clientRequest, proxyResponse, proxyRequest); 66 | } 67 | } 68 | 69 | @Override 70 | protected String rewriteTarget(HttpServletRequest request) { 71 | String uri = request.getRequestURI(); 72 | 73 | String remoteHost = getRemoteHost(request); 74 | 75 | if (!isUriValid(uri)) { 76 | LOGGER.warn("[INVALID_SESSION_HASH] [{}] - request uri is {}", remoteHost, uri); 77 | return null; 78 | } 79 | 80 | String route = config.getRoute(getSessionHash(uri)); 81 | String command = getCommand(uri); 82 | 83 | if (route == null) { 84 | LOGGER.error("[ROUTE_NOT_FOUND] [{}] - request uri is {}", remoteHost, uri); 85 | return null; 86 | } 87 | 88 | if (isSessionDeleteRequest(request, command)) { 89 | LOGGER.info("[SESSION_DELETED] [{}] [{}] [{}]", remoteHost, route, command); 90 | statsCounter.deleteSession(getFullSessionId(uri), route); 91 | } else { 92 | statsCounter.updateSession(getFullSessionId(uri), route); 93 | } 94 | 95 | try { 96 | return redirectionUrl(route, command); 97 | } catch (Exception exception) { 98 | LOGGER.error("[REDIRECTION_URL_ERROR] [{}] - error building redirection uri because of {}\n" 99 | + " request uri: {}\n" 100 | + " parsed route: {}\n" 101 | + " parsed command: {}", 102 | remoteHost, exception.toString(), uri, route, command); 103 | } 104 | return null; 105 | } 106 | 107 | protected Request getRequestWithoutSessionId(HttpServletRequest clientRequest, Request proxyRequest) throws IOException { 108 | String content = IOUtils.toString(clientRequest.getInputStream(), UTF_8); 109 | if (!content.isEmpty()) { 110 | String remoteHost = getRemoteHost(clientRequest); 111 | content = removeSessionIdSafe(content, remoteHost); 112 | } 113 | return proxyRequest.content( 114 | new StringContentProvider(clientRequest.getContentType(), content, UTF_8)); 115 | } 116 | 117 | private String removeSessionIdSafe(String content, String remoteHost) { 118 | try { 119 | JsonMessage message = JsonMessageFactory.from(content); 120 | message.setSessionId(null); 121 | return message.toJson(); 122 | } catch (IOException exception) { 123 | LOGGER.error("[UNABLE_TO_REMOVE_SESSION_ID] [{}] - could not create proxy request without session id, " 124 | + "proxying request as is. Request content is: {}", 125 | remoteHost, content, exception); 126 | } 127 | return content; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/QuotaServlet.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | 6 | import javax.servlet.ServletException; 7 | import javax.servlet.annotation.HttpConstraint; 8 | import javax.servlet.annotation.ServletSecurity; 9 | import javax.servlet.annotation.WebServlet; 10 | import javax.servlet.http.HttpServletRequest; 11 | import javax.servlet.http.HttpServletResponse; 12 | import java.io.IOException; 13 | import java.io.OutputStream; 14 | 15 | import static java.nio.charset.StandardCharsets.UTF_8; 16 | import static javax.servlet.http.HttpServletResponse.SC_OK; 17 | import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 18 | import static ru.qatools.gridrouter.json.JsonFormatter.toJson; 19 | 20 | /** 21 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 22 | */ 23 | @WebServlet(urlPatterns = {"/quota"}, asyncSupported = true) 24 | @ServletSecurity(value = @HttpConstraint(rolesAllowed = {"user"})) 25 | public class QuotaServlet extends SpringHttpServlet { 26 | 27 | @Autowired 28 | private transient ConfigRepository config; 29 | 30 | @Override 31 | protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { 32 | resp.setStatus(SC_OK); 33 | resp.setContentType(APPLICATION_JSON_VALUE); 34 | try (OutputStream output = resp.getOutputStream()) { 35 | String jsonResponse = toJson(config.getBrowsersCountMap(req.getRemoteUser())); 36 | IOUtils.write(jsonResponse, output, UTF_8); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/RequestUtils.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import javax.servlet.http.HttpServletRequest; 4 | 5 | public final class RequestUtils { 6 | 7 | private static final String X_FORWARDED_FOR = "X-Forwarded-For"; 8 | 9 | public static String getRemoteHost(HttpServletRequest request) { 10 | String remoteHost = request.getHeader(X_FORWARDED_FOR); 11 | if (remoteHost == null) { 12 | return request.getRemoteHost(); 13 | } 14 | return remoteHost; 15 | } 16 | 17 | private RequestUtils(){} 18 | } 19 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/RouteServlet.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import org.apache.commons.io.IOUtils; 5 | import org.apache.http.HttpResponse; 6 | import org.apache.http.client.config.RequestConfig; 7 | import org.apache.http.client.methods.HttpPost; 8 | import org.apache.http.entity.StringEntity; 9 | import org.apache.http.impl.client.CloseableHttpClient; 10 | import org.apache.http.impl.client.HttpClientBuilder; 11 | import org.apache.http.impl.client.LaxRedirectStrategy; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.beans.factory.annotation.Value; 16 | import ru.qatools.gridrouter.caps.CapabilityProcessorFactory; 17 | import ru.qatools.gridrouter.config.Host; 18 | import ru.qatools.gridrouter.config.HostSelectionStrategy; 19 | import ru.qatools.gridrouter.config.Region; 20 | import ru.qatools.gridrouter.config.Version; 21 | import ru.qatools.gridrouter.json.JsonCapabilities; 22 | import ru.qatools.gridrouter.json.JsonMessage; 23 | import ru.qatools.gridrouter.json.JsonMessageFactory; 24 | import ru.qatools.gridrouter.sessions.AvailableBrowserCheckExeption; 25 | import ru.qatools.gridrouter.sessions.AvailableBrowsersChecker; 26 | import ru.qatools.gridrouter.sessions.StatsCounter; 27 | 28 | import javax.servlet.ServletException; 29 | import javax.servlet.annotation.HttpConstraint; 30 | import javax.servlet.annotation.ServletSecurity; 31 | import javax.servlet.annotation.WebServlet; 32 | import javax.servlet.http.HttpServletRequest; 33 | import javax.servlet.http.HttpServletResponse; 34 | import java.io.IOException; 35 | import java.io.OutputStream; 36 | import java.time.Instant; 37 | import java.util.ArrayList; 38 | import java.util.List; 39 | import java.util.concurrent.Callable; 40 | import java.util.concurrent.Executors; 41 | import java.util.concurrent.ScheduledExecutorService; 42 | import java.util.concurrent.TimeUnit; 43 | import java.util.concurrent.atomic.AtomicBoolean; 44 | import java.util.concurrent.atomic.AtomicLong; 45 | 46 | import static java.lang.String.format; 47 | import static java.nio.charset.StandardCharsets.UTF_8; 48 | import static java.util.stream.Collectors.toList; 49 | import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR; 50 | import static javax.servlet.http.HttpServletResponse.SC_OK; 51 | import static org.apache.http.HttpHeaders.ACCEPT; 52 | import static org.apache.http.entity.ContentType.APPLICATION_JSON; 53 | import static ru.qatools.gridrouter.RequestUtils.getRemoteHost; 54 | 55 | /** 56 | * @author Alexander Andyashin aandryashin@yandex-team.ru 57 | * @author Dmitry Baev charlie@yandex-team.ru 58 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 59 | * @author Artem Eroshenko eroshenkoam@yandex-team.ru 60 | */ 61 | @WebServlet(urlPatterns = {"/wd/hub/session"}, asyncSupported = true) 62 | @ServletSecurity(value = @HttpConstraint(rolesAllowed = {"user"})) 63 | public class RouteServlet extends SpringHttpServlet { 64 | 65 | private static final Logger LOGGER = LoggerFactory.getLogger(RouteServlet.class); 66 | 67 | private static final String ROUTE_TIMEOUT_CAPABILITY = "grid.router.route.timeout.seconds"; 68 | 69 | private static final int MAX_ROUTE_TIMEOUT_SECONDS = 300; 70 | 71 | @Autowired 72 | private transient ConfigRepository config; 73 | @Autowired 74 | private transient HostSelectionStrategy hostSelectionStrategy; 75 | @Autowired 76 | private transient StatsCounter statsCounter; 77 | @Autowired 78 | private transient CapabilityProcessorFactory capabilityProcessorFactory; 79 | @Autowired 80 | private transient AvailableBrowsersChecker avblBrowsersChecker; 81 | 82 | @Value("${grid.router.route.timeout.seconds:120}") 83 | private int routeTimeout; 84 | 85 | private AtomicLong requestCounter = new AtomicLong(); 86 | 87 | @Override 88 | protected void doPost(HttpServletRequest request, HttpServletResponse response) 89 | throws ServletException, IOException { 90 | ScheduledExecutorService executor = Executors.newScheduledThreadPool(2); 91 | JsonMessage message = JsonMessageFactory.from(request.getInputStream()); 92 | 93 | long requestId = requestCounter.getAndIncrement(); 94 | int routeTimeout = getRouteTimeout(request.getRemoteUser(), message); 95 | AtomicBoolean terminated = new AtomicBoolean(false); 96 | executor.submit(getRouteCallable(request, message, response, requestId, routeTimeout, terminated)); 97 | executor.shutdown(); 98 | try { 99 | executor.awaitTermination(routeTimeout, TimeUnit.SECONDS); 100 | terminated.set(true); 101 | } catch (InterruptedException e) { 102 | executor.shutdownNow(); 103 | } 104 | replyWithError("Timed out when searching for valid host", response); 105 | } 106 | 107 | private Callable getRouteCallable(HttpServletRequest request, JsonMessage message, HttpServletResponse response, 108 | long requestId, int routeTimeout, AtomicBoolean terminated) { 109 | return () -> { 110 | route(request, message, response, requestId, routeTimeout, terminated); 111 | return null; 112 | }; 113 | } 114 | 115 | private int getRouteTimeout(String user, JsonMessage message) { 116 | JsonCapabilities caps = message.getDesiredCapabilities(); 117 | try { 118 | if (caps.any().containsKey(ROUTE_TIMEOUT_CAPABILITY)) { 119 | Integer desiredRouteTimeout = Integer.valueOf(String.valueOf(caps.any().get(ROUTE_TIMEOUT_CAPABILITY))); 120 | routeTimeout = (desiredRouteTimeout < MAX_ROUTE_TIMEOUT_SECONDS) ? 121 | desiredRouteTimeout : 122 | MAX_ROUTE_TIMEOUT_SECONDS; 123 | LOGGER.warn("[{}] [INVALID_ROUTE_TIMEOUT] [{}]", user, desiredRouteTimeout); 124 | } 125 | } catch (NumberFormatException ignored) { 126 | } 127 | return routeTimeout; 128 | } 129 | 130 | private void route(HttpServletRequest request, JsonMessage message, 131 | HttpServletResponse response, 132 | long requestId, int routeTimeout, AtomicBoolean terminated) throws IOException { 133 | 134 | 135 | long initialSeconds = Instant.now().getEpochSecond(); 136 | 137 | JsonCapabilities caps = message.getDesiredCapabilities(); 138 | 139 | String user = request.getRemoteUser(); 140 | String remoteHost = getRemoteHost(request); 141 | String browser = caps.describe(); 142 | Version actualVersion = config.findVersion(user, caps); 143 | 144 | if (actualVersion == null) { 145 | LOGGER.warn("[{}] [UNSUPPORTED_BROWSER] [{}] [{}] [{}]", requestId, user, remoteHost, browser); 146 | replyWithError(format("Cannot find %s capabilities on any available node", 147 | caps.describe()), response); 148 | return; 149 | } 150 | 151 | caps.setVersion(actualVersion.getNumber()); 152 | 153 | capabilityProcessorFactory.getProcessor(caps).process(caps); 154 | 155 | List allRegions = actualVersion.getRegions() 156 | .stream().map(Region::copy).collect(toList()); 157 | List unvisitedRegions = new ArrayList<>(allRegions); 158 | 159 | int attempt = 0; 160 | JsonMessage hubMessage = null; 161 | try (CloseableHttpClient client = newHttpClient(routeTimeout * 1000)) { 162 | if (actualVersion.getPermittedCount() != null) { 163 | avblBrowsersChecker.ensureFreeBrowsersAvailable(user, remoteHost, browser, actualVersion); 164 | } 165 | 166 | while (!allRegions.isEmpty() && !terminated.get()) { 167 | attempt++; 168 | 169 | Region currentRegion = hostSelectionStrategy.selectRegion(allRegions, unvisitedRegions); 170 | Host host = hostSelectionStrategy.selectHost(currentRegion.getHosts()); 171 | 172 | String route = host.getRoute(); 173 | try { 174 | LOGGER.info("[{}] [SESSION_ATTEMPTED] [{}] [{}] [{}] [{}] [{}]", requestId, user, remoteHost, browser, route, attempt); 175 | 176 | String target = route + request.getRequestURI(); 177 | HttpResponse hubResponse = client.execute(post(target, message)); 178 | hubMessage = JsonMessageFactory.from(hubResponse.getEntity().getContent()); 179 | 180 | if (hubResponse.getStatusLine().getStatusCode() == SC_OK) { 181 | String sessionId = hubMessage.getSessionId(); 182 | hubMessage.setSessionId(host.getRouteId() + sessionId); 183 | replyWithOk(hubMessage, response); 184 | long createdDurationSeconds = Instant.now().getEpochSecond() - initialSeconds; 185 | LOGGER.info("[{}] [{}] [SESSION_CREATED] [{}] [{}] [{}] [{}] [{}] [{}]", 186 | requestId, createdDurationSeconds, user, remoteHost, browser, route, sessionId, attempt); 187 | statsCounter.startSession(hubMessage.getSessionId(), user, caps.getBrowserName(), actualVersion.getNumber(), route); 188 | return; 189 | } 190 | LOGGER.warn("[{}] [SESSION_FAILED] [{}] [{}] [{}] [{}] - {}", 191 | requestId, user, remoteHost, browser, route, hubMessage.getErrorMessage()); 192 | } catch (JsonProcessingException exception) { 193 | LOGGER.error("[{}] [BAD_HUB_JSON] [{}] [{}] [{}] [{}] - {}", "", 194 | requestId, user, remoteHost, browser, route, exception.getMessage()); 195 | } catch (IOException exception) { 196 | LOGGER.error("[{}] [HUB_COMMUNICATION_FAILURE] [{}] [{}] [{}] - {}", 197 | requestId, user, remoteHost, browser, route, exception.getMessage()); 198 | } 199 | 200 | currentRegion.getHosts().remove(host); 201 | if (currentRegion.getHosts().isEmpty()) { 202 | allRegions.remove(currentRegion); 203 | } 204 | 205 | unvisitedRegions.remove(currentRegion); 206 | if (unvisitedRegions.isEmpty()) { 207 | unvisitedRegions = new ArrayList<>(allRegions); 208 | } 209 | } 210 | } catch (AvailableBrowserCheckExeption e) { 211 | LOGGER.error("[{}] [AVAILABLE_BROWSER_CHECK_ERROR] [{}] [{}] [{}] - {}", 212 | requestId, user, remoteHost, browser, e.getMessage()); 213 | } 214 | 215 | LOGGER.error("[{}] [SESSION_NOT_CREATED] [{}] [{}] [{}]", requestId, user, remoteHost, browser); 216 | if (hubMessage == null) { 217 | replyWithError("Cannot create session on any available node", response); 218 | } else { 219 | replyWithError(hubMessage, response); 220 | } 221 | } 222 | 223 | 224 | protected void replyWithOk(JsonMessage message, HttpServletResponse response) throws IOException { 225 | reply(SC_OK, message, response); 226 | } 227 | 228 | protected void replyWithError(String errorMessage, HttpServletResponse response) throws IOException { 229 | replyWithError(JsonMessageFactory.error(13, errorMessage), response); 230 | } 231 | 232 | protected void replyWithError(JsonMessage message, HttpServletResponse response) throws IOException { 233 | reply(SC_INTERNAL_SERVER_ERROR, message, response); 234 | } 235 | 236 | protected void reply(int code, JsonMessage message, HttpServletResponse response) throws IOException { 237 | response.setStatus(code); 238 | response.setContentType(APPLICATION_JSON.toString()); 239 | String messageRaw = message.toJson(); 240 | response.setContentLength(messageRaw.getBytes(UTF_8).length); 241 | try (OutputStream output = response.getOutputStream()) { 242 | IOUtils.write(messageRaw, output, UTF_8); 243 | } 244 | } 245 | 246 | protected HttpPost post(String target, JsonMessage message) throws IOException { 247 | HttpPost method = new HttpPost(target); 248 | StringEntity entity = new StringEntity(message.toJson(), APPLICATION_JSON); 249 | method.setEntity(entity); 250 | method.setHeader(ACCEPT, APPLICATION_JSON.getMimeType()); 251 | return method; 252 | } 253 | 254 | protected CloseableHttpClient newHttpClient(int maxTimeout) { 255 | return HttpClientBuilder.create().setDefaultRequestConfig( 256 | RequestConfig.custom() 257 | .setConnectionRequestTimeout(10000) 258 | .setConnectTimeout(10000) 259 | .setSocketTimeout(maxTimeout) 260 | .build() 261 | ).setRedirectStrategy(new LaxRedirectStrategy()).disableAutomaticRetries().build(); 262 | } 263 | } -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/SessionStorageEvictionScheduler.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | import org.springframework.scheduling.annotation.Scheduled; 8 | import ru.qatools.gridrouter.sessions.StatsCounter; 9 | 10 | import java.time.Duration; 11 | 12 | /** 13 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 14 | */ 15 | @Configuration 16 | @EnableScheduling 17 | public class SessionStorageEvictionScheduler { 18 | 19 | @Value("${grid.router.evict.sessions.timeout.seconds}") 20 | private int timeout; 21 | 22 | @Autowired 23 | private StatsCounter statsCounter; 24 | 25 | @Scheduled(cron = "${grid.router.evict.sessions.cron}") 26 | public void expireOldSessions() { 27 | statsCounter.expireSessionsOlderThan(Duration.ofSeconds(timeout)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/SpringHttpServlet.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import javax.servlet.ServletConfig; 4 | import javax.servlet.ServletException; 5 | import javax.servlet.http.HttpServlet; 6 | 7 | import static org.springframework.web.context.support.SpringBeanAutowiringSupport.processInjectionBasedOnServletContext; 8 | 9 | /** 10 | * @author Ilya Sadykov 11 | */ 12 | public abstract class SpringHttpServlet extends HttpServlet { 13 | @Override 14 | public void init(ServletConfig config) throws ServletException { 15 | super.init(config); 16 | processInjectionBasedOnServletContext(this, config.getServletContext()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/StatsServlet.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter; 2 | 3 | import org.apache.commons.io.IOUtils; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import ru.qatools.gridrouter.json.JsonFormatter; 6 | import ru.qatools.gridrouter.sessions.StatsCounter; 7 | 8 | import javax.servlet.ServletException; 9 | import javax.servlet.annotation.HttpConstraint; 10 | import javax.servlet.annotation.ServletSecurity; 11 | import javax.servlet.annotation.WebServlet; 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | import java.io.OutputStream; 16 | 17 | import static java.nio.charset.StandardCharsets.UTF_8; 18 | import static javax.servlet.http.HttpServletResponse.SC_OK; 19 | import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; 20 | 21 | /** 22 | * @author Dmitry Baev charlie@yandex-team.ru 23 | */ 24 | @WebServlet(urlPatterns = {"/stats"}, asyncSupported = true) 25 | @ServletSecurity(value = @HttpConstraint(rolesAllowed = {"user"})) 26 | public class StatsServlet extends SpringHttpServlet { 27 | 28 | @Autowired 29 | private transient StatsCounter statsCounter; 30 | 31 | @Override 32 | protected void doGet(HttpServletRequest request, HttpServletResponse response) 33 | throws ServletException, IOException { 34 | response.setStatus(SC_OK); 35 | response.setContentType(APPLICATION_JSON_VALUE); 36 | try (OutputStream output = response.getOutputStream()) { 37 | IOUtils.write(JsonFormatter.toJson( 38 | statsCounter.getStats(request.getRemoteUser()) 39 | ), output, UTF_8); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/caps/AppiumCapabilityProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.caps; 2 | 3 | import org.springframework.stereotype.Service; 4 | import ru.qatools.gridrouter.json.JsonCapabilities; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | *

10 | * Sets "keepKeyChains" capability for Mac sessions. 11 | *

12 | * 13 | * @author Ivan Krutov vania-pooh@yandex-team.ru 14 | * 15 | */ 16 | @SuppressWarnings("JavadocReference") 17 | @Service 18 | public class AppiumCapabilityProcessor implements CapabilityProcessor { 19 | 20 | private static final String PLATFORM_NAME = "platformName"; 21 | private static final String IOS = "iOS"; 22 | 23 | @Override 24 | public boolean accept(JsonCapabilities caps) { 25 | return caps.getBrowserName().isEmpty() && isMac(caps); 26 | } 27 | 28 | @Override 29 | public void process(JsonCapabilities caps) { 30 | caps.any().put("keepKeyChains", true); 31 | } 32 | 33 | private boolean isMac(JsonCapabilities jsonCapabilities) { 34 | Map capsMap = jsonCapabilities.any(); 35 | return 36 | capsMap.containsKey(PLATFORM_NAME) && 37 | String.valueOf(capsMap.get(PLATFORM_NAME)).contains(IOS); 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/caps/CapabilityProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.caps; 2 | 3 | import ru.qatools.gridrouter.json.JsonCapabilities; 4 | 5 | /** 6 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 7 | */ 8 | public interface CapabilityProcessor { 9 | 10 | boolean accept(JsonCapabilities caps); 11 | 12 | void process(JsonCapabilities caps); 13 | } 14 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/caps/CapabilityProcessorFactory.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.caps; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Component; 5 | import ru.qatools.gridrouter.json.JsonCapabilities; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 11 | */ 12 | @Component 13 | public class CapabilityProcessorFactory { 14 | 15 | @Autowired 16 | @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") 17 | private List processors; 18 | 19 | public CapabilityProcessor getProcessor(JsonCapabilities caps) { 20 | return processors.stream() 21 | .filter(p -> p.accept(caps)) 22 | .findFirst() 23 | .orElse(new DummyCapabilityProcessor()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/caps/DummyCapabilityProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.caps; 2 | 3 | import ru.qatools.gridrouter.json.JsonCapabilities; 4 | 5 | /** 6 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 7 | */ 8 | public class DummyCapabilityProcessor implements CapabilityProcessor { 9 | 10 | @Override 11 | public boolean accept(JsonCapabilities caps) { 12 | throw new UnsupportedOperationException("Method DummyCapabilityProcessor::accept should never be called"); 13 | } 14 | 15 | @Override 16 | public void process(JsonCapabilities caps) { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /proxy/src/main/java/ru/qatools/gridrouter/caps/IECapabilityProcessor.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.gridrouter.caps; 2 | 3 | import org.springframework.stereotype.Service; 4 | import ru.qatools.gridrouter.json.JsonCapabilities; 5 | import ru.qatools.gridrouter.json.Proxy; 6 | 7 | /** 8 | *

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

12 | *

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

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