├── screen.png ├── service ├── src │ ├── test │ │ ├── resources │ │ │ ├── selenograph-hub-remove.properties │ │ │ ├── selenograph-node-remove.properties │ │ │ ├── camelot.properties │ │ │ ├── user4.xml │ │ │ ├── camelot-test.properties │ │ │ ├── user1.xml │ │ │ ├── log4j.properties │ │ │ ├── user3.xml │ │ │ ├── user2.xml │ │ │ └── camelot-extensions.xml │ │ └── java │ │ │ └── ru │ │ │ └── qatools │ │ │ └── selenograph │ │ │ ├── gridrouter │ │ │ ├── RegexTest.java │ │ │ ├── QueueWaitAvailableBrowsersCheckerTest.java │ │ │ ├── QuotaSummaryClientNotifierTest.java │ │ │ ├── SmartHostSelectionStrategyTest.java │ │ │ └── QuotaStatsAggregatorTest.java │ │ │ ├── util │ │ │ ├── EmbeddedMongodbService.java │ │ │ └── FullStacktraceThrowableRendererTest.java │ │ │ ├── utils │ │ │ └── TestProperties.java │ │ │ └── api │ │ │ └── BasePluginTest.java │ └── main │ │ ├── java │ │ └── ru │ │ │ └── qatools │ │ │ └── selenograph │ │ │ ├── gridrouter │ │ │ ├── UpdatableSelectionStrategy.java │ │ │ ├── SessionEventFilter.java │ │ │ ├── QuotaSummaryClientNotifier.java │ │ │ ├── JettyProxyInitializer.java │ │ │ ├── SessionsCountsPerUser.java │ │ │ ├── ApiResource.java │ │ │ ├── QuotaStatsAggregator.java │ │ │ ├── QueueWaitAvailableBrowsersChecker.java │ │ │ ├── SmartHostSelectionStrategy.java │ │ │ └── SessionsAggregator.java │ │ │ ├── ext │ │ │ ├── jackson │ │ │ │ ├── SelenographSerializers.java │ │ │ │ ├── SelenographJacksonModule.java │ │ │ │ ├── ObjectMapperProvider.java │ │ │ │ └── SelenographDeserializers.java │ │ │ ├── SelenographMongoSerializerBuilder.java │ │ │ ├── SelenographMessagesSerializer.java │ │ │ ├── SelenographMongoSerializer.java │ │ │ └── SelenographDB.java │ │ │ ├── api │ │ │ ├── ApiResource.java │ │ │ └── InputResource.java │ │ │ └── util │ │ │ └── FullStacktraceThrowableRenderer.java │ │ └── resources │ │ ├── META-INF │ │ └── web-fragment.xml │ │ ├── camelot.xml │ │ ├── camelot-extensions.xml │ │ └── camelot-default.properties └── pom.xml ├── server ├── .babelrc ├── src │ └── main │ │ ├── frontend │ │ ├── js │ │ │ ├── icons │ │ │ │ └── browsers │ │ │ │ │ ├── edge │ │ │ │ │ └── edge_128x128.png │ │ │ │ │ ├── opera │ │ │ │ │ └── opera_128x128.png │ │ │ │ │ ├── chrome │ │ │ │ │ └── chrome_128x128.png │ │ │ │ │ ├── safari │ │ │ │ │ └── safari_128x128.png │ │ │ │ │ ├── android │ │ │ │ │ └── android_128x128.png │ │ │ │ │ ├── chromium │ │ │ │ │ └── chromium_128x128.png │ │ │ │ │ ├── firefox │ │ │ │ │ └── firefox_128x128.png │ │ │ │ │ ├── safari-ios │ │ │ │ │ └── safari-ios_128x128.png │ │ │ │ │ └── internet-explorer │ │ │ │ │ └── internet-explorer_128x128.png │ │ │ ├── app.jsx │ │ │ ├── components │ │ │ │ ├── BrowserVersion.jsx │ │ │ │ ├── Browser.jsx │ │ │ │ ├── QuotaSelector.jsx │ │ │ │ └── Browsers.jsx │ │ │ └── util │ │ │ │ ├── fetch.js │ │ │ │ └── websocket.js │ │ ├── index.html │ │ └── tools │ │ │ └── server.js │ │ ├── resources │ │ ├── selenograph-default.properties │ │ ├── log4j.properties │ │ └── camelot-extensions.xml │ │ ├── java │ │ └── ru │ │ │ └── qatools │ │ │ └── selenograph │ │ │ └── Application.java │ │ └── webapp │ │ └── WEB-INF │ │ └── web.xml ├── typings │ ├── webpack-dev-server.d.ts │ ├── main.d.ts │ ├── browser.d.ts │ ├── main │ │ ├── ambient │ │ │ ├── mime │ │ │ │ └── mime.d.ts │ │ │ ├── react-dom │ │ │ │ └── react-dom.d.ts │ │ │ ├── serve-static │ │ │ │ └── serve-static.d.ts │ │ │ ├── atmosphere │ │ │ │ └── atmosphere.d.ts │ │ │ └── isomorphic-fetch │ │ │ │ └── isomorphic-fetch.d.ts │ │ └── definitions │ │ │ └── webpack │ │ │ └── webpack.d.ts │ └── browser │ │ └── ambient │ │ ├── mime │ │ └── mime.d.ts │ │ ├── react-dom │ │ └── react-dom.d.ts │ │ ├── serve-static │ │ └── serve-static.d.ts │ │ ├── atmosphere │ │ └── atmosphere.d.ts │ │ └── isomorphic-fetch │ │ └── isomorphic-fetch.d.ts ├── .eslintrc ├── webpack.config.js ├── package.json └── pom.xml ├── .gitignore ├── AUTHORS ├── beans ├── src │ └── main │ │ ├── resources │ │ └── xsd │ │ │ ├── bindings.xjb │ │ │ ├── front.xsd │ │ │ └── beans.xsd │ │ └── java │ │ └── ru │ │ └── qatools │ │ └── selenograph │ │ └── gridrouter │ │ ├── Key.java │ │ ├── UserBrowser.java │ │ ├── WaitAvailableBrowserState.java │ │ ├── Timestamped.java │ │ └── BrowserSummaries.java └── pom.xml ├── LICENSE ├── README.md └── pom.xml /screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/screen.png -------------------------------------------------------------------------------- /service/src/test/resources/selenograph-hub-remove.properties: -------------------------------------------------------------------------------- 1 | selenograph.hub.remove.timeout=0 2 | -------------------------------------------------------------------------------- /service/src/test/resources/selenograph-node-remove.properties: -------------------------------------------------------------------------------- 1 | selenograph.node.remove.timeout=0 2 | -------------------------------------------------------------------------------- /server/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"], 3 | "plugins": ["transform-class-properties", "react-require"] 4 | } -------------------------------------------------------------------------------- /server/src/main/frontend/js/icons/browsers/edge/edge_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/server/src/main/frontend/js/icons/browsers/edge/edge_128x128.png -------------------------------------------------------------------------------- /server/src/main/frontend/js/icons/browsers/opera/opera_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/server/src/main/frontend/js/icons/browsers/opera/opera_128x128.png -------------------------------------------------------------------------------- /server/src/main/frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Selenograph 5 | 6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /server/src/main/frontend/js/icons/browsers/chrome/chrome_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/server/src/main/frontend/js/icons/browsers/chrome/chrome_128x128.png -------------------------------------------------------------------------------- /server/src/main/frontend/js/icons/browsers/safari/safari_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/server/src/main/frontend/js/icons/browsers/safari/safari_128x128.png -------------------------------------------------------------------------------- /server/src/main/frontend/js/icons/browsers/android/android_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/server/src/main/frontend/js/icons/browsers/android/android_128x128.png -------------------------------------------------------------------------------- /server/src/main/frontend/js/icons/browsers/chromium/chromium_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/server/src/main/frontend/js/icons/browsers/chromium/chromium_128x128.png -------------------------------------------------------------------------------- /server/src/main/frontend/js/icons/browsers/firefox/firefox_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/server/src/main/frontend/js/icons/browsers/firefox/firefox_128x128.png -------------------------------------------------------------------------------- /server/src/main/frontend/js/icons/browsers/safari-ios/safari-ios_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/server/src/main/frontend/js/icons/browsers/safari-ios/safari-ios_128x128.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | *.war 6 | *.ear 7 | 8 | target 9 | .idea 10 | *.iml 11 | *.ipr 12 | *.iws 13 | 14 | overlays 15 | server/node 16 | server/node_modules 17 | npm-debug.log -------------------------------------------------------------------------------- /server/src/main/frontend/js/icons/browsers/internet-explorer/internet-explorer_128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/seleniumkit/selenograph/HEAD/server/src/main/frontend/js/icons/browsers/internet-explorer/internet-explorer_128x128.png -------------------------------------------------------------------------------- /server/src/main/resources/selenograph-default.properties: -------------------------------------------------------------------------------- 1 | camelot.factory=camelot-factory-mongodb 2 | camelot.quartzFactory=camelot-quartz-factory-mongodb 3 | camelot.clientSendersProvider=camelot-client-senders-mongodb 4 | graphite.host= 5 | -------------------------------------------------------------------------------- /server/src/main/frontend/js/app.jsx: -------------------------------------------------------------------------------- 1 | import Browsers from './components/Browsers.jsx'; 2 | import { createElement } from 'react'; 3 | import { render } from 'react-dom'; 4 | import injectTapEventPlugin from 'react-tap-event-plugin'; 5 | 6 | injectTapEventPlugin(); 7 | render( 8 | createElement(Browsers), 9 | document.getElementById('app') 10 | ); -------------------------------------------------------------------------------- /service/src/test/resources/camelot.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | selenograph.browsers.start.cooldown=0 3 | 4 | grid.config.quota.directory=classpath: 5 | grid.config.quota.hotReload=true 6 | grid.router.evict.sessions.cron=0 * * * * * 7 | grid.router.evict.sessions.timeout.seconds=120 8 | grid.router.queue.timeout.seconds=1 9 | -------------------------------------------------------------------------------- /service/src/test/resources/user4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /service/src/test/resources/camelot-test.properties: -------------------------------------------------------------------------------- 1 | camelot.factory=camelot-factory-mongodb 2 | camelot.mongodb.writeconcern=SAFE 3 | camelot.mongodb.username=camelot 4 | camelot.mongodb.password=camelot 5 | camelot.mongodb.dbname=selenograph 6 | camelot.serializer=selenograph-messages-serializer 7 | camelot.mongodb.serializer.builder=selenograph-mongo-serializer-builder 8 | grid.router.quota.repository=ru.qatools.gridrouter.ConfigRepositoryXml -------------------------------------------------------------------------------- /service/src/test/resources/user1.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /server/typings/webpack-dev-server.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'webpack-dev-server' { 2 | import * as webpack from 'webpack'; 3 | import {RequestHandler} from 'express'; 4 | 5 | class WebpackDevServer { 6 | constructor(compiler:webpack.compiler.Compiler, configuration:Object) 7 | 8 | use(...middlewares:RequestHandler[]) 9 | 10 | listen(port:Number, callback?:Function) 11 | } 12 | 13 | export = WebpackDevServer; 14 | } 15 | -------------------------------------------------------------------------------- /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 | * Ilya Sadykov 10 | -------------------------------------------------------------------------------- /service/src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | log4j.rootLogger=INFO, out 3 | 4 | log4j.logger.ru.qatools.selenograph=DEBUG 5 | 6 | # CONSOLE appender not used by default 7 | log4j.appender.out=org.apache.log4j.ConsoleAppender 8 | log4j.appender.out.layout=org.apache.log4j.PatternLayout 9 | log4j.appender.out.layout.ConversionPattern=%d [%-30.30t] %-5p %-30.30c{1} - %m%n 10 | 11 | log4j.throwableRenderer=org.apache.log4j.EnhancedThrowableRenderer 12 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/UpdatableSelectionStrategy.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import ru.qatools.selenograph.front.HubSummary; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | /** 9 | * @author Ilya Sadykov 10 | */ 11 | public interface UpdatableSelectionStrategy { 12 | long getTimestamp(); 13 | 14 | Map getHubs(); 15 | 16 | void updateHubSummaries(List hubSummaries, long timestamp); 17 | } 18 | -------------------------------------------------------------------------------- /server/src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | log4j.rootLogger=INFO, logger 3 | 4 | log4j.logger.ru.qatools.selenograph=WARN 5 | 6 | # CONSOLE appender not used by default 7 | log4j.appender.logger=org.apache.log4j.ConsoleAppender 8 | log4j.appender.logger.layout=org.apache.log4j.PatternLayout 9 | log4j.appender.logger.layout.ConversionPattern=%d [%-10.10t] %-5p %c{1} - %m%n 10 | 11 | log4j.throwableRenderer=ru.qatools.selenograph.util.FullStacktraceThrowableRenderer 12 | -------------------------------------------------------------------------------- /service/src/test/resources/user3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /server/src/main/frontend/js/components/BrowserVersion.jsx: -------------------------------------------------------------------------------- 1 | import LinearProgress from 'material-ui/lib/linear-progress'; 2 | 3 | export default function BrowserVersion({running, max, version}) { 4 | return ( 5 |
6 |
7 | {version} 8 | {running}/{max} 9 |
10 | 14 |
15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /server/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "parser": "babel-eslint", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module", 7 | "ecmaFeatures": { 8 | "jsx": true 9 | } 10 | }, 11 | "env": { 12 | "es6": true, 13 | "browser": true, 14 | "node": true 15 | }, 16 | "plugins": [ 17 | "react" 18 | ], 19 | "rules": { 20 | "quotes": [2, "single"], 21 | "one-var": [2, "never"], 22 | "semi": [2, "always"], 23 | "react/jsx-no-undef": 2, 24 | "react/jsx-uses-vars": 2, 25 | "react/no-unknown-property": 2 26 | } 27 | } -------------------------------------------------------------------------------- /service/src/test/resources/user2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /server/src/main/frontend/js/components/Browser.jsx: -------------------------------------------------------------------------------- 1 | import LinearProgress from 'material-ui/lib/linear-progress'; 2 | 3 | export default function Browser({running, max, name}) { 4 | return ( 5 |
6 |
7 | {name} 8 | {running}/{max} 9 |
10 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/ext/jackson/SelenographSerializers.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.ext.jackson; 2 | 3 | import com.fasterxml.jackson.databind.module.SimpleSerializers; 4 | import org.bson.types.ObjectId; 5 | import org.mongojack.internal.DBRefSerializer; 6 | import org.mongojack.internal.ObjectIdSerializer; 7 | 8 | /** 9 | * @author Ilya Sadykov 10 | */ 11 | public class SelenographSerializers extends SimpleSerializers { 12 | public SelenographSerializers() { 13 | addSerializer(new DBRefSerializer()); 14 | addSerializer(ObjectId.class, new ObjectIdSerializer()); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/ext/SelenographMongoSerializerBuilder.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.ext; 2 | 3 | import ru.yandex.qatools.camelot.common.MessagesSerializer; 4 | import ru.yandex.qatools.camelot.mongodb.MongoSerializer; 5 | import ru.yandex.qatools.camelot.mongodb.MongoSerializerBuilder; 6 | 7 | /** 8 | * @author Ilya Sadykov 9 | */ 10 | public class SelenographMongoSerializerBuilder extends MongoSerializerBuilder{ 11 | @Override 12 | public MongoSerializer build(MessagesSerializer msgSerializer, ClassLoader classLoader) { 13 | return new SelenographMongoSerializer(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /beans/src/main/resources/xsd/bindings.xjb: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /beans/src/main/java/ru/qatools/selenograph/gridrouter/Key.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | 5 | import static java.lang.Float.parseFloat; 6 | 7 | /** 8 | * @author Ilya Sadykov (mailto: smecsia@yandex-team.ru) 9 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 10 | */ 11 | public class Key { 12 | 13 | private Key() { 14 | } 15 | 16 | public static String browserVersion(String version) { 17 | return StringUtils.isNumeric(version) ? String.valueOf(parseFloat(version)) : version; 18 | } 19 | 20 | public static String browserName(String name) { 21 | return name; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/ext/SelenographMessagesSerializer.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.ext; 2 | 3 | import ru.yandex.qatools.camelot.common.BasicMessagesSerializer; 4 | 5 | /** 6 | * @author Ilya Sadykov 7 | */ 8 | public class SelenographMessagesSerializer extends BasicMessagesSerializer { 9 | 10 | @Override 11 | public Object deserialize(Object body, ClassLoader classLoader) { 12 | return body; 13 | } 14 | 15 | @Override 16 | public Object serialize(Object body, ClassLoader classLoader) { 17 | return body; 18 | } 19 | 20 | @Override 21 | public String identifyBodyClassName(Object body) { 22 | return (body != null) ? body.getClass().getName() : null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/SessionEventFilter.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import ru.yandex.qatools.camelot.api.CustomFilter; 4 | 5 | import static org.springframework.util.StringUtils.isEmpty; 6 | 7 | /** 8 | * @author Ilya Sadykov 9 | */ 10 | public class SessionEventFilter implements CustomFilter { 11 | @Override 12 | public boolean filter(Object body) { 13 | return body != null && body instanceof SessionEvent 14 | && !isEmpty(((SessionEvent) body).getSessionId()) 15 | && !isEmpty(((SessionEvent) body).getUser()) 16 | && !isEmpty(((SessionEvent) body).getBrowser()) 17 | && !isEmpty(((SessionEvent) body).getVersion()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /server/src/main/frontend/js/util/fetch.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-fetch'; 2 | 3 | function checkStatus(response) { 4 | if (response.ok) { 5 | return response; 6 | } else { 7 | throw new Error(response.statusText); 8 | } 9 | } 10 | 11 | function parseJSON(response) { 12 | return response.json(); 13 | } 14 | 15 | export default function enhancedFetch(url, options = {}) { 16 | options.headers = Object.assign({ 17 | 'Accept': 'application/json', 18 | 'Content-Type': 'application/json' 19 | }, options.headers); 20 | if (typeof options.body !== 'string') { 21 | options.body = JSON.stringify(options.body); 22 | } 23 | return fetch(url, options) 24 | .then(checkStatus) 25 | .then(parseJSON); 26 | } -------------------------------------------------------------------------------- /server/typings/main.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/ext/jackson/SelenographJacksonModule.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.ext.jackson; 2 | 3 | import com.fasterxml.jackson.core.Version; 4 | import com.fasterxml.jackson.databind.Module; 5 | 6 | /** 7 | * @author Ilya Sadykov 8 | */ 9 | public class SelenographJacksonModule extends Module { 10 | @Override 11 | public String getModuleName() { 12 | return "selenograph"; 13 | } 14 | 15 | @Override 16 | public Version version() { 17 | return new Version(1, 0, 0, null, "ru.qatools.selenograph", "selenograph"); 18 | } 19 | 20 | @Override 21 | public void setupModule(SetupContext context) { 22 | context.addSerializers(new SelenographSerializers()); 23 | context.addDeserializers(new SelenographDeserializers()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/typings/browser.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | /// 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | /// 11 | /// 12 | /// 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (C) YANDEX LLC, 2016 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 | -------------------------------------------------------------------------------- /beans/src/main/java/ru/qatools/selenograph/gridrouter/UserBrowser.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * @author Ilya Sadykov 7 | */ 8 | public class UserBrowser extends BrowserContext { 9 | @Override 10 | public boolean equals(Object object) { 11 | return object != null && //NOSONAR 12 | object instanceof UserBrowser && 13 | Objects.equals(((UserBrowser) object).getBrowser(), getBrowser()) && 14 | Objects.equals(((UserBrowser) object).getVersion(), getVersion()) && 15 | Objects.equals(((UserBrowser) object).getUser(), getUser()); 16 | } 17 | 18 | @Override 19 | public int hashCode() { 20 | return String.format("%s%s%s", getUser(), getBrowser(), getVersion()).hashCode(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /service/src/test/java/ru/qatools/selenograph/gridrouter/RegexTest.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.hamcrest.core.Is.is; 6 | import static org.springframework.test.util.MatcherAssertionErrors.assertThat; 7 | import static ru.qatools.selenograph.gridrouter.SessionsAggregator.ROUTE_REGEX; 8 | 9 | /** 10 | * @author Ilya Sadykov 11 | */ 12 | public class RegexTest { 13 | @Test 14 | public void testRegex() throws Exception { 15 | assertThat( 16 | "http://firefox-33.haze.yandex.net:4444".replaceAll(ROUTE_REGEX,"$1"), 17 | is("firefox-33.haze.yandex.net") 18 | ); 19 | assertThat( 20 | "http://firefox-33.haze.yandex.net:4444".replaceAll(ROUTE_REGEX,"$2"), 21 | is("4444") 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /server/src/main/frontend/js/util/websocket.js: -------------------------------------------------------------------------------- 1 | import atmosphere from 'atmosphere.js'; 2 | import debug from 'debug'; 3 | 4 | var log = debug('selenium-face:atmosphere'); 5 | var websocketBaseUrl = '/websocket'; 6 | 7 | export default class Subscription { 8 | constructor(url, callback) { 9 | this.url = websocketBaseUrl + url; 10 | atmosphere.subscribe({ 11 | url: this.url, 12 | contentType: 'application/json', 13 | trackMessageLength: true, 14 | reconnectInterval: 5000, 15 | transport: 'websocket', 16 | onMessage: function(response) { 17 | log(response.responseBody); 18 | callback(JSON.parse(response.responseBody)); 19 | } 20 | }); 21 | } 22 | 23 | destroy() { 24 | atmosphere.unsubscribeUrl(this.url); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/typings/main/ambient/mime/mime.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/cb5206a8ac1c9a3ddfd126f5ecea6729b2361452/mime/mime.d.ts 3 | // Type definitions for mime 4 | // Project: https://github.com/broofa/node-mime 5 | // Definitions by: Jeff Goddard 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | // Imported from: https://github.com/soywiz/typescript-node-definitions/mime.d.ts 9 | 10 | declare module "mime" { 11 | export function lookup(path: string): string; 12 | export function extension(mime: string): string; 13 | export function load(filepath: string): void; 14 | export function define(mimes: Object): void; 15 | 16 | interface Charsets { 17 | lookup(mime: string): string; 18 | } 19 | 20 | export var charsets: Charsets; 21 | export var default_type: string; 22 | } -------------------------------------------------------------------------------- /server/typings/browser/ambient/mime/mime.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/cb5206a8ac1c9a3ddfd126f5ecea6729b2361452/mime/mime.d.ts 3 | // Type definitions for mime 4 | // Project: https://github.com/broofa/node-mime 5 | // Definitions by: Jeff Goddard 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | // Imported from: https://github.com/soywiz/typescript-node-definitions/mime.d.ts 9 | 10 | declare module "mime" { 11 | export function lookup(path: string): string; 12 | export function extension(mime: string): string; 13 | export function load(filepath: string): void; 14 | export function define(mimes: Object): void; 15 | 16 | interface Charsets { 17 | lookup(mime: string): string; 18 | } 19 | 20 | export var charsets: Charsets; 21 | export var default_type: string; 22 | } -------------------------------------------------------------------------------- /beans/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | selenograph 5 | ru.qatools.seleniumkit 6 | 1.3-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | selenograph-beans 11 | 12 | 13 | 14 | org.jvnet.jaxb2_commons 15 | jaxb2-basics-runtime 16 | 17 | 18 | org.apache.commons 19 | commons-lang3 20 | 3.4 21 | 22 | 23 | -------------------------------------------------------------------------------- /service/src/main/resources/META-INF/web-fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | ru.qatools.selenograph.gridrouter.JettyProxyInitializer 9 | 10 | 11 | 12 | authorized 13 | /wd/hub/session 14 | /stats 15 | 16 | 17 | user 18 | 19 | 20 | 21 | BASIC 22 | Selenium Grid Router 23 | 24 | 25 | -------------------------------------------------------------------------------- /server/src/main/java/ru/qatools/selenograph/Application.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph; 2 | 3 | import org.glassfish.jersey.jackson.JacksonFeature; 4 | import org.glassfish.jersey.server.ResourceConfig; 5 | import org.glassfish.jersey.server.internal.scanning.PackageNamesScanner; 6 | import org.glassfish.jersey.server.spring.scope.RequestContextFilter; 7 | import ru.yandex.qatools.camelot.features.LoadPluginResourceFeature; 8 | import ru.yandex.qatools.camelot.web.SystemInfoResource; 9 | 10 | import static java.lang.String.format; 11 | 12 | /** 13 | * @author Ilya Sadykov 14 | */ 15 | public class Application extends ResourceConfig { 16 | 17 | public Application() { 18 | register(RequestContextFilter.class); 19 | register(JacksonFeature.class); 20 | register(LoadPluginResourceFeature.class); 21 | register(SystemInfoResource.class); 22 | registerFinder(packageScanner("resources")); 23 | } 24 | 25 | private PackageNamesScanner packageScanner(String path) { 26 | return new PackageNamesScanner(new String[]{format("%s%s", getClass().getPackage().getName(), path)}, true); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /server/webpack.config.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node*/ 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const cheerio = require('cheerio'); 5 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 6 | const target = path.join(__dirname, 'target/selenograph-server-' + getVersionFromPOM()); 7 | 8 | module.exports = { 9 | entry: ['./src/main/frontend/js/app.jsx'], 10 | output: { 11 | path: target, 12 | filename: 'js/app.js' 13 | }, 14 | devtool: 'source-map', 15 | module: { 16 | loaders: [ 17 | {test: /\.(js|jsx)$/, loader: 'babel', exclude: /node_modules/}, 18 | {test: /\.png$/, loader: 'url', query: {limit: 10000}}, 19 | {test: /\.css$/, loader: 'style!css'}, 20 | {test: /\.scss$/, loader: ['style', 'css', 'sass'].join('!')} 21 | ] 22 | }, 23 | plugins: [ 24 | new HtmlWebpackPlugin({template: './src/main/frontend/index.html'}) 25 | ], 26 | devServer: { 27 | contentBase: target 28 | }, 29 | debug: true, 30 | progress: true 31 | }; 32 | 33 | function getVersionFromPOM() { 34 | const pom = cheerio.load(fs.readFileSync('pom.xml', 'utf8')); 35 | return pom('project > parent > version').text(); 36 | } -------------------------------------------------------------------------------- /beans/src/main/java/ru/qatools/selenograph/gridrouter/WaitAvailableBrowserState.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import java.io.Serializable; 4 | import java.time.Duration; 5 | import java.time.ZonedDateTime; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import static java.time.ZonedDateTime.now; 10 | import static java.util.stream.Collectors.toList; 11 | 12 | /** 13 | * @author Ilya Sadykov 14 | */ 15 | public class WaitAvailableBrowserState extends BrowserContext implements Serializable { //NOSONAR 16 | private Map requestIds = new HashMap<>(); 17 | 18 | public void addRequest(String requestId) { 19 | requestIds.put(requestId, now()); 20 | } 21 | 22 | public void removeRequest(String requestId) { 23 | requestIds.remove(requestId); 24 | } 25 | 26 | public void expireRequestsOlderThan(Duration duration) { 27 | requestIds.entrySet().stream() 28 | .filter(e -> duration.compareTo(Duration.between(e.getValue(), now())) < 0) 29 | .map(Map.Entry::getKey).collect(toList()) 30 | .forEach(this::removeRequest); 31 | } 32 | 33 | public int size() { 34 | return requestIds.size(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/QuotaSummaryClientNotifier.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import ru.qatools.selenograph.ext.SelenographDB; 6 | import ru.yandex.qatools.camelot.api.ClientMessageSender; 7 | import ru.yandex.qatools.camelot.api.annotations.ClientSender; 8 | import ru.yandex.qatools.camelot.api.annotations.Filter; 9 | import ru.yandex.qatools.camelot.api.annotations.OnTimer; 10 | 11 | import javax.inject.Inject; 12 | 13 | /** 14 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 15 | */ 16 | @Filter(instanceOf = {}) 17 | public class QuotaSummaryClientNotifier { 18 | 19 | private static final Logger LOGGER = LoggerFactory.getLogger(QuotaSummaryClientNotifier.class); 20 | @Inject 21 | SelenographDB database; 22 | 23 | @ClientSender 24 | ClientMessageSender client; 25 | 26 | @OnTimer(cron = "${selenograph.gridrouter.clientNotifyCron}", perState = false, skipIfNotCompleted = true) 27 | public void sendUpdatesToClient() { 28 | LOGGER.debug("sending client updates"); 29 | client.send(database.getQuotasSummary()); 30 | LOGGER.debug("successfully sent client updates"); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /server/src/main/resources/camelot-extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | classpath*:camelot-default.properties 13 | classpath*:camelot.properties 14 | classpath*:selenograph-default.properties 15 | classpath*:selenograph.properties 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /service/src/test/java/ru/qatools/selenograph/util/EmbeddedMongodbService.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.util; 2 | 3 | import de.flapdoodle.embed.mongo.distribution.Version; 4 | import ru.yandex.qatools.embed.service.MongoEmbeddedService; 5 | 6 | import java.io.IOException; 7 | 8 | /** 9 | * @author Ilya Sadykov 10 | */ 11 | public class EmbeddedMongodbService extends MongoEmbeddedService { 12 | public EmbeddedMongodbService(String replicaSet, String mongoDatabaseName) throws IOException { 13 | super(replicaSet, mongoDatabaseName); 14 | } 15 | 16 | public EmbeddedMongodbService(String replicaSet, String mongoDatabaseName, String mongoUsername, String mongoPassword, String replSetName) throws IOException { 17 | super(replicaSet, mongoDatabaseName, mongoUsername, mongoPassword, replSetName); 18 | } 19 | 20 | public EmbeddedMongodbService(String replicaSet, String mongoDatabaseName, String mongoUsername, String mongoPassword, String replSetName, String dataDirectory, boolean enabled, int initTimeout) throws IOException { 21 | super(replicaSet, mongoDatabaseName, mongoUsername, mongoPassword, replSetName, dataDirectory, enabled, initTimeout); 22 | } 23 | 24 | public void setVersion(Version.Main version) { 25 | useVersion(version); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /server/src/main/frontend/tools/server.js: -------------------------------------------------------------------------------- 1 | /*eslint-env node */ 2 | const _ = require('lodash'); 3 | const webpack = require('webpack'); 4 | const WebpackDevServer = require('webpack-dev-server'); 5 | const proxy = require('http-proxy').createProxyServer(); 6 | const config = require('../../../../webpack.config'); 7 | const port = 3000 || process.env.PORT; 8 | 9 | config.entry.unshift(`webpack-dev-server/client?http://localhost:${port}`, 'webpack/hot/dev-server'); 10 | config.plugins.push(new webpack.HotModuleReplacementPlugin()); 11 | var compiler = webpack(config); 12 | var server = new WebpackDevServer(compiler, _.assign({}, config.devServer, { 13 | proxy: { 14 | '/api/*': 'http://localhost:8080' 15 | }, 16 | stats: { colors: true }, 17 | inline: true, 18 | hot: true 19 | })); 20 | 21 | server.listeningApp.on('upgrade', function(req, socket) { 22 | if (req.url.match('/websocket')) { 23 | proxy.ws(req, socket, {'target': 'ws://localhost:8080'}); 24 | } 25 | }); 26 | 27 | server.listen(port, function(error) { 28 | if(error) { 29 | console.error(error); //eslint-disable-line no-console 30 | } else { 31 | console.info('==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.', port, port); //eslint-disable-line no-console 32 | } 33 | }); -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/JettyProxyInitializer.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | import javax.servlet.ServletContextEvent; 7 | import javax.servlet.ServletContextListener; 8 | 9 | import static java.util.concurrent.Executors.newFixedThreadPool; 10 | import static org.springframework.web.context.support.WebApplicationContextUtils.getWebApplicationContext; 11 | 12 | /** 13 | * @author Ilya Sadykov 14 | */ 15 | public class JettyProxyInitializer implements ServletContextListener { 16 | private final static Logger LOGGER = LoggerFactory.getLogger(JettyProxyInitializer.class); 17 | 18 | @Override 19 | public void contextInitialized(ServletContextEvent sce) { 20 | LOGGER.info("Servlet context initialized"); 21 | int tpSize = Integer.valueOf(getWebApplicationContext(sce.getServletContext()).getEnvironment() 22 | .getProperty("grid.config.jetty.executor.threads.count", "500")); 23 | sce.getServletContext().setAttribute("org.eclipse.jetty.server.Executor", newFixedThreadPool(tpSize)); 24 | } 25 | 26 | @Override 27 | public void contextDestroyed(ServletContextEvent sce) { 28 | LOGGER.info("Servlet context destroyed"); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/main/frontend/js/components/QuotaSelector.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import DropDownMenu from 'material-ui/lib/DropDownMenu'; 3 | import MenuItem from 'material-ui/lib/menus/menu-item'; 4 | 5 | export default class QuotaSelector extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = {selected: 0}; 10 | this.handleChange = this.handleChange.bind(this); 11 | } 12 | 13 | handleChange(event, index, value) { 14 | this.setState({selected: value}); 15 | if (this.props.onChange) { 16 | this.props.onChange(event.target.textContent); 17 | } 18 | } 19 | 20 | renderItems() { 21 | return this.props.quotas.map((quota, index) => { 22 | return ( 23 | 27 | ); 28 | }); 29 | } 30 | 31 | render() { 32 | return ( 33 | 38 | {this.renderItems()} 39 | 40 | ); 41 | } 42 | } -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/ext/jackson/ObjectMapperProvider.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.ext.jackson; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 7 | 8 | import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY; 9 | import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE; 10 | import static com.fasterxml.jackson.annotation.PropertyAccessor.FIELD; 11 | import static com.fasterxml.jackson.annotation.PropertyAccessor.GETTER; 12 | import static com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping.NON_FINAL; 13 | 14 | /** 15 | * @author Ilya Sadykov 16 | */ 17 | public class ObjectMapperProvider { 18 | 19 | public ObjectMapper provide() { 20 | ObjectMapper result = new ObjectMapper(); 21 | result.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); 22 | result.setSerializationInclusion(JsonInclude.Include.NON_NULL); 23 | result.setVisibility(FIELD, ANY); 24 | result.setVisibility(GETTER, NONE); 25 | result.registerModule(new JavaTimeModule()); 26 | result.registerModule(new SelenographJacksonModule()); 27 | result.enableDefaultTyping(NON_FINAL); 28 | return result; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /service/src/main/resources/camelot.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ru.qatools.selenograph.gridrouter.ApiResource 11 | 12 | 13 | ru.qatools.selenograph.api.InputResource 14 | 15 | 16 | ru.qatools.selenograph.api.ApiResource 17 | 18 | 20 | ru.qatools.selenograph.gridrouter.QuotaSummaryClientNotifier 21 | 22 | 23 | ru.qatools.selenograph.gridrouter.QueueWaitAvailableBrowsersChecker 24 | 25 | 26 | ru.qatools.selenograph.gridrouter.QuotaStatsAggregator 27 | 28 | 29 | ru.yandex.qatools.camelot.plugin.GraphiteReportProcessor 30 | 31 | 32 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "selenograph", 3 | "version": "0.0.1", 4 | "dependencies": { 5 | "atmosphere.js": "~2.3.1", 6 | "fbjs": "~0.7.2", 7 | "isomorphic-fetch": "~2.2.1", 8 | "lodash": "^4.5.1", 9 | "material-design-icons": "^2.2.0", 10 | "material-ui": "^0.14.4", 11 | "material-ui-sass": "^0.7.2", 12 | "react": "^0.14.7", 13 | "react-addons-create-fragment": "^0.14.7", 14 | "react-addons-pure-render-mixin": "^0.14.7", 15 | "react-addons-transition-group": "^0.14.7", 16 | "react-addons-update": "^0.14.7", 17 | "react-dom": "^0.14.7", 18 | "react-tap-event-plugin": "^0.2.1", 19 | "whatwg-fetch": "~0.11.0" 20 | }, 21 | "devDependencies": { 22 | "babel-core": "^6.3.26", 23 | "babel-eslint": "^5.0.0-beta6", 24 | "babel-loader": "^6.2.0", 25 | "babel-plugin-react-require": "^2.1.0", 26 | "babel-plugin-transform-class-properties": "^6.5.2", 27 | "babel-preset-es2015": "^6.3.13", 28 | "babel-preset-react": "^6.3.13", 29 | "babel-runtime": "^6.3.19", 30 | "cheerio": "~0.20.0", 31 | "css-loader": "~0.23.1", 32 | "eslint": "^2.2.0", 33 | "eslint-plugin-react": "^4.0.0", 34 | "file-loader": "^0.8.5", 35 | "html-webpack-plugin": "^2.9.0", 36 | "sass-loader": "~3.1.2", 37 | "style-loader": "~0.13.0", 38 | "url-loader": "^0.5.7", 39 | "webpack": "^1.12.14", 40 | "webpack-dev-server": "^1.14.1" 41 | }, 42 | "scripts": { 43 | "build": "webpack", 44 | "start": "node src/main/frontend/tools/server.js", 45 | "test": "eslint src/main/frontend --ext .js,.jsx" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /service/src/test/java/ru/qatools/selenograph/util/FullStacktraceThrowableRendererTest.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.util; 2 | 3 | import org.apache.commons.lang3.NotImplementedException; 4 | import org.junit.Test; 5 | 6 | import static java.util.Arrays.asList; 7 | import static org.hamcrest.MatcherAssert.assertThat; 8 | import static org.hamcrest.core.IsCollectionContaining.hasItem; 9 | 10 | /** 11 | * @author Ilya Sadykov 12 | */ 13 | public class FullStacktraceThrowableRendererTest { 14 | 15 | 16 | @Test(expected = RuntimeException.class) 17 | public void testRender() throws Exception { 18 | try { 19 | callNotImplementedMethod(); 20 | } catch (NotImplementedException e) { 21 | final String[] rendered = new FullStacktraceThrowableRenderer().doRender(e); 22 | assertThat(asList(rendered), 23 | hasItem("org.apache.commons.lang3.NotImplementedException: java.lang.IllegalArgumentException: Illegal!")); 24 | assertThat(asList(rendered), 25 | hasItem("\tat ru.qatools.selenograph.util.FullStacktraceThrowableRendererTest." + 26 | "callIllegalArgumentMethod(FullStacktraceThrowableRendererTest.java:40)")); 27 | throw e; 28 | } 29 | } 30 | 31 | private void callNotImplementedMethod() { 32 | try { 33 | callIllegalArgumentMethod(); 34 | } catch (IllegalArgumentException e) { 35 | throw new NotImplementedException(e); 36 | } 37 | } 38 | 39 | private void callIllegalArgumentMethod() { 40 | throw new IllegalArgumentException("Illegal!"); 41 | } 42 | } -------------------------------------------------------------------------------- /beans/src/main/resources/xsd/front.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 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/api/ApiResource.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.api; 2 | 3 | import ru.yandex.qatools.camelot.api.PluginsInterop; 4 | import ru.yandex.qatools.camelot.api.annotations.Plugins; 5 | 6 | import javax.ws.rs.GET; 7 | import javax.ws.rs.Path; 8 | import javax.ws.rs.PathParam; 9 | import javax.ws.rs.Produces; 10 | import java.util.Collection; 11 | import java.util.Map; 12 | 13 | import static javax.ws.rs.core.MediaType.APPLICATION_JSON; 14 | 15 | /** 16 | * @author Ilya Sadykov (mailto: smecsia@yandex-team.ru) 17 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 18 | */ 19 | @Path("/selenograph") 20 | public class ApiResource { 21 | 22 | @Plugins 23 | PluginsInterop plugins; 24 | 25 | @GET 26 | @Path("/count/{pluginId}") 27 | @Produces({APPLICATION_JSON}) 28 | public int getCount(@PathParam("pluginId") String pluginId) { 29 | return plugins.repo(pluginId).keys().size(); 30 | } 31 | 32 | @GET 33 | @Path("/map/{pluginId}") 34 | @Produces({APPLICATION_JSON}) 35 | public Map getMap(@PathParam("pluginId") String pluginId) { 36 | return plugins.repo(pluginId).valuesMap(); 37 | } 38 | 39 | @GET 40 | @Path("/list/{pluginId}") 41 | @Produces({APPLICATION_JSON}) 42 | public Collection getList(@PathParam("pluginId") String pluginId) { 43 | return getMap(pluginId).values(); 44 | } 45 | 46 | @GET 47 | @Path("/map/{pluginId}/{key}") 48 | @Produces({APPLICATION_JSON}) 49 | public Object getState(@PathParam("pluginId") String pluginId, @PathParam("key") String key) { 50 | return plugins.repo(pluginId).get(key); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /service/src/test/java/ru/qatools/selenograph/utils/TestProperties.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.utils; 2 | 3 | import ru.yandex.qatools.properties.PropertyLoader; 4 | import ru.yandex.qatools.properties.annotations.Property; 5 | 6 | import static ru.yandex.qatools.properties.utils.PropertiesUtils.readProperties; 7 | 8 | /** 9 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 10 | */ 11 | @SuppressWarnings("FieldCanBeLocal") 12 | public class TestProperties { 13 | 14 | public TestProperties() { 15 | PropertyLoader.populate(this); 16 | } 17 | 18 | public TestProperties(String resourceName) { 19 | PropertyLoader.populate(this, readProperties(ClassLoader.getSystemResourceAsStream(resourceName))); 20 | } 21 | 22 | @Property("selenograph.test.timeout.in.seconds") 23 | private int timeout = 3; 24 | 25 | @Property("selenograph.browserStartsHistoryLength") 26 | private int browserStartsHistory; 27 | 28 | @Property("selenograph.criticalBrowserStartFailuresPercentage") 29 | private int criticalBrowserStartFailuresPercentage; 30 | 31 | @Property("selenograph.openstack.wait.after.termination") 32 | private int waitAfterTermination; 33 | 34 | /** 35 | * @return timeout in milliseconds 36 | */ 37 | public int getTimeout() { 38 | return timeout * 1000; 39 | } 40 | 41 | public int getBrowserStartsHistory() { 42 | return browserStartsHistory; 43 | } 44 | 45 | public int getCriticalBrowserStartFailuresPercentage() { 46 | return criticalBrowserStartFailuresPercentage; 47 | } 48 | 49 | public int getWaitAfterTermination() { 50 | return waitAfterTermination; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /service/src/test/resources/camelot-extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /beans/src/main/java/ru/qatools/selenograph/gridrouter/Timestamped.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.jvnet.jaxb2_commons.lang.*; 4 | import org.jvnet.jaxb2_commons.locator.ObjectLocator; 5 | 6 | import java.io.Serializable; 7 | 8 | import static java.lang.System.currentTimeMillis; 9 | 10 | /** 11 | * @author Ilya Sadykov 12 | */ 13 | public class Timestamped implements Serializable, Equals, HashCode, ToString { 14 | private long timestamp = currentTimeMillis(); 15 | 16 | public long getTimestamp() { 17 | return timestamp; 18 | } 19 | 20 | public void setTimestamp(long timestamp) { 21 | this.timestamp = timestamp; 22 | } 23 | 24 | @SuppressWarnings("unchecked") 25 | public T withTimestamp(long timestamp) { 26 | setTimestamp(timestamp); 27 | return (T) this; 28 | } 29 | 30 | @Override 31 | public boolean equals(ObjectLocator thisLocator, ObjectLocator thatLocator, Object that, EqualsStrategy strategy) { 32 | return (that instanceof Timestamped && ((Timestamped) that).timestamp == this.timestamp); 33 | } 34 | 35 | @Override 36 | public int hashCode(ObjectLocator thisLocator, HashCodeStrategy strategy) { 37 | return Long.valueOf(timestamp).hashCode(); 38 | } 39 | 40 | @Override 41 | public StringBuilder append(ObjectLocator locator, StringBuilder builder, ToStringStrategy strategy) { 42 | strategy.appendStart(locator, this, builder); 43 | appendFields(locator, builder, strategy); 44 | strategy.appendEnd(locator, this, builder); 45 | return builder; 46 | } 47 | 48 | @Override 49 | public StringBuilder appendFields(ObjectLocator locator, StringBuilder builder, ToStringStrategy strategy) { 50 | strategy.appendField(locator, this, "timestamp", builder, timestamp); 51 | return builder; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/util/FullStacktraceThrowableRenderer.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.util; 2 | 3 | import org.apache.commons.lang3.ArrayUtils; 4 | import org.apache.log4j.EnhancedThrowableRenderer; 5 | import org.apache.log4j.spi.ThrowableRenderer; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | /** 11 | * @author Ilya Sadykov 12 | */ 13 | public class FullStacktraceThrowableRenderer implements ThrowableRenderer { 14 | private static final EnhancedThrowableRenderer RENDERER = new EnhancedThrowableRenderer(); 15 | 16 | private static String[] formatStackTrace(Throwable exc) { 17 | Throwable cause = exc; 18 | final List result = new ArrayList<>(); 19 | result.add("=============== Full stacktrace follows: ==============="); 20 | while (cause != null) { 21 | result.add("Caused by " + cause.toString() + ": " + cause.getMessage()); 22 | result.addAll(formatStackTrace(cause.getStackTrace())); 23 | cause = cause.getCause(); 24 | } 25 | return result.stream().toArray(String[]::new); 26 | } 27 | 28 | private static List formatStackTrace(StackTraceElement[] stackTraceElements) { 29 | final List result = new ArrayList<>(); 30 | for (StackTraceElement element : stackTraceElements) { 31 | final StringBuilder builder = new StringBuilder(); 32 | builder.append("\tat ").append(element.getClassName()). 33 | append(".").append(element.getMethodName()); 34 | builder.append("(").append(element.getFileName()); 35 | if (element.getLineNumber() > 0) { 36 | builder.append(":").append(element.getLineNumber()); 37 | } 38 | builder.append(")"); 39 | result.add(builder.toString()); 40 | } 41 | return result; 42 | } 43 | 44 | @Override 45 | public String[] doRender(Throwable t) { 46 | return ArrayUtils.addAll(RENDERER.doRender(t), formatStackTrace(t)); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/api/InputResource.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.api; 2 | 3 | import org.apache.camel.CamelContext; 4 | import org.apache.camel.component.seda.QueueReference; 5 | import org.apache.camel.component.seda.SedaComponent; 6 | 7 | import javax.inject.Inject; 8 | import javax.ws.rs.*; 9 | import javax.ws.rs.core.MediaType; 10 | import javax.ws.rs.core.Response; 11 | import java.util.Map; 12 | import java.util.concurrent.atomic.AtomicLong; 13 | 14 | import static java.util.stream.Collectors.toList; 15 | import static javax.ws.rs.core.MediaType.TEXT_PLAIN; 16 | import static javax.ws.rs.core.Response.ok; 17 | 18 | /** 19 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 20 | */ 21 | @Path("/") 22 | public class InputResource { 23 | private static final AtomicLong MESSAGES_COUNT = new AtomicLong(); 24 | @Inject 25 | CamelContext camelContext; 26 | 27 | @PUT 28 | @Path("/events") 29 | @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) 30 | public Response sendMessage(String message) { 31 | MESSAGES_COUNT.incrementAndGet(); 32 | return ok("ok").build(); 33 | } 34 | 35 | @GET 36 | @Path("/perform") 37 | public Response resetDelayedQueues(@QueryParam("action") String action, @QueryParam("object") String object) { 38 | switch (action) { 39 | case "clearQueue": 40 | getSedaQueues().get(object).getQueue().clear(); 41 | break; 42 | case "printQueue": 43 | return ok() 44 | .header("Content-Type", "application/json") 45 | .entity(getSedaQueues().get(object).getQueue().parallelStream().collect(toList())) 46 | .build(); 47 | } 48 | return ok("ok").build(); 49 | } 50 | 51 | private Map getSedaQueues() { 52 | return camelContext.getComponent("seda", SedaComponent.class).getQueues(); 53 | } 54 | 55 | 56 | @GET 57 | @Path("/events/count") 58 | @Produces({TEXT_PLAIN}) 59 | public Long getCount() { 60 | return MESSAGES_COUNT.get(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /beans/src/main/java/ru/qatools/selenograph/gridrouter/BrowserSummaries.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import ru.qatools.selenograph.front.BrowserSummary; 4 | import ru.qatools.selenograph.front.VersionSummary; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | import static ru.qatools.selenograph.gridrouter.Key.browserVersion; 11 | 12 | /** 13 | * @author Ilya Sadykov 14 | */ 15 | public class BrowserSummaries extends ArrayList implements List { 16 | 17 | public void addOrIncrement(Map availableMap, 18 | Map runningMap) { 19 | availableMap.entrySet().forEach(e -> { //NOSONAR 20 | final BrowserContext bc = e.getKey(); 21 | int max = e.getValue(); 22 | int running = runningMap.getOrDefault(e.getKey(), 0); 23 | final BrowserSummary summary = parallelStream() 24 | .filter(bs -> bs.getName().equals(e.getKey().getBrowser())) 25 | .findAny().orElseGet(() -> { 26 | final BrowserSummary bs = new BrowserSummary().withName(bc.getBrowser()); 27 | add(bs); 28 | return bs; 29 | }); 30 | final VersionSummary version = summary.getVersions().parallelStream() 31 | .filter(v -> v.getVersion().equals(bc.getVersion())) 32 | .findAny().orElseGet(() -> { 33 | final VersionSummary vs = new VersionSummary().withVersion(browserVersion(bc.getVersion())); 34 | summary.getVersions().add(vs); 35 | return vs; 36 | }); 37 | summary.withMax(summary.getMax() + max); 38 | summary.withRunning(summary.getRunning() + running); 39 | version.withMax(version.getMax() + max); 40 | version.withRunning(version.getRunning() + running); 41 | }); 42 | } 43 | 44 | public void sort() { 45 | forEach(s -> s.getVersions().sort((v1, v2) -> v1.getVersion().compareTo(v2.getVersion()))); 46 | sort((s1, s2) -> s1.getName().compareTo(s2.getName())); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /server/src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | contextConfigLocation 8 | classpath:camelot-web-context.xml 9 | 10 | 11 | org.springframework.web.context.ContextLoaderListener 12 | 13 | 14 | 15 | SelenographServlet 16 | ru.yandex.qatools.camelot.spring.ServletContainer 17 | 18 | javax.ws.rs.Application 19 | ru.qatools.selenograph.Application 20 | 21 | true 22 | 1 23 | 24 | 25 | SelenographServlet 26 | /api/* 27 | 28 | 29 | 30 | 31 | SelenographWebSocket 32 | ru.yandex.qatools.camelot.web.core.AtmosphereServlet 33 | 34 | org.atmosphere.cpr.packages 35 | ru.yandex.qatools.camelot.web.core 36 | 37 | 38 | org.atmosphere.websocket.messageContentType 39 | application/json 40 | 41 | true 42 | 0 43 | 44 | 45 | SelenographWebSocket 46 | /websocket/* 47 | 48 | 49 | index.html 50 | 51 | 52 | -------------------------------------------------------------------------------- /service/src/main/resources/camelot-extensions.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/ext/SelenographMongoSerializer.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.ext; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.mongodb.BasicDBList; 5 | import com.mongodb.BasicDBObject; 6 | import com.mongodb.MongoException; 7 | import com.mongodb.util.JSON; 8 | import org.bson.Document; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | import ru.qatools.selenograph.ext.jackson.ObjectMapperProvider; 12 | import ru.yandex.qatools.camelot.mongodb.MongoSerializer; 13 | 14 | import java.util.List; 15 | 16 | /** 17 | * @author Ilya Sadykov 18 | */ 19 | @SuppressWarnings({"deprecation", "unchecked"}) 20 | public class SelenographMongoSerializer implements MongoSerializer { 21 | public static final String OBJECT_FIELD = "object"; 22 | private static final Logger LOGGER = LoggerFactory.getLogger(SelenographMongoSerializer.class); 23 | private final ObjectMapper objectMapper; 24 | 25 | @SuppressWarnings("deprecation") 26 | public SelenographMongoSerializer() { 27 | this.objectMapper = new ObjectMapperProvider().provide(); 28 | } 29 | 30 | /** 31 | * Serialize the object to bytes 32 | */ 33 | @Override 34 | public BasicDBObject toDBObject(Object object) { 35 | try { 36 | return new BasicDBObject( 37 | OBJECT_FIELD, JSON.parse(objectMapper.writeValueAsString(object)) 38 | ); 39 | } catch (Exception e) { 40 | LOGGER.error("Failed to serialize object to basic db object", e); 41 | return new BasicDBObject(); //NOSONAR 42 | } 43 | } 44 | 45 | /** 46 | * Deserialize the input bytes into object 47 | */ 48 | @Override 49 | @SuppressWarnings("unchecked") 50 | public T fromDBObject(Document input, Class expected) 51 | throws Exception { //NOSONAR 52 | try { 53 | if (input == null) { 54 | return null; 55 | } 56 | final BasicDBList list = new BasicDBList(); 57 | list.addAll((List) input.get(OBJECT_FIELD)); 58 | return (T) objectMapper.readValue(list.toString(), expected); 59 | } catch (Exception e) { 60 | throw new MongoException("Unknown error occurred converting BSON to object", e); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /server/typings/main/ambient/react-dom/react-dom.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/ca5bfe76d2d9bf6852cbc712d9f3e0047c93486e/react/react-dom.d.ts 3 | // Type definitions for React v0.14 (react-dom) 4 | // Project: http://facebook.github.io/react/ 5 | // Definitions by: Asana , AssureSign , Microsoft 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | 9 | declare namespace __React { 10 | namespace __DOM { 11 | function findDOMNode(instance: ReactInstance): E; 12 | function findDOMNode(instance: ReactInstance): Element; 13 | 14 | function render

( 15 | element: DOMElement

, 16 | container: Element, 17 | callback?: (element: Element) => any): Element; 18 | function render( 19 | element: ClassicElement

, 20 | container: Element, 21 | callback?: (component: ClassicComponent) => any): ClassicComponent; 22 | function render( 23 | element: ReactElement

, 24 | container: Element, 25 | callback?: (component: Component) => any): Component; 26 | 27 | function unmountComponentAtNode(container: Element): boolean; 28 | 29 | var version: string; 30 | 31 | function unstable_batchedUpdates(callback: (a: A, b: B) => any, a: A, b: B): void; 32 | function unstable_batchedUpdates(callback: (a: A) => any, a: A): void; 33 | function unstable_batchedUpdates(callback: () => any): void; 34 | 35 | function unstable_renderSubtreeIntoContainer

( 36 | parentComponent: Component, 37 | nextElement: DOMElement

, 38 | container: Element, 39 | callback?: (element: Element) => any): Element; 40 | function unstable_renderSubtreeIntoContainer( 41 | parentComponent: Component, 42 | nextElement: ClassicElement

, 43 | container: Element, 44 | callback?: (component: ClassicComponent) => any): ClassicComponent; 45 | function unstable_renderSubtreeIntoContainer( 46 | parentComponent: Component, 47 | nextElement: ReactElement

, 48 | container: Element, 49 | callback?: (component: Component) => any): Component; 50 | } 51 | 52 | namespace __DOMServer { 53 | function renderToString(element: ReactElement): string; 54 | function renderToStaticMarkup(element: ReactElement): string; 55 | var version: string; 56 | } 57 | } 58 | 59 | declare module "react-dom" { 60 | import DOM = __React.__DOM; 61 | export = DOM; 62 | } 63 | 64 | declare module "react-dom/server" { 65 | import DOMServer = __React.__DOMServer; 66 | export = DOMServer; 67 | } -------------------------------------------------------------------------------- /server/typings/browser/ambient/react-dom/react-dom.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/ca5bfe76d2d9bf6852cbc712d9f3e0047c93486e/react/react-dom.d.ts 3 | // Type definitions for React v0.14 (react-dom) 4 | // Project: http://facebook.github.io/react/ 5 | // Definitions by: Asana , AssureSign , Microsoft 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | 9 | declare namespace __React { 10 | namespace __DOM { 11 | function findDOMNode(instance: ReactInstance): E; 12 | function findDOMNode(instance: ReactInstance): Element; 13 | 14 | function render

( 15 | element: DOMElement

, 16 | container: Element, 17 | callback?: (element: Element) => any): Element; 18 | function render( 19 | element: ClassicElement

, 20 | container: Element, 21 | callback?: (component: ClassicComponent) => any): ClassicComponent; 22 | function render( 23 | element: ReactElement

, 24 | container: Element, 25 | callback?: (component: Component) => any): Component; 26 | 27 | function unmountComponentAtNode(container: Element): boolean; 28 | 29 | var version: string; 30 | 31 | function unstable_batchedUpdates(callback: (a: A, b: B) => any, a: A, b: B): void; 32 | function unstable_batchedUpdates(callback: (a: A) => any, a: A): void; 33 | function unstable_batchedUpdates(callback: () => any): void; 34 | 35 | function unstable_renderSubtreeIntoContainer

( 36 | parentComponent: Component, 37 | nextElement: DOMElement

, 38 | container: Element, 39 | callback?: (element: Element) => any): Element; 40 | function unstable_renderSubtreeIntoContainer( 41 | parentComponent: Component, 42 | nextElement: ClassicElement

, 43 | container: Element, 44 | callback?: (component: ClassicComponent) => any): ClassicComponent; 45 | function unstable_renderSubtreeIntoContainer( 46 | parentComponent: Component, 47 | nextElement: ReactElement

, 48 | container: Element, 49 | callback?: (component: Component) => any): Component; 50 | } 51 | 52 | namespace __DOMServer { 53 | function renderToString(element: ReactElement): string; 54 | function renderToStaticMarkup(element: ReactElement): string; 55 | var version: string; 56 | } 57 | } 58 | 59 | declare module "react-dom" { 60 | import DOM = __React.__DOM; 61 | export = DOM; 62 | } 63 | 64 | declare module "react-dom/server" { 65 | import DOMServer = __React.__DOMServer; 66 | export = DOMServer; 67 | } -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/SessionsCountsPerUser.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import ru.qatools.gridrouter.sessions.GridRouterUserStats; 4 | 5 | import java.io.Serializable; 6 | import java.util.HashMap; 7 | import java.util.Map; 8 | 9 | import static java.lang.Math.round; 10 | import static java.lang.String.format; 11 | 12 | /** 13 | * @author Ilya Sadykov 14 | */ 15 | public class SessionsCountsPerUser extends HashMap implements Serializable, GridRouterUserStats { 16 | 17 | public SessionsCountsPerUser(Map counts) { 18 | resetStats(counts); 19 | } 20 | 21 | public SessionsCountsPerUser() { 22 | // need to have no-args constructor 23 | } 24 | 25 | public static BrowserContext fromBrowserString(String browser) { 26 | final String[] parts = browser.replaceAll("\\[DOT\\]", ".").split(":"); 27 | return new UserBrowser().withUser(parts[0]).withBrowser(parts[1]).withVersion(parts[2]); 28 | } 29 | 30 | public static String toBrowserString(BrowserContext browser) { 31 | return toBrowserString(browser.getUser(), browser.getBrowser(), browser.getVersion()); 32 | } 33 | 34 | public static String toBrowserString(String user, String browser, String version) { 35 | return format("%s:%s:%s", user, browser, version).replaceAll("\\.", "[DOT]"); 36 | } 37 | 38 | public SessionsState getFor(String user, String browser, String version) { 39 | return get(toBrowserString(user, browser, version)); 40 | } 41 | 42 | public void updateStats(SessionsCountsPerUser counts) { 43 | counts.entrySet().forEach(count -> { 44 | putIfAbsent(count.getKey(), new SessionsState()); 45 | final SessionsState state = get(count.getKey()); 46 | state.setRaw(count.getValue().getRaw()); 47 | state.setMax(state.getRaw() > state.getMax() ? state.getRaw() : state.getMax()); 48 | state.setAvg(round(((float) state.getAvg() + (float) state.getRaw()) / 2.0f)); 49 | state.setBrowser(count.getValue().getBrowser()); 50 | state.setVersion(count.getValue().getVersion()); 51 | state.setUser(count.getValue().getUser()); 52 | }); 53 | } 54 | 55 | public void resetStats(Map counts) { 56 | counts.entrySet().forEach(e -> { 57 | final BrowserContext browser = e.getKey(); 58 | final Integer count = e.getValue(); 59 | putIfAbsent(toBrowserString(browser), new SessionsState()); 60 | final SessionsState state = get(toBrowserString(browser)); 61 | state.setRaw(count); 62 | state.setAvg(count); 63 | state.setMax(count); 64 | state.setBrowser(browser.getBrowser()); 65 | state.setVersion(browser.getVersion()); 66 | state.setUser(browser.getUser()); 67 | state.setTimestamp(browser.getTimestamp()); 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/ext/jackson/SelenographDeserializers.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.ext.jackson; 2 | 3 | import com.fasterxml.jackson.core.JsonParser; 4 | import com.fasterxml.jackson.core.JsonToken; 5 | import com.fasterxml.jackson.databind.*; 6 | import com.fasterxml.jackson.databind.deser.Deserializers; 7 | import com.fasterxml.jackson.databind.deser.std.DateDeserializers; 8 | import com.fasterxml.jackson.databind.deser.std.NumberDeserializers; 9 | import com.fasterxml.jackson.databind.deser.std.StdDeserializer; 10 | 11 | import java.io.IOException; 12 | import java.util.Date; 13 | 14 | import static ru.yandex.qatools.camelot.util.TypesUtil.isLong; 15 | 16 | /** 17 | * @author Ilya Sadykov 18 | */ 19 | public class SelenographDeserializers extends Deserializers.Base { 20 | 21 | @Override 22 | public JsonDeserializer findBeanDeserializer(JavaType type, DeserializationConfig config, BeanDescription beanDesc) throws JsonMappingException { 23 | if (isLong(type.getRawClass())) { 24 | return new LongDeserializer(); 25 | } 26 | if (type.getRawClass().equals(Date.class)) { 27 | return new DateDeserializer(); 28 | } 29 | return super.findBeanDeserializer(type, config, beanDesc); 30 | } 31 | 32 | private static class LongDeserializer extends StdDeserializer { 33 | NumberDeserializers.LongDeserializer longDeserializer = //NOSONAR 34 | new NumberDeserializers.LongDeserializer(Long.class, 0L); 35 | 36 | LongDeserializer() { 37 | super(Long.class); 38 | } 39 | 40 | @Override 41 | public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { 42 | final boolean array = p.hasToken(JsonToken.START_ARRAY); 43 | if (p.hasToken(JsonToken.START_OBJECT) || array) { 44 | p.nextToken(); 45 | long value; 46 | if(array){ 47 | p.nextToken(); 48 | value = Long.parseLong(p.getText()); 49 | } else { 50 | value = Long.parseLong(p.nextTextValue()); 51 | } 52 | p.nextToken(); 53 | return value; 54 | } 55 | return longDeserializer.deserialize(p, ctxt); 56 | } 57 | } 58 | 59 | private static class DateDeserializer extends StdDeserializer { 60 | DateDeserializers.DateDeserializer dateDeserializer = new DateDeserializers.DateDeserializer(); //NOSONAR 61 | 62 | protected DateDeserializer() { 63 | super(Date.class); 64 | } 65 | 66 | @Override 67 | public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { 68 | if (p.hasToken(JsonToken.START_OBJECT)) { 69 | p.nextToken(); 70 | p.nextToken(); 71 | long value = Long.parseLong(p.getText()); 72 | p.nextToken(); 73 | return new Date(value); 74 | } 75 | return dateDeserializer.deserialize(p, ctxt); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /service/src/test/java/ru/qatools/selenograph/gridrouter/QueueWaitAvailableBrowsersCheckerTest.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import ru.qatools.gridrouter.config.Version; 8 | import ru.qatools.selenograph.ext.SelenographDB; 9 | import ru.yandex.qatools.camelot.test.*; 10 | 11 | import static com.jayway.awaitility.Awaitility.await; 12 | import static java.lang.Thread.sleep; 13 | import static java.util.UUID.randomUUID; 14 | import static java.util.concurrent.TimeUnit.SECONDS; 15 | import static java.util.stream.IntStream.rangeClosed; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.Matchers.equalTo; 18 | import static org.hamcrest.Matchers.notNullValue; 19 | import static org.mockito.Matchers.any; 20 | import static org.mockito.Mockito.timeout; 21 | import static org.mockito.Mockito.verify; 22 | 23 | /** 24 | * @author Ilya Sadykov 25 | */ 26 | @DisableTimers 27 | @RunWith(CamelotTestRunner.class) 28 | public class QueueWaitAvailableBrowsersCheckerTest { 29 | 30 | @Helper 31 | TestHelper helper; 32 | 33 | @Autowired 34 | QueueWaitAvailableBrowsersChecker queue; 35 | 36 | @PluginMock 37 | QueueWaitAvailableBrowsersChecker mock; 38 | 39 | @AggregatorState(QueueWaitAvailableBrowsersChecker.class) 40 | AggregatorStateStorage repo; 41 | 42 | @Autowired 43 | SelenographDB database; 44 | 45 | @Before 46 | public void setUp() throws Exception { 47 | database.clear(); 48 | } 49 | 50 | @Test 51 | public void testCountEnqueuedRequests() throws Exception { 52 | final String reqId = randomUUID().toString(); 53 | rangeClosed(0, 2).forEach(i -> queue.onWait("user", "firefox", version("33"), reqId, i)); 54 | verify(mock, timeout(4000L).times(1)).onBeforeRequest(any(), any()); 55 | verify(mock, timeout(4000L).times(1)).onEnqueued(any(), any()); 56 | await().atMost(4, SECONDS).until(() -> state("user-firefox-33"), notNullValue()); 57 | 58 | assertThat(state("user-firefox-33").size(), equalTo(1)); 59 | 60 | queue.onWaitFinished("user", "firefox", version("33"), reqId, 3); 61 | verify(mock, timeout(2000L).times(1)).onDequeued(any(), any()); 62 | 63 | await().atMost(2, SECONDS).until(() -> state("user-firefox-33").size(), equalTo(0)); 64 | } 65 | 66 | @Test 67 | public void testExpiredRequestsRemoval() throws Exception { 68 | queue.onWait("user", "firefox", version("33"), "reqId", 0); 69 | await().atMost(4, SECONDS).until(() -> state("user-firefox-33"), notNullValue()); 70 | assertThat(state("user-firefox-33").size(), equalTo(1)); 71 | sleep(1000); 72 | helper.invokeTimersFor(QueueWaitAvailableBrowsersChecker.class); 73 | await().atMost(2, SECONDS).until(() -> state("user-firefox-33").size(), equalTo(0)); 74 | } 75 | 76 | private WaitAvailableBrowserState state(String key) { 77 | return repo.getActual(key); 78 | } 79 | 80 | private Version version(String number) { 81 | final Version version = new Version(); 82 | version.setNumber(number); 83 | return version; 84 | } 85 | } -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/ApiResource.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import ru.qatools.gridrouter.config.HostSelectionStrategy; 5 | import ru.qatools.selenograph.ext.SelenographDB; 6 | import ru.yandex.qatools.camelot.api.AggregatorRepository; 7 | import ru.yandex.qatools.camelot.api.annotations.Repository; 8 | 9 | import javax.ws.rs.GET; 10 | import javax.ws.rs.Path; 11 | import javax.ws.rs.PathParam; 12 | import javax.ws.rs.Produces; 13 | import java.io.IOException; 14 | import java.text.SimpleDateFormat; 15 | import java.util.Date; 16 | import java.util.List; 17 | import java.util.Map; 18 | 19 | import static java.util.Collections.emptyMap; 20 | import static java.util.stream.Collectors.toList; 21 | import static javax.ws.rs.core.MediaType.APPLICATION_JSON; 22 | import static ru.yandex.qatools.camelot.util.MapUtil.map; 23 | 24 | /** 25 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 26 | */ 27 | @Path("/selenograph") 28 | public class ApiResource { 29 | 30 | private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("MMM,dd HH:mm:ss.SSS"); 31 | 32 | @Repository(QueueWaitAvailableBrowsersChecker.class) 33 | AggregatorRepository queueRepo; 34 | 35 | @Autowired 36 | SelenographDB database; 37 | 38 | @Autowired 39 | @SuppressWarnings("SpringJavaAutowiredMembersInspection") 40 | private HostSelectionStrategy strategy; 41 | 42 | @GET 43 | @Path("/strategy") 44 | @Produces({APPLICATION_JSON}) 45 | public StrategyData getStrategy() throws IOException { 46 | return new StrategyData(strategy); 47 | } 48 | 49 | @GET 50 | @Path("/queues") 51 | @Produces({APPLICATION_JSON}) 52 | public List getQueues() throws IOException { 53 | return queueRepo.valuesMap().entrySet().stream().map(e -> 54 | map(e.getKey(), map( 55 | "browser", e.getValue().getBrowser(), 56 | "version", e.getValue().getVersion(), 57 | "user", e.getValue().getUser(), 58 | "count", e.getValue().size() 59 | ))).collect(toList()); 60 | } 61 | 62 | @GET 63 | @Path("/quotas") 64 | @Produces({APPLICATION_JSON}) 65 | public Map getQuotas() throws IOException { 66 | return database.getQuotasSummary(); 67 | } 68 | 69 | @GET 70 | @Path("/quota/{quotaName}") 71 | @Produces({APPLICATION_JSON}) 72 | public BrowserSummaries getQuota(@PathParam("quotaName") String quotaName) throws IOException { 73 | return database.getQuotasSummary().get(quotaName); 74 | } 75 | 76 | private class StrategyData { 77 | public final String lastUpdated; 78 | public final Map hubs; 79 | 80 | public StrategyData(HostSelectionStrategy strategy) { 81 | if (strategy != null && strategy instanceof UpdatableSelectionStrategy) { 82 | final UpdatableSelectionStrategy smartStrategy = ((UpdatableSelectionStrategy) strategy); 83 | lastUpdated = DATE_FORMAT.format(new Date(smartStrategy.getTimestamp())); 84 | hubs = smartStrategy.getHubs(); 85 | } else { 86 | lastUpdated = "NOT APPLICABLE"; 87 | hubs = emptyMap(); 88 | } 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/QuotaStatsAggregator.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import ru.qatools.selenograph.ext.SelenographDB; 6 | import ru.yandex.qatools.camelot.api.EventProducer; 7 | import ru.yandex.qatools.camelot.api.annotations.*; 8 | import ru.yandex.qatools.camelot.plugin.GraphiteReportProcessor; 9 | import ru.yandex.qatools.camelot.plugin.GraphiteValue; 10 | import ru.yandex.qatools.fsm.annotations.AfterTransit; 11 | import ru.yandex.qatools.fsm.annotations.FSM; 12 | import ru.yandex.qatools.fsm.annotations.Transit; 13 | import ru.yandex.qatools.fsm.annotations.Transitions; 14 | 15 | import javax.inject.Inject; 16 | import java.util.Map; 17 | 18 | import static java.lang.String.format; 19 | import static java.lang.System.currentTimeMillis; 20 | import static ru.qatools.selenograph.gridrouter.SessionsCountsPerUser.fromBrowserString; 21 | 22 | /** 23 | * @author Ilya Sadykov 24 | */ 25 | @Aggregate 26 | @FSM(start = SessionsCountsPerUser.class) 27 | @Filter(instanceOf = SessionsCountsPerUser.class) 28 | @Transitions(@Transit(on = SessionsCountsPerUser.class)) 29 | public class QuotaStatsAggregator { 30 | private static final Logger LOGGER = LoggerFactory.getLogger(QuotaStatsAggregator.class); 31 | @Inject 32 | SessionsAggregator sessions; 33 | 34 | @Inject 35 | SelenographDB database; 36 | 37 | @Input 38 | EventProducer input; 39 | 40 | @ConfigValue("selenograph.gridrouter.graphite.prefix") 41 | String graphitePrefix; 42 | 43 | @Input(GraphiteReportProcessor.class) 44 | EventProducer graphite; 45 | 46 | @AfterTransit 47 | public void updateStats(SessionsCountsPerUser state, SessionsCountsPerUser to, SessionsCountsPerUser event) { 48 | state.updateStats(event); 49 | } 50 | 51 | @OnTimer(cron = "${selenograph.quota.stats.update.cron}", perState = false, skipIfNotCompleted = true) 52 | public void updateQuotaStats() { 53 | input.produce(new SessionsCountsPerUser(database.sessionsByUserCount())); 54 | } 55 | 56 | @OnTimer(cron = "0 * * * * ?", readOnly = false, skipIfNotCompleted = true) 57 | public void resetStats(SessionsCountsPerUser counts) { 58 | final Map current = database.sessionsByUserCount(); 59 | counts.entrySet().forEach(entry -> { 60 | final SessionsState state = entry.getValue(); 61 | LOGGER.info("Sending stats to graphite for {}:{}:{}...", 62 | state.getUser(), state.getBrowser(), state.getVersion()); 63 | final String prefix = format("%s.%s.%s-%s", graphitePrefix, state.getUser(), 64 | state.getBrowser().replace(".", "_"), 65 | state.getVersion().replace(".", "_")); 66 | final long timestamp = currentTimeMillis() / 1000L; 67 | graphite.produce(new GraphiteValue(prefix + ".stats.raw", state.getRaw(), timestamp)); 68 | graphite.produce(new GraphiteValue(prefix + ".stats.max", state.getMax(), timestamp)); 69 | graphite.produce(new GraphiteValue(prefix + ".stats.avg", state.getAvg(), timestamp)); 70 | Integer currentCount = current.get(fromBrowserString(entry.getKey())); 71 | if (currentCount == null) { 72 | currentCount = 0; 73 | } 74 | state.setMax(currentCount); 75 | state.setAvg(currentCount); 76 | state.setRaw(currentCount); 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /server/src/main/frontend/js/components/Browsers.jsx: -------------------------------------------------------------------------------- 1 | import { Component } from 'react'; 2 | import keys from 'lodash/keys'; 3 | import Avatar from 'material-ui/lib/avatar'; 4 | import List from 'material-ui/lib/lists/list'; 5 | import ListItem from 'material-ui/lib/lists/list-item'; 6 | import AppBar from 'material-ui/lib/app-bar'; 7 | import QuotaSelector from './QuotaSelector.jsx'; 8 | import Browser from './Browser.jsx'; 9 | import BrowserVersion from './BrowserVersion.jsx'; 10 | import fetch from '../util/fetch.js'; 11 | import WS from './../util/websocket.js'; 12 | 13 | const ENDPOINTS = { 14 | hubs: '/api/selenograph/hubs', 15 | quotas: '/api/selenograph/quotas' 16 | }; 17 | 18 | const BROWSER_ICONS = { 19 | opera: require('../icons/browsers/opera/opera_128x128.png'), 20 | chrome: require('../icons/browsers/chrome/chrome_128x128.png'), 21 | android: require('../icons/browsers/android/android_128x128.png'), 22 | firefox: require('../icons/browsers/firefox/firefox_128x128.png'), 23 | safari: require('../icons/browsers/safari/safari_128x128.png'), 24 | ios: require('../icons/browsers/safari-ios/safari-ios_128x128.png'), 25 | microsoftedge: require('../icons/browsers/edge/edge_128x128.png'), 26 | 'internet explorer': require('../icons/browsers/internet-explorer/internet-explorer_128x128.png') 27 | }; 28 | 29 | export default class Browsers extends Component { 30 | state = {browsersList: [], quotas: [], quotaMap: {}}; 31 | 32 | constructor(props) { 33 | super(props); 34 | this.quotaChanged = this.quotaChanged.bind(this); 35 | } 36 | 37 | componentDidMount() { 38 | fetch(ENDPOINTS.quotas) 39 | .then(quotas => { 40 | this.websocket = new WS('?pluginId=quotaClientNotifier', quotas => { 41 | this.updateQuotas(quotas); 42 | }); 43 | this.updateQuotas(quotas); 44 | }); 45 | } 46 | 47 | componentWillUnmount() { 48 | if(this.websocket) { 49 | this.websocket.destroy(); 50 | } 51 | } 52 | 53 | updateQuotas(quotas) { 54 | this.setState({ 55 | browsersList: quotas.all, 56 | quotas: keys(quotas).map(s => { 57 | return {name: s}; 58 | }), 59 | quotaMap: quotas 60 | }); 61 | } 62 | 63 | quotaChanged(quota) { 64 | this.setState({ 65 | browsersList: this.state.quotaMap[quota] 66 | }); 67 | } 68 | 69 | renderVersions(versions) { 70 | return versions.map(ver => 71 | 73 | }/> 74 | ); 75 | } 76 | 77 | renderBrowsers(browsers) { 78 | return browsers.map(item => 79 | } 81 | primaryText={ } 82 | primaryTogglesNestedList={true} 83 | nestedItems={ this.renderVersions(item.versions) }/> 84 | ); 85 | } 86 | 87 | render() { 88 | return ( 89 |

95 | ); 96 | } 97 | } 98 | 99 | -------------------------------------------------------------------------------- /service/src/main/resources/camelot-default.properties: -------------------------------------------------------------------------------- 1 | # suppress inspection "UnusedProperty" for whole file 2 | 3 | ############################# 4 | # Selenograph options # 5 | ############################# 6 | selenograph.gridrouter.hostSelectionStrategy=ru.qatools.selenograph.gridrouter.SmartHostSelectionStrategy 7 | selenograph.gridrouter.statsCounter=ru.qatools.selenograph.gridrouter.SessionsAggregator 8 | selenograph.gridrouter.requestQueueChecker=ru.qatools.selenograph.gridrouter.QueueWaitAvailableBrowsersChecker 9 | selenograph.gridrouter.clientNotifyCron=*/5 * * * * ? 10 | selenograph.hub.alive.cron=*/45 * * * * ? 11 | selenograph.hub.check.cron=0 * * * * ? 12 | selenograph.maxSessionTimeMillis=1200000 13 | selenograph.nodeInactivityTimeoutMillis=3600000 14 | selenograph.browserStartsHistoryLength=5 15 | selenograph.criticalBrowserStartFailuresPercentage=0.8 16 | selenograph.browsers.start.cooldown=120 17 | grid.router.evict.sessions.cron=0 * * * * * 18 | grid.router.evict.sessions.timeout.seconds=120 19 | selenograph.hub.alive.enabled=true 20 | selenograph.sessions.emulate.enabled=false 21 | selenograph.sessions.emulate.chance=70 22 | selenograph.sessions.emulate.cron=*/30 * * * * ? 23 | selenograph.quota.stats.update.cron=*/20 * * * * ? 24 | selenograph.sessions.bulk.flush.interval.ms=10000 25 | 26 | grid.router.queue.timeout.seconds=300 27 | grid.router.queue.interval.seconds=2 28 | grid.router.queue.request.timeout.cron=0 * * * * ? 29 | 30 | ############################# 31 | # Camelot options # 32 | ############################# 33 | camelot.input.uri=seda:events.input?concurrentConsumers=250&size=3000&blockWhenFull=true 34 | camelot.uribuilder=camelot-uribuilder-basic 35 | camelot.serializer=camelot-serializer-fst 36 | camelot.delayedRoute.delay.ms=2000 37 | camelot.threadpool.default.size=64 38 | camelot.threadpool.default.maxSize=512 39 | camelot.threadpool.default.keepAliveMillis=5000 40 | camelot.threadpool.multicast.size=64 41 | camelot.threadpool.multicast.maxSize=512 42 | camelot.threadpool.multicast.keepAliveMillis=10000 43 | 44 | ############################# 45 | # MongoDB options # 46 | ############################# 47 | camelot.factory=camelot-factory-mongodb 48 | camelot.quartzFactory=camelot-quartz-factory-mongodb 49 | camelot.clientSendersProvider=camelot-client-senders-mongodb 50 | camelot.mongodb.replicaset=localhost:27017 51 | camelot.mongodb.dbname=selenograph 52 | camelot.mongodb.writeconcern=SAFE 53 | camelot.mongodb.frontend.queue.maxsize=10 54 | camelot.mongodb.direct.queue.maxsize=10 55 | camelot.serializer=selenograph-messages-serializer 56 | camelot.mongodb.serializer.builder=selenograph-mongo-serializer-builder 57 | # camelot.mongodb.username= 58 | # camelot.mongodb.password= 59 | # camelot.mongodb.connections.per.host=30 60 | # camelot.mongodb.threads.connection.mult=40 61 | # camelot.mongodb.connect.timeout=15000 62 | # camelot.mongodb.heartbeat.timeout=15000 63 | # camelot.mongodb.heartbeat.frequency=1000 64 | # camelot.mongodb.heartbeat.socket.timeout=10000 65 | # camelot.mongodb.readpreference=PRIMARY_PREFERRED 66 | # camelot.mongodb.socket.timeout=60000 67 | # camelot.mongodb.waitForLockSec=120 68 | # camelot.mongodb.lockPollMaxIntervalMs=7 69 | 70 | ############################# 71 | # Graphite options # 72 | ############################# 73 | selenograph.gridrouter.graphite.prefix=selenograph 74 | graphite.host=127.0.0.1 75 | graphite.port=42000 76 | 77 | ############################# 78 | # Grid Router # 79 | ############################# 80 | grid.config.quota.directory=file:/etc/grid-router/quota 81 | grid.config.quota.hotReload=true 82 | grid.router.quota.repository=ru.qatools.gridrouter.ConfigRepositoryXml -------------------------------------------------------------------------------- /beans/src/main/resources/xsd/beans.xsd: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | ru.qatools.selenograph.gridrouter.Timestamped<ru.qatools.selenograph.gridrouter.BrowserContext> 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 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 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 | 89 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/QueueWaitAvailableBrowsersChecker.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | import ru.qatools.gridrouter.config.Version; 6 | import ru.qatools.gridrouter.sessions.WaitAvailableBrowsersChecker; 7 | import ru.yandex.qatools.camelot.api.EventProducer; 8 | import ru.yandex.qatools.camelot.api.annotations.*; 9 | import ru.yandex.qatools.fsm.annotations.*; 10 | 11 | import java.time.Duration; 12 | 13 | import static java.lang.String.format; 14 | 15 | /** 16 | * @author Ilya Sadykov 17 | */ 18 | @Aggregate 19 | @FSM(start = WaitAvailableBrowserState.class) 20 | @Filter(instanceOf = SessionRequest.class) 21 | @Transitions(@Transit(on = SessionRequest.class)) 22 | public class QueueWaitAvailableBrowsersChecker extends WaitAvailableBrowsersChecker { 23 | private static final Logger LOGGER = LoggerFactory.getLogger(QueueWaitAvailableBrowsersChecker.class); 24 | @Input 25 | private EventProducer input; 26 | 27 | @Override 28 | protected void onWait(String user, String browser, Version version, String requestId, int waitAttempt) { 29 | super.onWait(user, browser, version, requestId, waitAttempt); 30 | if (waitAttempt == 0) { 31 | input.produce(new SessionRequestEnqueued() 32 | .withRequestId(requestId) 33 | .withUser(user) 34 | .withBrowser(browser) 35 | .withVersion(version.getNumber()) 36 | ); 37 | } 38 | } 39 | 40 | @Override 41 | protected void onWaitFinished(String user, String browser, Version version, String requestId, int waitAttempt) { 42 | super.onWaitFinished(user, browser, version, requestId, waitAttempt); 43 | input.produce(new SessionRequestDequeued() 44 | .withRequestId(requestId) 45 | .withUser(user) 46 | .withBrowser(browser) 47 | .withVersion(version.getNumber()) 48 | ); 49 | } 50 | 51 | @AggregationKey 52 | public String aggregationKey(SessionRequest event) { 53 | return format("%s-%s-%s", event.getUser(), event.getBrowser(), event.getVersion()); 54 | } 55 | 56 | @BeforeTransit 57 | public void onBeforeRequest(WaitAvailableBrowserState state, SessionRequest event) { 58 | state.setVersion(event.getVersion()); 59 | state.setBrowser(event.getBrowser()); 60 | state.setUser(event.getUser()); 61 | } 62 | 63 | @OnTransit 64 | public void onDequeued(WaitAvailableBrowserState state, SessionRequestDequeued event) { 65 | LOGGER.debug("Removing request {} from the queue for {}-{}-{}, queue size={}", 66 | event.getRequestId(), event.getUser(), event.getBrowser(), event.getVersion(), state.size()); 67 | state.removeRequest(event.getRequestId()); 68 | } 69 | 70 | @OnTransit 71 | public void onEnqueued(WaitAvailableBrowserState state, SessionRequestEnqueued event) { 72 | LOGGER.debug("Adding new request {} to the queue for {}-{}-{}, queue size={}", 73 | event.getRequestId(), event.getUser(), event.getBrowser(), event.getVersion(), state.size()); 74 | state.addRequest(event.getRequestId()); 75 | } 76 | 77 | @OnTimer(cron = "${grid.router.queue.request.timeout.cron}", readOnly = false) 78 | public void checkAndExpireTimedoutRequests(WaitAvailableBrowserState state) { 79 | LOGGER.debug("Before expiration of outdated session requests for {}-{}-{}, queue size = {}", 80 | state.getUser(), state.getBrowser(), state.getVersion(), state.size()); 81 | state.expireRequestsOlderThan(Duration.ofMillis(queueTimeout)); 82 | LOGGER.debug("After expiration of outdated session requests for {}-{}-{}, queue size = {}", 83 | state.getUser(), state.getBrowser(), state.getVersion(), state.size()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/typings/browser/ambient/serve-static/serve-static.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/0fa4e9e61385646ea6a4cba2aef357353d2ce77f/serve-static/serve-static.d.ts 3 | // Type definitions for serve-static 1.7.1 4 | // Project: https://github.com/expressjs/serve-static 5 | // Definitions by: Uros Smolnik 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | /* =================== USAGE =================== 9 | 10 | import * as serveStatic from "serve-static"; 11 | app.use(serveStatic("public/ftp", {"index": ["default.html", "default.htm"]})) 12 | 13 | =============================================== */ 14 | 15 | 16 | declare module "serve-static" { 17 | import * as express from "express"; 18 | 19 | /** 20 | * Create a new middleware function to serve files from within a given root directory. 21 | * The file to serve will be determined by combining req.url with the provided root directory. 22 | * When a file is not found, instead of sending a 404 response, this module will instead call next() to move on to the next middleware, allowing for stacking and fall-backs. 23 | */ 24 | function serveStatic(root: string, options?: { 25 | /** 26 | * Set how "dotfiles" are treated when encountered. A dotfile is a file or directory that begins with a dot ("."). 27 | * Note this check is done on the path itself without checking if the path actually exists on the disk. 28 | * If root is specified, only the dotfiles above the root are checked (i.e. the root itself can be within a dotfile when when set to "deny"). 29 | * The default value is 'ignore'. 30 | * 'allow' No special treatment for dotfiles 31 | * 'deny' Send a 403 for any request for a dotfile 32 | * 'ignore' Pretend like the dotfile does not exist and call next() 33 | */ 34 | dotfiles?: string; 35 | 36 | /** 37 | * Enable or disable etag generation, defaults to true. 38 | */ 39 | etag?: boolean; 40 | 41 | /** 42 | * Set file extension fallbacks. When set, if a file is not found, the given extensions will be added to the file name and search for. 43 | * The first that exists will be served. Example: ['html', 'htm']. 44 | * The default value is false. 45 | */ 46 | extensions?: string[]; 47 | 48 | /** 49 | * By default this module will send "index.html" files in response to a request on a directory. 50 | * To disable this set false or to supply a new index pass a string or an array in preferred order. 51 | */ 52 | index?: boolean|string|string[]; 53 | 54 | /** 55 | * Enable or disable Last-Modified header, defaults to true. Uses the file system's last modified value. 56 | */ 57 | lastModified?: boolean; 58 | 59 | /** 60 | * Provide a max-age in milliseconds for http caching, defaults to 0. This can also be a string accepted by the ms module. 61 | */ 62 | maxAge?: number|string; 63 | 64 | /** 65 | * Redirect to trailing "/" when the pathname is a dir. Defaults to true. 66 | */ 67 | redirect?: boolean; 68 | 69 | /** 70 | * Function to set custom headers on response. Alterations to the headers need to occur synchronously. 71 | * The function is called as fn(res, path, stat), where the arguments are: 72 | * res the response object 73 | * path the file path that is being sent 74 | * stat the stat object of the file that is being sent 75 | */ 76 | setHeaders?: (res: express.Response, path: string, stat: any) => any; 77 | }): express.Handler; 78 | 79 | import * as m from "mime"; 80 | 81 | module serveStatic { 82 | var mime: typeof m; 83 | } 84 | 85 | export = serveStatic; 86 | } -------------------------------------------------------------------------------- /server/typings/main/ambient/serve-static/serve-static.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/0fa4e9e61385646ea6a4cba2aef357353d2ce77f/serve-static/serve-static.d.ts 3 | // Type definitions for serve-static 1.7.1 4 | // Project: https://github.com/expressjs/serve-static 5 | // Definitions by: Uros Smolnik 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | /* =================== USAGE =================== 9 | 10 | import * as serveStatic from "serve-static"; 11 | app.use(serveStatic("public/ftp", {"index": ["default.html", "default.htm"]})) 12 | 13 | =============================================== */ 14 | 15 | 16 | declare module "serve-static" { 17 | import * as express from "express"; 18 | 19 | /** 20 | * Create a new middleware function to serve files from within a given root directory. 21 | * The file to serve will be determined by combining req.url with the provided root directory. 22 | * When a file is not found, instead of sending a 404 response, this module will instead call next() to move on to the next middleware, allowing for stacking and fall-backs. 23 | */ 24 | function serveStatic(root: string, options?: { 25 | /** 26 | * Set how "dotfiles" are treated when encountered. A dotfile is a file or directory that begins with a dot ("."). 27 | * Note this check is done on the path itself without checking if the path actually exists on the disk. 28 | * If root is specified, only the dotfiles above the root are checked (i.e. the root itself can be within a dotfile when when set to "deny"). 29 | * The default value is 'ignore'. 30 | * 'allow' No special treatment for dotfiles 31 | * 'deny' Send a 403 for any request for a dotfile 32 | * 'ignore' Pretend like the dotfile does not exist and call next() 33 | */ 34 | dotfiles?: string; 35 | 36 | /** 37 | * Enable or disable etag generation, defaults to true. 38 | */ 39 | etag?: boolean; 40 | 41 | /** 42 | * Set file extension fallbacks. When set, if a file is not found, the given extensions will be added to the file name and search for. 43 | * The first that exists will be served. Example: ['html', 'htm']. 44 | * The default value is false. 45 | */ 46 | extensions?: string[]; 47 | 48 | /** 49 | * By default this module will send "index.html" files in response to a request on a directory. 50 | * To disable this set false or to supply a new index pass a string or an array in preferred order. 51 | */ 52 | index?: boolean|string|string[]; 53 | 54 | /** 55 | * Enable or disable Last-Modified header, defaults to true. Uses the file system's last modified value. 56 | */ 57 | lastModified?: boolean; 58 | 59 | /** 60 | * Provide a max-age in milliseconds for http caching, defaults to 0. This can also be a string accepted by the ms module. 61 | */ 62 | maxAge?: number|string; 63 | 64 | /** 65 | * Redirect to trailing "/" when the pathname is a dir. Defaults to true. 66 | */ 67 | redirect?: boolean; 68 | 69 | /** 70 | * Function to set custom headers on response. Alterations to the headers need to occur synchronously. 71 | * The function is called as fn(res, path, stat), where the arguments are: 72 | * res the response object 73 | * path the file path that is being sent 74 | * stat the stat object of the file that is being sent 75 | */ 76 | setHeaders?: (res: express.Response, path: string, stat: any) => any; 77 | }): express.Handler; 78 | 79 | import * as m from "mime"; 80 | 81 | module serveStatic { 82 | var mime: typeof m; 83 | } 84 | 85 | export = serveStatic; 86 | } -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/SmartHostSelectionStrategy.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import ru.qatools.gridrouter.config.Host; 4 | import ru.qatools.gridrouter.config.RandomHostSelectionStrategy; 5 | import ru.qatools.gridrouter.config.Region; 6 | import ru.qatools.gridrouter.config.WithCount; 7 | import ru.qatools.selenograph.front.HubSummary; 8 | 9 | import java.util.ArrayList; 10 | import java.util.HashMap; 11 | import java.util.List; 12 | import java.util.Map; 13 | import java.util.function.Function; 14 | 15 | import static java.util.Collections.emptyMap; 16 | import static java.util.stream.Collectors.summingInt; 17 | import static java.util.stream.Collectors.toMap; 18 | import static ru.qatools.clay.utils.DateUtil.isTimePassedSince; 19 | 20 | /** 21 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 22 | */ 23 | public class SmartHostSelectionStrategy extends RandomHostSelectionStrategy 24 | implements UpdatableSelectionStrategy { 25 | 26 | protected static final int FALLBACK_TIMEOUT = 5 * 1000; 27 | protected static final int HUB_MAX_AGE = 45 * 1000; 28 | 29 | private long timestamp; 30 | 31 | /** 32 | * hostname -> free percentage (0 - 100) 33 | */ 34 | private Map hubs = emptyMap(); 35 | 36 | //TODO split by browser and version 37 | public void updateHubSummaries(List hubSummaries, long timestamp) { 38 | this.timestamp = timestamp; 39 | this.hubs = hubSummaries.stream() 40 | .filter(this::isUpdatedRecently) 41 | .collect(toMap(HubSummary::getAddress, this::getFreePercentage)) 42 | .entrySet().stream() 43 | .filter(e -> e.getValue() > 0) 44 | .collect(toMap(Map.Entry::getKey, Map.Entry::getValue)); 45 | } 46 | 47 | @Override 48 | public long getTimestamp() { 49 | return timestamp; 50 | } 51 | 52 | @Override 53 | public Map getHubs() { 54 | return hubs; 55 | } 56 | 57 | @Override 58 | public Region selectRegion(List allRegions, List unvisitedRegions) { 59 | return selectRandom(allRegions, this::toFreePercentage); 60 | } 61 | 62 | @Override 63 | public Host selectHost(List hosts) { 64 | return selectRandom(hosts, this::toFreePercentage); 65 | } 66 | 67 | private T selectRandom(List elements, Function countMapper) { 68 | if (isTimePassedSince(FALLBACK_TIMEOUT, timestamp)) { 69 | return super.selectRandom(elements); 70 | } 71 | 72 | List elementsWithPercentage = new ArrayList<>(elements.size()); 73 | Map actual = new HashMap<>(elements.size(), 1); 74 | 75 | for (T element : elements) { 76 | int count = countMapper.apply(element); 77 | if (count > 0) { 78 | WithCount withCount = () -> count; 79 | elementsWithPercentage.add(withCount); 80 | actual.put(withCount, element); 81 | } 82 | } 83 | 84 | if (elementsWithPercentage.isEmpty()) { 85 | return super.selectRandom(elements); 86 | } 87 | 88 | return actual.get(super.selectRandom(elementsWithPercentage)); 89 | } 90 | 91 | public boolean isUpdatedRecently(HubSummary hub) { 92 | return !isTimePassedSince(HUB_MAX_AGE, hub.getTimestamp()); 93 | } 94 | 95 | private int toFreePercentage(Region region) { 96 | return region.getHosts().stream().collect(summingInt(this::toFreePercentage)); 97 | } 98 | 99 | private int toFreePercentage(Host host) { 100 | return hubs.getOrDefault(host.getAddress(), 0); 101 | } 102 | 103 | private int getFreePercentage(HubSummary hub) { 104 | return hub.getMax() == 0 ? 0 : 100 - hub.getRunning() * 100 / hub.getMax(); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /server/typings/browser/ambient/atmosphere/atmosphere.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/11179de6605ca0f50ffafbcd8ae1c7df5020acca/atmosphere/atmosphere.d.ts 3 | // Type definitions for Atmosphere v2.1.5 4 | // Project: https://github.com/Atmosphere/atmosphere-javascript 5 | // Definitions by: Kai Toedter 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | declare module Atmosphere { 9 | interface Atmosphere { 10 | /** 11 | * The atmosphere API is a little bit special here: the first parameter can either be 12 | * a URL string or a Request object. If it is a URL string, then the additional parameters are expected. 13 | */ 14 | subscribe?: (requestOrUrl:any, callback?:Function, request?:Request) => Request; 15 | unsubscribe?: () => void; 16 | 17 | AtmosphereRequest?: AtmosphereRequest; 18 | } 19 | 20 | // needed to fit JavaScript "new atmosphere.AtmosphereRequest()" 21 | // and compile with --noImplicitAny 22 | interface AtmosphereRequest { 23 | new(): Request; 24 | } 25 | 26 | interface Request { 27 | timeout?: number; 28 | method?: string; 29 | headers?: any; 30 | contentType?: string; 31 | callback?: Function; 32 | url?: string; 33 | data?: string; 34 | suspend?: boolean; 35 | maxRequest?: number; 36 | reconnect?: boolean; 37 | maxStreamingLength?: number; 38 | lastIndex?: number; 39 | logLevel?: string; 40 | requestCount?: number; 41 | fallbackMethod?: string; 42 | fallbackTransport?: string; 43 | transport?: string; 44 | webSocketImpl?: any; 45 | webSocketBinaryType?: any; 46 | dispatchUrl?: string; 47 | webSocketPathDelimiter?: string; 48 | enableXDR?: boolean; 49 | rewriteURL?: boolean; 50 | attachHeadersAsQueryString?: boolean; 51 | executeCallbackBeforeReconnect?: boolean; 52 | readyState?: number; 53 | lastTimestamp?: number; 54 | withCredentials?: boolean; 55 | trackMessageLength?: boolean; 56 | messageDelimiter?: string; 57 | connectTimeout?: number; 58 | reconnectInterval?: number; 59 | dropHeaders?: boolean; 60 | uuid?: number; 61 | async?: boolean; 62 | shared?: boolean; 63 | readResponsesHeaders?: boolean; 64 | maxReconnectOnClose?: number; 65 | enableProtocol?: boolean; 66 | pollingInterval?: number; 67 | 68 | onError?: (response?:Response) => void; 69 | onClose?: (response?:Response) => void; 70 | onOpen?: (response?:Response) => void; 71 | onMessage?: (response:Response) => void; 72 | onReopen?: (request?:Request, response?:Response) => void; 73 | onReconnect?: (request?:Request, response?:Response) => void; 74 | onMessagePublished?: (response?:Response) => void; 75 | onTransportFailure?: (reason?:string, response?:Response) => void; 76 | onLocalMessage?: (request?:Request) => void; 77 | onFailureToReconnect?: (request?:Request, response?:Response) => void; 78 | onClientTimeout?: (request?:Request) => void; 79 | 80 | subscribe?: (options:Request) => void; 81 | execute?: () => void; 82 | close?: () => void; 83 | disconnect?: () => void; 84 | getUrl?: () => string; 85 | push?: (message:string, dispatchUrl?:string) => void; 86 | getUUID?: () => void; 87 | pushLocal?: (message:string) => void; 88 | } 89 | 90 | interface Response { 91 | status?: number; 92 | reasonPhrase?: string; 93 | responseBody?: string; 94 | messages?: string[]; 95 | headers?: string[]; 96 | state?: string; 97 | transport?: string; 98 | error?: string; 99 | request?: Request; 100 | partialMessage?: string; 101 | errorHandled?: boolean; 102 | closedByClientTimeout?: boolean; 103 | } 104 | } 105 | 106 | declare var atmosphere:Atmosphere.Atmosphere; -------------------------------------------------------------------------------- /server/typings/main/ambient/atmosphere/atmosphere.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/11179de6605ca0f50ffafbcd8ae1c7df5020acca/atmosphere/atmosphere.d.ts 3 | // Type definitions for Atmosphere v2.1.5 4 | // Project: https://github.com/Atmosphere/atmosphere-javascript 5 | // Definitions by: Kai Toedter 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | declare module Atmosphere { 9 | interface Atmosphere { 10 | /** 11 | * The atmosphere API is a little bit special here: the first parameter can either be 12 | * a URL string or a Request object. If it is a URL string, then the additional parameters are expected. 13 | */ 14 | subscribe?: (requestOrUrl:any, callback?:Function, request?:Request) => Request; 15 | unsubscribe?: () => void; 16 | 17 | AtmosphereRequest?: AtmosphereRequest; 18 | } 19 | 20 | // needed to fit JavaScript "new atmosphere.AtmosphereRequest()" 21 | // and compile with --noImplicitAny 22 | interface AtmosphereRequest { 23 | new(): Request; 24 | } 25 | 26 | interface Request { 27 | timeout?: number; 28 | method?: string; 29 | headers?: any; 30 | contentType?: string; 31 | callback?: Function; 32 | url?: string; 33 | data?: string; 34 | suspend?: boolean; 35 | maxRequest?: number; 36 | reconnect?: boolean; 37 | maxStreamingLength?: number; 38 | lastIndex?: number; 39 | logLevel?: string; 40 | requestCount?: number; 41 | fallbackMethod?: string; 42 | fallbackTransport?: string; 43 | transport?: string; 44 | webSocketImpl?: any; 45 | webSocketBinaryType?: any; 46 | dispatchUrl?: string; 47 | webSocketPathDelimiter?: string; 48 | enableXDR?: boolean; 49 | rewriteURL?: boolean; 50 | attachHeadersAsQueryString?: boolean; 51 | executeCallbackBeforeReconnect?: boolean; 52 | readyState?: number; 53 | lastTimestamp?: number; 54 | withCredentials?: boolean; 55 | trackMessageLength?: boolean; 56 | messageDelimiter?: string; 57 | connectTimeout?: number; 58 | reconnectInterval?: number; 59 | dropHeaders?: boolean; 60 | uuid?: number; 61 | async?: boolean; 62 | shared?: boolean; 63 | readResponsesHeaders?: boolean; 64 | maxReconnectOnClose?: number; 65 | enableProtocol?: boolean; 66 | pollingInterval?: number; 67 | 68 | onError?: (response?:Response) => void; 69 | onClose?: (response?:Response) => void; 70 | onOpen?: (response?:Response) => void; 71 | onMessage?: (response:Response) => void; 72 | onReopen?: (request?:Request, response?:Response) => void; 73 | onReconnect?: (request?:Request, response?:Response) => void; 74 | onMessagePublished?: (response?:Response) => void; 75 | onTransportFailure?: (reason?:string, response?:Response) => void; 76 | onLocalMessage?: (request?:Request) => void; 77 | onFailureToReconnect?: (request?:Request, response?:Response) => void; 78 | onClientTimeout?: (request?:Request) => void; 79 | 80 | subscribe?: (options:Request) => void; 81 | execute?: () => void; 82 | close?: () => void; 83 | disconnect?: () => void; 84 | getUrl?: () => string; 85 | push?: (message:string, dispatchUrl?:string) => void; 86 | getUUID?: () => void; 87 | pushLocal?: (message:string) => void; 88 | } 89 | 90 | interface Response { 91 | status?: number; 92 | reasonPhrase?: string; 93 | responseBody?: string; 94 | messages?: string[]; 95 | headers?: string[]; 96 | state?: string; 97 | transport?: string; 98 | error?: string; 99 | request?: Request; 100 | partialMessage?: string; 101 | errorHandled?: boolean; 102 | closedByClientTimeout?: boolean; 103 | } 104 | } 105 | 106 | declare var atmosphere:Atmosphere.Atmosphere; -------------------------------------------------------------------------------- /server/typings/main/ambient/isomorphic-fetch/isomorphic-fetch.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/83a71a28177843c3a65587588c5fc37ef193a75f/isomorphic-fetch/isomorphic-fetch.d.ts 3 | // Type definitions for isomorphic-fetch 4 | // Project: https://github.com/matthew-andrews/isomorphic-fetch 5 | // Definitions by: Todd Lucas 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | declare enum RequestContext { 9 | "audio", "beacon", "cspreport", "download", "embed", "eventsource", 10 | "favicon", "fetch", "font", "form", "frame", "hyperlink", "iframe", 11 | "image", "imageset", "import", "internal", "location", "manifest", 12 | "object", "ping", "plugin", "prefetch", "script", "serviceworker", 13 | "sharedworker", "subresource", "style", "track", "video", "worker", 14 | "xmlhttprequest", "xslt" 15 | } 16 | declare enum RequestMode { "same-origin", "no-cors", "cors" } 17 | declare enum RequestCredentials { "omit", "same-origin", "include" } 18 | declare enum RequestCache { 19 | "default", "no-store", "reload", "no-cache", "force-cache", 20 | "only-if-cached" 21 | } 22 | declare enum ResponseType { "basic", "cors", "default", "error", "opaque" } 23 | 24 | declare type HeaderInit = Headers | Array; 25 | declare type BodyInit = Blob | FormData | string; 26 | declare type RequestInfo = Request | string; 27 | 28 | interface RequestInit { 29 | method?: string; 30 | headers?: HeaderInit | { [index: string]: string }; 31 | body?: BodyInit; 32 | mode?: string | RequestMode; 33 | credentials?: string | RequestCredentials; 34 | cache?: string | RequestCache; 35 | } 36 | 37 | interface IHeaders { 38 | get(name: string): string; 39 | getAll(name: string): Array; 40 | has(name: string): boolean; 41 | } 42 | 43 | declare class Headers implements IHeaders { 44 | append(name: string, value: string): void; 45 | delete(name: string):void; 46 | get(name: string): string; 47 | getAll(name: string): Array; 48 | has(name: string): boolean; 49 | set(name: string, value: string): void; 50 | } 51 | 52 | interface IBody { 53 | bodyUsed: boolean; 54 | arrayBuffer(): Promise; 55 | blob(): Promise; 56 | formData(): Promise; 57 | json(): Promise; 58 | json(): Promise; 59 | text(): Promise; 60 | } 61 | 62 | declare class Body implements IBody { 63 | bodyUsed: boolean; 64 | arrayBuffer(): Promise; 65 | blob(): Promise; 66 | formData(): Promise; 67 | json(): Promise; 68 | json(): Promise; 69 | text(): Promise; 70 | } 71 | 72 | interface IRequest extends IBody { 73 | method: string; 74 | url: string; 75 | headers: Headers; 76 | context: string | RequestContext; 77 | referrer: string; 78 | mode: string | RequestMode; 79 | credentials: string | RequestCredentials; 80 | cache: string | RequestCache; 81 | } 82 | 83 | declare class Request extends Body implements IRequest { 84 | constructor(input: string | Request, init?: RequestInit); 85 | method: string; 86 | url: string; 87 | headers: Headers; 88 | context: string | RequestContext; 89 | referrer: string; 90 | mode: string | RequestMode; 91 | credentials: string | RequestCredentials; 92 | cache: string | RequestCache; 93 | } 94 | 95 | interface IResponse extends IBody { 96 | url: string; 97 | status: number; 98 | statusText: string; 99 | ok: boolean; 100 | headers: IHeaders; 101 | type: string | ResponseType; 102 | size: number; 103 | timeout: number; 104 | redirect(url: string, status: number): IResponse; 105 | error(): IResponse; 106 | clone(): IResponse; 107 | } 108 | 109 | interface IFetchStatic { 110 | Promise: any; 111 | Headers: IHeaders 112 | Request: IRequest; 113 | Response: IResponse; 114 | (url: string | IRequest, init?: RequestInit): Promise; 115 | } 116 | 117 | declare module "isomorphic-fetch" { 118 | export default IFetchStatic; 119 | } 120 | 121 | declare var fetch: IFetchStatic; -------------------------------------------------------------------------------- /service/src/test/java/ru/qatools/selenograph/api/BasePluginTest.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.api; 2 | 3 | import org.hamcrest.Matcher; 4 | import org.junit.After; 5 | import org.junit.Before; 6 | import org.junit.Rule; 7 | import org.junit.rules.TestName; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import ru.yandex.qatools.camelot.test.AggregatorStateStorage; 11 | import ru.yandex.qatools.camelot.test.Helper; 12 | import ru.yandex.qatools.camelot.test.TestHelper; 13 | import ru.yandex.qatools.matchers.decorators.TimeoutWaiter; 14 | import ru.qatools.selenograph.utils.TestProperties; 15 | 16 | import static java.lang.String.format; 17 | import static org.hamcrest.MatcherAssert.assertThat; 18 | import static org.hamcrest.Matchers.not; 19 | import static ru.yandex.qatools.camelot.test.Matchers.containStateFor; 20 | import static ru.yandex.qatools.matchers.decorators.MatcherDecorators.should; 21 | 22 | /** 23 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 24 | */ 25 | public abstract class BasePluginTest { 26 | 27 | protected static final int TIMEOUT = new TestProperties().getTimeout(); 28 | 29 | protected final Logger logger = LoggerFactory.getLogger(this.getClass()); 30 | 31 | @Helper 32 | protected TestHelper helper; 33 | 34 | @Rule 35 | public final TestName testName = new TestName(); 36 | 37 | @Before 38 | public final void logTestStart() { 39 | logger.info(format("%n--------------------------------------" + 40 | "%nStarting test %s.%s" + 41 | "%n--------------------------------------", 42 | this.getClass().getSimpleName(), 43 | testName.getMethodName())); 44 | } 45 | 46 | @After 47 | public final void logTestFinish() { 48 | logger.info(format("%n--------------------------------------" + 49 | "%nFinished test %s.%s" + 50 | "%n--------------------------------------", 51 | this.getClass().getSimpleName(), 52 | testName.getMethodName())); 53 | } 54 | 55 | protected static TimeoutWaiter timeoutHasExpired() { 56 | return TimeoutWaiter.timeoutHasExpired(TIMEOUT); 57 | } 58 | 59 | protected final void send(Object event) { 60 | helper.send(event); 61 | } 62 | 63 | protected void sleep(int timeout) { 64 | try { 65 | Thread.sleep(timeout); 66 | } catch (InterruptedException e) { 67 | throw new RuntimeException(e); 68 | } 69 | } 70 | 71 | protected final void shouldStop(AggregatorStateStorage storage, 72 | String aggregationKey, 73 | String reason) { 74 | storageShould(not(containStateFor(aggregationKey)), storage, reason); 75 | } 76 | 77 | protected final void stateShouldBe(Class stateClass, 78 | AggregatorStateStorage storage, 79 | String aggregationKey) { 80 | stateShouldBe(stateClass, storage, aggregationKey, 81 | "state should be instance of " + stateClass.getSimpleName()); 82 | } 83 | 84 | protected final void stateShouldBe(Class clazz, 85 | AggregatorStateStorage storage, 86 | String aggregationKey, 87 | String reason) { 88 | storageShould(containStateFor(aggregationKey, clazz), storage, reason); 89 | } 90 | 91 | protected final void storageShould(Matcher matcher, 92 | AggregatorStateStorage storage, 93 | String reason) { 94 | assertThat(reason, storage, should(matcher).whileWaitingUntil(timeoutHasExpired())); 95 | } 96 | 97 | protected final void storageShould(Matcher matcher, 98 | AggregatorStateStorage storage) { 99 | assertThat(storage, should(matcher).whileWaitingUntil(timeoutHasExpired())); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /server/typings/browser/ambient/isomorphic-fetch/isomorphic-fetch.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/DefinitelyTyped/DefinitelyTyped/83a71a28177843c3a65587588c5fc37ef193a75f/isomorphic-fetch/isomorphic-fetch.d.ts 3 | // Type definitions for isomorphic-fetch 4 | // Project: https://github.com/matthew-andrews/isomorphic-fetch 5 | // Definitions by: Todd Lucas 6 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 7 | 8 | declare enum RequestContext { 9 | "audio", "beacon", "cspreport", "download", "embed", "eventsource", 10 | "favicon", "fetch", "font", "form", "frame", "hyperlink", "iframe", 11 | "image", "imageset", "import", "internal", "location", "manifest", 12 | "object", "ping", "plugin", "prefetch", "script", "serviceworker", 13 | "sharedworker", "subresource", "style", "track", "video", "worker", 14 | "xmlhttprequest", "xslt" 15 | } 16 | declare enum RequestMode { "same-origin", "no-cors", "cors" } 17 | declare enum RequestCredentials { "omit", "same-origin", "include" } 18 | declare enum RequestCache { 19 | "default", "no-store", "reload", "no-cache", "force-cache", 20 | "only-if-cached" 21 | } 22 | declare enum ResponseType { "basic", "cors", "default", "error", "opaque" } 23 | 24 | declare type HeaderInit = Headers | Array; 25 | declare type BodyInit = Blob | FormData | string; 26 | declare type RequestInfo = Request | string; 27 | 28 | interface RequestInit { 29 | method?: string; 30 | headers?: HeaderInit | { [index: string]: string }; 31 | body?: BodyInit; 32 | mode?: string | RequestMode; 33 | credentials?: string | RequestCredentials; 34 | cache?: string | RequestCache; 35 | } 36 | 37 | interface IHeaders { 38 | get(name: string): string; 39 | getAll(name: string): Array; 40 | has(name: string): boolean; 41 | } 42 | 43 | declare class Headers implements IHeaders { 44 | append(name: string, value: string): void; 45 | delete(name: string):void; 46 | get(name: string): string; 47 | getAll(name: string): Array; 48 | has(name: string): boolean; 49 | set(name: string, value: string): void; 50 | } 51 | 52 | interface IBody { 53 | bodyUsed: boolean; 54 | arrayBuffer(): Promise; 55 | blob(): Promise; 56 | formData(): Promise; 57 | json(): Promise; 58 | json(): Promise; 59 | text(): Promise; 60 | } 61 | 62 | declare class Body implements IBody { 63 | bodyUsed: boolean; 64 | arrayBuffer(): Promise; 65 | blob(): Promise; 66 | formData(): Promise; 67 | json(): Promise; 68 | json(): Promise; 69 | text(): Promise; 70 | } 71 | 72 | interface IRequest extends IBody { 73 | method: string; 74 | url: string; 75 | headers: Headers; 76 | context: string | RequestContext; 77 | referrer: string; 78 | mode: string | RequestMode; 79 | credentials: string | RequestCredentials; 80 | cache: string | RequestCache; 81 | } 82 | 83 | declare class Request extends Body implements IRequest { 84 | constructor(input: string | Request, init?: RequestInit); 85 | method: string; 86 | url: string; 87 | headers: Headers; 88 | context: string | RequestContext; 89 | referrer: string; 90 | mode: string | RequestMode; 91 | credentials: string | RequestCredentials; 92 | cache: string | RequestCache; 93 | } 94 | 95 | interface IResponse extends IBody { 96 | url: string; 97 | status: number; 98 | statusText: string; 99 | ok: boolean; 100 | headers: IHeaders; 101 | type: string | ResponseType; 102 | size: number; 103 | timeout: number; 104 | redirect(url: string, status: number): IResponse; 105 | error(): IResponse; 106 | clone(): IResponse; 107 | } 108 | 109 | interface IFetchStatic { 110 | Promise: any; 111 | Headers: IHeaders 112 | Request: IRequest; 113 | Response: IResponse; 114 | (url: string | IRequest, init?: RequestInit): Promise; 115 | } 116 | 117 | declare module "isomorphic-fetch" { 118 | export default IFetchStatic; 119 | } 120 | 121 | declare var fetch: IFetchStatic; -------------------------------------------------------------------------------- /service/src/test/java/ru/qatools/selenograph/gridrouter/QuotaSummaryClientNotifierTest.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.mockito.ArgumentCaptor; 6 | import ru.qatools.selenograph.front.BrowserSummary; 7 | import ru.qatools.selenograph.front.VersionSummary; 8 | import ru.yandex.qatools.camelot.api.ClientMessageSender; 9 | import ru.yandex.qatools.camelot.test.*; 10 | 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | import static com.jayway.awaitility.Awaitility.await; 15 | import static java.util.concurrent.TimeUnit.SECONDS; 16 | import static org.hamcrest.Matchers.hasItems; 17 | import static org.hamcrest.Matchers.hasSize; 18 | import static org.junit.Assert.assertEquals; 19 | import static org.junit.Assert.assertThat; 20 | import static org.mockito.Mockito.*; 21 | import static ru.qatools.selenograph.ext.SelenographDB.ALL; 22 | 23 | /** 24 | * @author Ilya Sadykov 25 | */ 26 | @SuppressWarnings("unchecked") 27 | @RunWith(CamelotTestRunner.class) 28 | @DisableTimers 29 | public class QuotaSummaryClientNotifierTest extends QuotaStatsAggregatorTest { 30 | 31 | @Helper 32 | TestHelper helper; 33 | 34 | @ClientSenderMock(QuotaSummaryClientNotifier.class) 35 | ClientMessageSender frontend; 36 | 37 | @Test 38 | public void sendSummaryToClient() throws Exception { 39 | String sessionId1 = startSessionFor("user1", "firefox", "32.0"); 40 | String sessionId2 = startSessionFor("user2", "chrome", "32.0"); 41 | String sessionId3 = startSessionFor("user3", "firefox", "33.0"); 42 | await().atMost(4, SECONDS).until(() -> sessions.getActiveSessions(), 43 | hasItems(sessionId1, sessionId2, sessionId3)); 44 | reset(frontend); 45 | helper.invokeTimersFor(QuotaSummaryClientNotifier.class); 46 | final ArgumentCaptor argument = ArgumentCaptor.forClass(Map.class); 47 | verify(frontend, timeout(1000)).send(argument.capture()); 48 | final Map sent = argument.getValue(); 49 | 50 | assertThat(sent.get(ALL), hasSize(2)); 51 | final BrowserSummary allFirefox = sent.get(ALL).stream() 52 | .filter(s -> s.getName().equals("firefox")).findFirst().orElseThrow(RuntimeException::new); 53 | assertEquals(4, allFirefox.getMax()); 54 | assertEquals(2, allFirefox.getRunning()); 55 | assertThat(allFirefox.getVersions(), hasSize(2)); 56 | final VersionSummary allFirefox32 = allFirefox.getVersions().stream() 57 | .filter(v -> v.getVersion().equals("32.0")).findFirst().orElseThrow(RuntimeException::new); 58 | assertEquals(3, allFirefox32.getMax()); 59 | assertEquals(1, allFirefox32.getRunning()); 60 | 61 | assertThat(sent.get("user1"), hasSize(1)); 62 | assertEquals(2, sent.get("user1").get(0).getMax()); 63 | assertEquals(1, sent.get("user1").get(0).getRunning()); 64 | final List user1Versions = sent.get("user1").get(0).getVersions(); 65 | assertThat(user1Versions, hasSize(1)); 66 | assertEquals(2, user1Versions.get(0).getMax()); 67 | assertEquals(1, user1Versions.get(0).getRunning()); 68 | 69 | assertThat(sent.get("user2"), hasSize(2)); 70 | final BrowserSummary user2Chrome = sent.get("user2").stream() 71 | .filter(s -> s.getName().equals("chrome")).findFirst().orElseThrow(RuntimeException::new); 72 | assertEquals(1, user2Chrome.getMax()); 73 | assertEquals(1, user2Chrome.getRunning()); 74 | final BrowserSummary user2Firefox = sent.get("user2").stream() 75 | .filter(s -> s.getName().equals("firefox")).findFirst().orElseThrow(RuntimeException::new); 76 | assertEquals(1, user2Firefox.getMax()); 77 | assertEquals(0, user2Firefox.getRunning()); 78 | assertThat(user2Firefox.getVersions(), hasSize(1)); 79 | assertEquals(1, user2Firefox.getVersions().get(0).getMax()); 80 | assertEquals(0, user2Firefox.getVersions().get(0).getRunning()); 81 | 82 | assertThat(sent.get("user3"), hasSize(1)); 83 | final BrowserSummary user3Firefox = sent.get("user3").get(0); 84 | assertEquals(2, user3Firefox.getMax()); 85 | assertEquals(1, user3Firefox.getRunning()); 86 | assertThat(user3Firefox.getVersions(), hasSize(2)); 87 | final VersionSummary user3Firefox33 = user3Firefox.getVersions().stream() 88 | .filter(v -> v.getVersion().equals("33.0")).findFirst().orElseThrow(RuntimeException::new); 89 | assertEquals(1, user3Firefox33.getMax()); 90 | assertEquals(1, user3Firefox33.getRunning()); 91 | 92 | } 93 | } -------------------------------------------------------------------------------- /service/src/test/java/ru/qatools/selenograph/gridrouter/SmartHostSelectionStrategyTest.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import ru.qatools.gridrouter.config.Host; 6 | import ru.qatools.gridrouter.config.Region; 7 | import ru.qatools.selenograph.front.HubSummary; 8 | 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | import static java.lang.System.currentTimeMillis; 13 | import static java.util.Arrays.asList; 14 | import static java.util.Collections.singletonList; 15 | import static java.util.stream.Collectors.toList; 16 | import static org.hamcrest.Matchers.equalTo; 17 | import static org.hamcrest.Matchers.is; 18 | import static org.junit.Assert.assertThat; 19 | import static ru.qatools.selenograph.gridrouter.SmartHostSelectionStrategy.FALLBACK_TIMEOUT; 20 | import static ru.qatools.selenograph.gridrouter.SmartHostSelectionStrategy.HUB_MAX_AGE; 21 | 22 | /** 23 | * @author Innokenty Shuvalov innokenty@yandex-team.ru 24 | */ 25 | public class SmartHostSelectionStrategyTest { 26 | 27 | private static final Host HOST_1 = new Host("host1", 4444, 5); 28 | private static final Host HOST_2 = new Host("host2", 4444, 5); 29 | private static final Host HOST_3 = new Host("host3", 4444, 5); 30 | 31 | private static final Region REGION_1 = new Region(asList(HOST_1), "region1"); 32 | private static final Region REGION_2 = new Region(asList(HOST_2), "region2"); 33 | 34 | private static final HubSummary SUMMARY_1 = hubSummary("host1:4444", 5, 5, currentTimeMillis()); 35 | private static final HubSummary SUMMARY_2 = hubSummary("host2:4444", 1, 5, currentTimeMillis()); 36 | 37 | private SmartHostSelectionStrategy strategy; 38 | 39 | @Before 40 | public void setUp() throws Exception { 41 | strategy = new SmartHostSelectionStrategy(); 42 | } 43 | 44 | @Test 45 | public void testSelectWithEmptySummariesMap() { 46 | assertThat(strategy.selectHost(singletonList(HOST_1)), is(equalTo(HOST_1))); 47 | } 48 | 49 | @Test 50 | public void testSelectWithNonMatchingSummaries() { 51 | strategy.updateHubSummaries(singletonList(SUMMARY_2), currentTimeMillis()); 52 | assertThat(strategy.selectHost(singletonList(HOST_1)), is(equalTo(HOST_1))); 53 | } 54 | 55 | @Test 56 | public void testSelectWithMatchingSummaries() { 57 | strategy.updateHubSummaries(asList(SUMMARY_1, SUMMARY_2), currentTimeMillis()); 58 | Host actual = strategy.selectHost(asList(HOST_1, HOST_2, HOST_3)); 59 | assertThat(actual, is(equalTo(HOST_2))); 60 | } 61 | 62 | @Test 63 | public void testSelectRegion() { 64 | strategy.updateHubSummaries(singletonList(SUMMARY_2), currentTimeMillis()); 65 | 66 | List allRegions = asList(REGION_1, REGION_2).stream().map(Region::copy).collect(toList()); 67 | List unvisitedRegions = new ArrayList<>(allRegions); 68 | 69 | Region actual = strategy.selectRegion(allRegions, unvisitedRegions); 70 | assertThat(actual, is(equalTo(allRegions.get(1)))); 71 | 72 | unvisitedRegions.remove(1); 73 | actual = strategy.selectRegion(allRegions, unvisitedRegions); 74 | assertThat(actual, is(equalTo(allRegions.get(1)))); 75 | 76 | allRegions.get(1).getHosts().clear(); 77 | actual = strategy.selectRegion(allRegions, unvisitedRegions); 78 | assertThat(actual, is(equalTo(allRegions.get(0)))); 79 | } 80 | 81 | @Test 82 | public void testOutdatedUpdateTimestamp() { 83 | strategy.updateHubSummaries(asList(SUMMARY_2), currentTimeMillis() - 2 * FALLBACK_TIMEOUT); 84 | Host host2WithZeroCount = new Host(HOST_2.getName(), HOST_2.getPort(), 0); 85 | Host actual = strategy.selectHost(asList(HOST_1, host2WithZeroCount)); 86 | assertThat(actual, is(equalTo(HOST_1))); 87 | } 88 | 89 | @Test 90 | public void testOutdatedHubTimestamp() { 91 | HubSummary summary2WithOutdatedTimestamp = SUMMARY_2; 92 | summary2WithOutdatedTimestamp.setTimestamp(currentTimeMillis() - 2 * HUB_MAX_AGE); 93 | strategy.updateHubSummaries(asList(summary2WithOutdatedTimestamp), currentTimeMillis()); 94 | Host host2WithZeroCount = new Host(HOST_2.getName(), HOST_2.getPort(), 0); 95 | Host actual = strategy.selectHost(asList(HOST_1, host2WithZeroCount)); 96 | assertThat(actual, is(equalTo(HOST_1))); 97 | } 98 | 99 | private static HubSummary hubSummary(String hub2, int running, int max, long timestamp) { 100 | HubSummary summary = new HubSummary(); 101 | summary.setTimestamp(timestamp); 102 | summary.setAddress(hub2); 103 | summary.setRunning(running); 104 | summary.setMax(max); 105 | return summary; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /service/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | selenograph 5 | ru.qatools.seleniumkit 6 | 1.3-SNAPSHOT 7 | 8 | 4.0.0 9 | 10 | selenograph-service 11 | 12 | 13 | 14 | 15 | ru.yandex.qatools.camelot 16 | camelot-serialize-fst 17 | 18 | 19 | ru.yandex.qatools.camelot 20 | camelot-mongodb 21 | 22 | 23 | com.fasterxml.jackson.datatype 24 | jackson-datatype-jsr310 25 | 2.7.3 26 | 27 | 28 | org.mongojack 29 | mongojack 30 | 2.6.0 31 | 32 | 33 | ru.yandex.qatools.embed 34 | embedded-services 35 | 1.21 36 | test 37 | 38 | 39 | mongo-java-driver 40 | org.mongodb 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | ${project.groupId} 49 | selenograph-beans 50 | 51 | 52 | 53 | 54 | ru.qatools.seleniumkit 55 | gridrouter-proxy 56 | classes 57 | 58 | 59 | 60 | 61 | javax.inject 62 | javax.inject 63 | 1 64 | 65 | 66 | ru.yandex.qatools.camelot 67 | camelot-core 68 | 69 | 70 | ru.yandex.qatools.camelot 71 | camelot-api 72 | 73 | 74 | ru.yandex.qatools.clay 75 | clay-utils 76 | 77 | 78 | ru.yandex.qatools.camelot.utils 79 | plugin-utils 80 | 1.4.1 81 | 82 | 83 | 84 | 85 | ru.yandex.qatools.camelot 86 | camelot-test 87 | test 88 | 89 | 90 | com.jayway.awaitility 91 | awaitility 92 | 1.7.0 93 | test 94 | 95 | 96 | org.hamcrest 97 | hamcrest-library 98 | 99 | 100 | org.hamcrest 101 | hamcrest-core 102 | 103 | 104 | cglib-nodep 105 | cglib 106 | 107 | 108 | 109 | 110 | org.hamcrest 111 | hamcrest-all 112 | test 113 | 114 | 115 | ru.yandex.qatools.properties 116 | properties-loader 117 | test 118 | 119 | 120 | com.google.code.gson 121 | gson 122 | 2.5 123 | test 124 | 125 | 126 | 127 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/gridrouter/SessionsAggregator.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 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.sessions.StatsCounter; 8 | import ru.qatools.selenograph.ext.SelenographDB; 9 | import ru.yandex.qatools.camelot.common.ProcessingEngine; 10 | 11 | import javax.inject.Inject; 12 | import java.time.Duration; 13 | import java.util.*; 14 | import java.util.concurrent.ConcurrentLinkedDeque; 15 | 16 | import static java.lang.System.currentTimeMillis; 17 | import static ru.qatools.selenograph.gridrouter.Key.browserName; 18 | import static ru.qatools.selenograph.gridrouter.Key.browserVersion; 19 | import static ru.yandex.qatools.camelot.api.Constants.Keys.ALL; 20 | 21 | /** 22 | * @author Ilya Sadykov 23 | */ 24 | public class SessionsAggregator implements StatsCounter { 25 | static final String ROUTE_REGEX = "http://([^:]+):(\\d+)"; 26 | private static final Logger LOGGER = LoggerFactory.getLogger(SessionsAggregator.class); 27 | private static final Queue bulkUpsertQueue = new ConcurrentLinkedDeque<>(); 28 | 29 | @Inject 30 | SelenographDB database; 31 | @Inject 32 | ProcessingEngine procEngine; 33 | 34 | @Autowired 35 | public SessionsAggregator(@Value("${selenograph.sessions.bulk.flush.interval.ms}") 36 | long bulkFlushIntervalMs) { 37 | final Timer bulkTimer = new Timer(); 38 | LOGGER.info("Initializing bulk flush timer..."); 39 | bulkTimer.schedule(new TimerTask() { 40 | @Override 41 | public void run() { 42 | flushBulkUpsertBuffer(); 43 | } 44 | }, new Random().nextInt(100), bulkFlushIntervalMs); 45 | } 46 | 47 | @Override 48 | public void startSession(String sessionId, String user, String browser, String version, String route) { 49 | final String name = browserName(browser); 50 | final String ver = browserVersion(version); 51 | LOGGER.info("Starting session {} for {}:{}:{} ({})", sessionId, user, name, ver, route); 52 | final StartSessionEvent startEvent = (StartSessionEvent) new StartSessionEvent() 53 | .withSessionId(sessionId) 54 | .withRoute(route) 55 | .withUser(user) 56 | .withBrowser(name) 57 | .withVersion(ver) 58 | .withTimestamp(currentTimeMillis()); 59 | bulkUpsertQueue.offer(startEvent); 60 | } 61 | 62 | @Override 63 | public void deleteSession(String sessionId, String route) { 64 | LOGGER.info("Removing session {} ({})", sessionId, route); 65 | bulkUpsertQueue.offer(new DeleteSessionEvent().withSessionId(sessionId).withRoute(route)); 66 | } 67 | 68 | @Override 69 | public void updateSession(String sessionId, String route) { 70 | LOGGER.info("Updating session {} ({})", sessionId, route); 71 | bulkUpsertQueue.offer((SessionEvent) new UpdateSessionEvent() 72 | .withSessionId(sessionId).withRoute(route).withTimestamp(currentTimeMillis())); 73 | } 74 | 75 | @Override 76 | public void expireSessionsOlderThan(Duration duration) { 77 | database.deleteSessionsOlderThan(duration.toMillis()); 78 | } 79 | 80 | @Override 81 | public Set getActiveSessions() { 82 | return database.getActiveSessions(); 83 | } 84 | 85 | @Override 86 | public SessionsCountsPerUser getStats(String user) { 87 | final SessionsCountsPerUser stats = (SessionsCountsPerUser) procEngine.getInterop() 88 | .repo(QuotaStatsAggregator.class).get(ALL); 89 | return stats != null ? stats : new SessionsCountsPerUser(); 90 | } 91 | 92 | @Override 93 | public int getSessionsCountForUser(String user) { 94 | return (int) database.countSessionsByUser(user); 95 | } 96 | 97 | @Override 98 | public int getSessionsCountForUserAndBrowser(String user, String browser, String version) { 99 | return (int) database.countSessionsByUserAndBrowser(user, browser, version); 100 | } 101 | 102 | Set sessionsByUser(String user) { 103 | return database.sessionsByUser(user); 104 | } 105 | 106 | public void flushBulkUpsertBuffer() { 107 | try { 108 | LOGGER.info("Flushing upsert buffer. Queue size is {}", bulkUpsertQueue.size()); 109 | SessionEvent event; 110 | final List events = new ArrayList<>(); 111 | while ((event = bulkUpsertQueue.poll()) != null) { 112 | events.add(event); 113 | } 114 | if (!events.isEmpty()) { 115 | database.bulkUpsertSessions(events); 116 | } 117 | } catch (Exception e) { 118 | LOGGER.error("Failed to perform bulk update of sessions", e); 119 | bulkUpsertQueue.clear(); 120 | } 121 | } 122 | 123 | public SessionEvent findSessionById(String sessionId) { 124 | return database.findSessionById(sessionId); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | selenograph 5 | ru.qatools.seleniumkit 6 | 1.3-SNAPSHOT 7 | 8 | 4.0.0 9 | war 10 | 11 | selenograph-server 12 | 13 | 9.2.15.v20160210 14 | /etc/grid-router/users.properties 15 | 16 | 17 | 18 | 19 | ru.yandex.qatools.camelot 20 | camelot-web 21 | war 22 | 23 | 24 | ru.yandex.qatools.camelot 25 | camelot-web 26 | classes 27 | 28 | 29 | ru.qatools.seleniumkit 30 | selenograph-service 31 | 32 | 33 | ru.yandex.qatools.camelot 34 | camelot-serialize-fst 35 | 36 | 37 | ru.yandex.qatools.camelot 38 | camelot-mongodb 39 | 40 | 41 | 42 | 43 | 44 | 45 | org.eclipse.jetty 46 | jetty-maven-plugin 47 | ${jetty.version} 48 | 49 | 0 50 | 51 | / 52 | true 53 | 54 | 9847 55 | tms-local 56 | 57 | 58 | Selenium Grid Router 59 | ${gridrouter.config.users.file} 60 | 61 | 62 | run-forked 63 | 64 | 65 | 66 | org.eclipse.jetty 67 | jetty-continuation 68 | ${jetty.version} 69 | 70 | 71 | org.eclipse.jetty 72 | jetty-servlet 73 | ${jetty.version} 74 | 75 | 76 | org.eclipse.jetty.websocket 77 | websocket-server 78 | ${jetty.version} 79 | 80 | 81 | 82 | 83 | org.apache.maven.plugins 84 | maven-war-plugin 85 | 2.6 86 | 87 | 88 | 89 | ru.yandex.qatools.camelot 90 | camelot-web 91 | 92 | WEB-INF/classes/camelot-web-context.xml 93 | 94 | 95 | 96 | 97 | 98 | 99 | com.github.eirslett 100 | frontend-maven-plugin 101 | 0.0.28 102 | 103 | 104 | install node and npm 105 | 106 | install-node-and-npm 107 | 108 | 109 | v4.3.1 110 | 3.7.4 111 | 112 | 113 | 114 | npm install 115 | 116 | npm 117 | 118 | 119 | install 120 | 121 | 122 | 123 | prepare-package 124 | webpack build 125 | 126 | webpack 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | -------------------------------------------------------------------------------- /service/src/test/java/ru/qatools/selenograph/gridrouter/QuotaStatsAggregatorTest.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.gridrouter; 2 | 3 | import org.hamcrest.Matcher; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.Mockito; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import ru.qatools.selenograph.ext.SelenographDB; 10 | import ru.yandex.qatools.camelot.plugin.GraphiteReportProcessor; 11 | import ru.yandex.qatools.camelot.plugin.GraphiteValue; 12 | import ru.yandex.qatools.camelot.test.*; 13 | 14 | import java.util.Set; 15 | import java.util.UUID; 16 | 17 | import static com.jayway.awaitility.Awaitility.await; 18 | import static java.util.concurrent.TimeUnit.SECONDS; 19 | import static java.util.stream.Collectors.toSet; 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | import static org.hamcrest.Matchers.*; 22 | import static org.hamcrest.core.Is.is; 23 | import static org.hamcrest.core.IsCollectionContaining.hasItem; 24 | import static org.mockito.Mockito.*; 25 | import static ru.yandex.qatools.matchers.decorators.MatcherDecorators.should; 26 | import static ru.yandex.qatools.matchers.decorators.TimeoutWaiter.timeoutHasExpired; 27 | 28 | /** 29 | * @author Ilya Sadykov 30 | */ 31 | @RunWith(CamelotTestRunner.class) 32 | @DisableTimers 33 | public class QuotaStatsAggregatorTest { 34 | public static final int TIMEOUT = 3000; 35 | 36 | @Helper 37 | TestHelper helper; 38 | 39 | @Autowired 40 | SelenographDB db; 41 | 42 | @Autowired 43 | SessionsAggregator sessions; 44 | 45 | @PluginMock 46 | QuotaStatsAggregator quotaStatsMock; 47 | 48 | @PluginMock 49 | GraphiteReportProcessor graphite; 50 | 51 | @Before 52 | public void tearDown() throws Exception { 53 | db.clear(); 54 | } 55 | 56 | @Test 57 | public void testUpdateAfterRemoved() throws Exception { 58 | sessions.updateSession("sessionId", "route"); 59 | flushSessionsBuffer(); 60 | await().atMost(4, SECONDS).until(() -> sessions.getActiveSessions(), not(hasItems("sessionId"))); 61 | sessions.deleteSession("sessionId"); 62 | } 63 | 64 | private void flushSessionsBuffer() { 65 | sessions.flushBulkUpsertBuffer(); 66 | } 67 | 68 | @Test 69 | public void testStartMultipleSessions() throws Exception { 70 | // Launch 3 sessions 71 | String sessionId1 = startSessionFor("vasya"); 72 | String sessionId2 = startSessionFor("vasya"); 73 | String sessionId3 = startSessionFor("vasya"); 74 | await().atMost(4, SECONDS).until(() -> activeSessionsFor("vasya"), 75 | hasItems(sessionId1, sessionId2, sessionId3)); 76 | assertThat(sessions.getSessionsCountForUser("vasya"), is(3)); 77 | sessions.updateSession(sessionId1); 78 | sessions.updateSession(sessionId2); 79 | sessions.updateSession(sessionId3); 80 | 81 | assertThat(activeSessionsFor("vasya"), hasItems(sessionId1, sessionId2, sessionId3)); 82 | 83 | helper.invokeTimerFor(QuotaStatsAggregator.class, "updateQuotaStats"); 84 | verifyQuotaStatsReceived(1); 85 | SessionsState vasyaStats = await().atMost(2, SECONDS).until(() -> statsFor("vasya"), notNullValue()); 86 | assertThat(vasyaStats.getAvg(), is(2)); 87 | assertThat(vasyaStats.getMax(), is(3)); 88 | assertThat(vasyaStats.getRaw(), is(3)); 89 | assertThat(vasyaStats.getBrowser(), is("firefox")); 90 | assertThat(vasyaStats.getUser(), is("vasya")); 91 | assertThat(vasyaStats.getVersion(), is("33.0")); 92 | 93 | // Stop two sessions 94 | stopSessionFor("vasya", sessionId1); 95 | stopSessionFor("vasya", sessionId2); 96 | await().atMost(2, SECONDS).until(() -> activeSessionsFor("vasya"), 97 | allOf(not(hasItem(sessionId1)), not(hasItem(sessionId2)))); 98 | assertThat(sessions.getSessionsCountForUser("vasya"), is(1)); 99 | 100 | helper.invokeTimerFor(QuotaStatsAggregator.class, "updateQuotaStats"); 101 | verifyQuotaStatsReceived(1); 102 | await().atMost(2, SECONDS).until(() -> statsFor("vasya").getRaw(), is(1)); 103 | assertThat(statsFor("vasya").getAvg(), is(2)); 104 | assertThat(statsFor("vasya").getMax(), is(3)); 105 | assertThat(statsFor("vasya").getRaw(), is(1)); 106 | 107 | // Start one more session 108 | String sessionId4 = startSessionFor("vasya"); 109 | assertSessionStateFor("vasya", sessionId4, notNullValue()); 110 | await().atMost(2, SECONDS).until(() -> activeSessionsFor("vasya"), hasItem(sessionId4)); 111 | assertThat(sessions.getSessionsCountForUser("vasya"), is(2)); 112 | assertThat(sessions.getActiveSessions(), containsInAnyOrder(sessionId3, sessionId4)); 113 | 114 | helper.invokeTimerFor(QuotaStatsAggregator.class, "updateQuotaStats"); 115 | verifyQuotaStatsReceived(1); 116 | await().atMost(2, SECONDS).until(() -> statsFor("vasya").getRaw(), is(2)); 117 | assertThat(statsFor("vasya").getAvg(), is(2)); 118 | assertThat(statsFor("vasya").getMax(), is(3)); 119 | assertThat(statsFor("vasya").getRaw(), is(2)); 120 | 121 | // Start one more session 122 | String sessionId5 = startSessionFor("petya"); 123 | assertSessionStateFor("petya", sessionId5, notNullValue()); 124 | await().atMost(2, SECONDS).until(() -> activeSessionsFor("petya"), hasItem(sessionId5)); 125 | assertThat(sessions.getSessionsCountForUser("petya"), is(1)); 126 | assertThat(sessions.getSessionsCountForUser("vasya"), is(2)); 127 | assertThat(sessions.getActiveSessions(), containsInAnyOrder(sessionId3, sessionId4, sessionId5)); 128 | helper.invokeTimerFor(QuotaStatsAggregator.class, "updateQuotaStats"); 129 | verifyQuotaStatsReceived(1); 130 | await().atMost(2, SECONDS).until(() -> statsFor("petya"), notNullValue()); 131 | assertThat(statsFor("petya").getAvg(), is(1)); 132 | assertThat(statsFor("petya").getMax(), is(1)); 133 | assertThat(statsFor("petya").getRaw(), is(1)); 134 | 135 | reset(graphite); 136 | helper.invokeTimerFor(QuotaStatsAggregator.class, "resetStats"); 137 | verify(graphite, timeout(TIMEOUT).times(6)).process(Mockito.any(GraphiteValue.class)); 138 | assertThat(statsFor("petya").getMax(), is(1)); 139 | assertThat(statsFor("petya").getAvg(), is(1)); 140 | assertThat(statsFor("petya").getRaw(), is(1)); 141 | assertThat(statsFor("vasya").getMax(), is(2)); 142 | assertThat(statsFor("vasya").getAvg(), is(2)); 143 | assertThat(statsFor("vasya").getRaw(), is(2)); 144 | } 145 | 146 | protected SessionsState statsFor(String user) { 147 | return sessions.getStats(user).getFor(user, "firefox", "33.0"); 148 | } 149 | 150 | private void verifyQuotaStatsReceived(int times) { 151 | verify(quotaStatsMock, timeout(4000).times(times)) 152 | .updateStats(Mockito.any(), Mockito.any(), Mockito.any()); 153 | reset(quotaStatsMock); 154 | } 155 | 156 | protected void assertSessionStateFor(String user, String sessionId, Matcher matcher) { 157 | assertThat(sessions.findSessionById(sessionId), 158 | should(matcher).whileWaitingUntil(timeoutHasExpired(TIMEOUT))); 159 | } 160 | 161 | protected Set activeSessionsFor(String user) { 162 | return sessions.sessionsByUser(user).stream().map(SessionEvent::getSessionId).collect(toSet()); 163 | } 164 | 165 | protected void stopSessionFor(String user, String sessionId) { 166 | sessions.deleteSession(sessionId); 167 | flushSessionsBuffer(); 168 | } 169 | 170 | protected String startSessionFor(String user) { 171 | final String sessionId = startSessionFor(user, "firefox", "33.0"); 172 | flushSessionsBuffer(); 173 | return sessionId; 174 | } 175 | 176 | protected String startSessionFor(String user, String browser, String version) { 177 | final String sessionId = UUID.randomUUID().toString(); 178 | sessions.startSession(sessionId, user, browser, version); 179 | flushSessionsBuffer(); 180 | return sessionId; 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Selenograph 2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/ru.qatools.seleniumkit/selenograph/badge.svg?style=flat)](https://maven-badges.herokuapp.com/maven-central/ru.qatools.seleniumkit/selenograph) 3 | [![Build status](http://ci.qatools.ru/job/pessimistic-mongodb_master-deploy/badge/icon)](http://ci.qatools.ru/job/selenograph_master-deploy/) 4 | 5 | **Selenograph** is a powered version of [Selenium Grid Router](https://github.com/seleniumkit/gridrouter) 6 | providing more information about currently running Selenium sessions and hubs state. With this information Selenograph finds available browser faster and provides data for the real time status and statistics. 7 | 8 | ![Screenshot](screen.png) 9 | 10 | ## Requirements 11 | 12 | Unlike the original Grid Router, Selenograph has the shared state between nodes, which is stored in database. 13 | We use [MongoDB](https://www.mongodb.org/) (v. 3.2+) as a database, because it provides high availability, fault tolerance and schema-less approach. 14 | Although it's recommended to use a replica set (at least 3 nodes of MongoDB), you can run just a single instance to play with Selenograph. 15 | 16 | ## Features 17 | 18 | * Web interface displaying all available quotas, browsers and versions with the corresponding number of currently running sessions 19 | * REST API showing information about currently running sessions for each quota and hub 20 | * Ability to export to Graphite e.g. in order to visualize quotas and browsers usage statistics 21 | 22 | ## Installation 23 | 24 | For Ubuntu users we provide deb packages. Please note that yandex-selenograph package conflicts with 25 | yandex-grid-router, so if you have previously installed Selenium Grid Router, you'll need to uninstall it first. 26 | 27 | Also ensure that you have Java 8 installed: 28 | ``` 29 | $ sudo add-apt-repository ppa:webupd8team/java 30 | $ sudo apt-get update 31 | $ sudo apt-get install oracle-java8-installer 32 | ``` 33 | 34 | To install Selenograph itself: 35 | ``` 36 | $ sudo add-apt-repository ppa:yandex-qatools/selenograph 37 | $ sudo apt-get update 38 | $ sudo apt-get install yandex-selenograph 39 | $ sudo service yandex-selenograph start 40 | ``` 41 | 42 | You can also run Selenograph in Docker container (not yet available - coming soon): 43 | 44 | ``` 45 | $ sudo docker run --net host \ 46 | --log-opt max-size=1g \ 47 | --log-opt max-file=2 \ 48 | -v /etc/grid-router:/etc/grid-router:ro \ 49 | -v /var/log/grid-router:/var/log/grid-router \ 50 | -d qatools/selenograph:latest 51 | ``` 52 | 53 | ## Configuration 54 | 55 | Most of configuration options duplicate the original [Selenium Grid Router](https://github.com/seleniumkit/gridrouter#configuration) ones. 56 | To configure the MongoDB connection and other Selenograph-specific options, you should edit `/etc/grid-router/selenograph.properties`: 57 | 58 | ``` 59 | ##### Main MongoDB options: ##### 60 | # Specifies the mongodb hosts 61 | camelot.mongodb.replicaset=localhost:27017 62 | 63 | # Set the database name for selenograph 64 | camelot.mongodb.dbname=selenograph 65 | 66 | # Set the username for selenograph user 67 | camelot.mongodb.username= 68 | 69 | # Set the password for selenograph user 70 | camelot.mongodb.password= 71 | 72 | 73 | ##### Graphite export options: ##### 74 | # Specifies the graphite api host (uncomment to enable) 75 | # graphite.host=127.0.0.1 76 | 77 | # Specifies the graphite api port 78 | # graphite.port=42000 79 | 80 | # Specifies the prefix for metrics 81 | # selenograph.gridrouter.graphite.prefix=selenograph 82 | 83 | 84 | ##### Advanced MongoDB options: ##### 85 | # The following options are not recommended to change 86 | # Edit them at your own risk only if you want to configure the connection options 87 | # camelot.mongodb.connections.per.host=30 88 | # camelot.mongodb.threads.connection.mult=40 89 | # camelot.mongodb.connect.timeout=15000 90 | # camelot.mongodb.heartbeat.timeout=15000 91 | # camelot.mongodb.heartbeat.frequency=1000 92 | # camelot.mongodb.heartbeat.socket.timeout=10000 93 | # camelot.mongodb.readpreference=PRIMARY_PREFERRED 94 | # camelot.mongodb.socket.timeout=60000 95 | # camelot.mongodb.waitForLockSec=120 96 | # camelot.mongodb.lockPollMaxIntervalMs=7 97 | ``` 98 | 99 | 100 | ## REST API 101 | 102 | ### /quota 103 | Requires Basic HTTP authorization with quota credentials. 104 | Shows information about browsers available in current quota. Output example: 105 | ``` 106 | { 107 | "firefox:33.0":135, 108 | "chrome:opera-28.0":10, 109 | "chrome:opera-29.0":10, 110 | "firefox:37.0":55, 111 | "firefox:36.0":30, 112 | "firefox:39.0":10, 113 | "firefox:38.0":110, 114 | "chrome:48.0":100, 115 | "chrome:43.0":10, 116 | "internet explorer:9":20, 117 | "chrome:42.0":130, 118 | "internet explorer:8":20, 119 | "chrome:45.0":20, 120 | "chrome:44.0":10, 121 | "chrome:opera-30.0":10, 122 | "MicrosoftEdge:12.1":10, 123 | "firefox:40.0":20, 124 | "internet explorer:11":20, 125 | "firefox:41.0":20, 126 | "iOS:7.1":2, 127 | "internet explorer:10":20, 128 | "chrome:yandex-browser":25, 129 | "chrome:41.0":55, 130 | "chrome:40.0":5, 131 | "iOS:8.4":2, 132 | "android:6.0":5, 133 | "opera:12.16":20 134 | } 135 | ``` 136 | ### /stats 137 | Requires Basic HTTP authorization with quota credentials. 138 | Returns usage statistics for each browser version for quota. `max` and `avg` numbers displays the maximum and average 139 | concurrent sessions during current minute. `raw` and `current` displays the currently running sessions count. 140 | Output example: 141 | ``` 142 | { 143 | "selenium:firefox:36.0":{ 144 | "max":0, 145 | "avg":0, 146 | "raw":0, 147 | "current":0 148 | }, 149 | "selenium:chrome:41.0":{ 150 | "max":0, 151 | "avg":0, 152 | "raw":0, 153 | "current":0 154 | }, 155 | "selenium:firefox:37.0":{ 156 | "max":0, 157 | "avg":0, 158 | "raw":0, 159 | "current":0 160 | }, 161 | "selenium:firefox:38.0":{ 162 | "max":0, 163 | "avg":0, 164 | "raw":0, 165 | "current":0 166 | } 167 | } 168 | ``` 169 | ### /ping 170 | A ping API for load balancers. Returns 200 when service is functioning properly. 171 | 172 | ### /api/selenograph/strategy 173 | Shows a list of all available hubs with percentage of free browsers (100% for completely free hub). 174 | This information is used while selecting the next hub to route the session to. Output example: 175 | ``` 176 | { 177 | "lastUpdated":"Feb,26 12:39:13.061", 178 | "hubs":{ 179 | "firefox33-1.selenium.net:4445":100, 180 | "firefox33-2.selenium.net:4445":75, 181 | "firefox42-1.selenium.net:4445":50, 182 | "firefox42-2.selenium.net:4445":100, 183 | "chrome45-1.selenium.net:4444":80, 184 | "firefox38-1.selenium.net:4445":100, 185 | } 186 | } 187 | ``` 188 | ### /api/selenograph/quotas 189 | Shows information about all available quotas: which browser versions exist and how many browsers are available for each version. 190 | This info is actually the mirror of the configured Gridrouter quotas enriched with currently running sessions count. 191 | ``` 192 | { 193 | "all":[ 194 | { 195 | "versions":[ 196 | { 197 | "version":"33.0", 198 | "running":211, 199 | "max":500 200 | } 201 | ], 202 | "name":"firefox", 203 | "running":211, 204 | "max":500 205 | }, 206 | { 207 | "versions":[ 208 | { 209 | "version":"11.0", 210 | "running": 280, 211 | "max": 584, 212 | "occupied":0 213 | }, 214 | ], 215 | "name":"internet explorer", 216 | "running":0, 217 | "max":584, 218 | "occupied":0 219 | }, 220 | ], 221 | "nick-ie11":[ 222 | { 223 | "versions":[ 224 | { 225 | "version":"11.0", 226 | "running": 280, 227 | "max": 584 228 | }, 229 | ], 230 | "name":"internet explorer", 231 | "running":0, 232 | "max": 584 233 | }, 234 | ], 235 | "john-firefox":[ 236 | { 237 | "versions":[ 238 | { 239 | "version":"33.0", 240 | "running": 211, 241 | "max": 500 242 | }, 243 | ], 244 | "name":"firefox", 245 | "running":211, 246 | "max": 500 247 | }, 248 | ] 249 | } 250 | ``` 251 | 252 | ### /api/selenograph/quota/:name 253 | 254 | The same as `/api/selenograph/quotas`, but for the single quota name. 255 | 256 | ``` 257 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | 5 | ru.qatools.seleniumkit 6 | selenograph 7 | pom 8 | 1.3-SNAPSHOT 9 | 10 | 11 | org.sonatype.oss 12 | oss-parent 13 | 9 14 | 15 | 16 | 17 | beans 18 | service 19 | server 20 | 21 | 22 | 23 | 1.8 24 | UTF-8 25 | 1.31 26 | 2.5.4 27 | 2.7.3 28 | 29 | 30 | 31 | scm:git:git@github.com:seleniumkit/selenograph.git 32 | scm:git:git@github.com:seleniumkit/selenograph.git 33 | scm:git:git@github.com:seleniumkit/selenograph.git 34 | HEAD 35 | 36 | 37 | 38 | 39 | 40 | 41 | ${project.groupId} 42 | selenograph-beans 43 | ${project.version} 44 | 45 | 46 | ${project.groupId} 47 | selenograph-service 48 | ${project.version} 49 | 50 | 51 | 52 | 53 | ru.yandex.qatools.camelot 54 | camelot-core 55 | ${camelot.version} 56 | 57 | 58 | ru.yandex.qatools.camelot 59 | camelot-web 60 | ${camelot.version} 61 | war 62 | 63 | 64 | com.fasterxml.jackson.core 65 | jackson-core 66 | 67 | 68 | com.fasterxml.jackson.core 69 | jackson-annotations 70 | 71 | 72 | com.fasterxml.jackson.core 73 | jackson-databind 74 | 75 | 76 | 77 | 78 | com.fasterxml.jackson.core 79 | jackson-core 80 | ${jackson.version} 81 | 82 | 83 | com.fasterxml.jackson.core 84 | jackson-annotations 85 | ${jackson.version} 86 | 87 | 88 | com.fasterxml.jackson.core 89 | jackson-databind 90 | ${jackson.version} 91 | 92 | 93 | ru.yandex.qatools.camelot 94 | camelot-web 95 | ${camelot.version} 96 | classes 97 | 98 | 99 | ru.yandex.qatools.camelot 100 | camelot-api 101 | ${camelot.version} 102 | 103 | 104 | ru.yandex.qatools.camelot 105 | camelot-test 106 | ${camelot.version} 107 | test 108 | 109 | 110 | ru.yandex.qatools.camelot 111 | camelot-serialize-fst 112 | ${camelot.version} 113 | 114 | 115 | ru.yandex.qatools.camelot 116 | camelot-mongodb 117 | ${camelot.version} 118 | 119 | 120 | ru.yandex.qatools.clay 121 | clay-utils 122 | 2.5 123 | 124 | 125 | 126 | 127 | ru.qatools.seleniumkit 128 | gridrouter-proxy 129 | ${gridrouter.version} 130 | classes 131 | 132 | 133 | 134 | 135 | org.jvnet.jaxb2_commons 136 | jaxb2-basics-runtime 137 | 0.9.5 138 | 139 | 140 | 141 | 142 | ru.yandex.qatools.properties 143 | properties-loader 144 | 1.5 145 | 146 | 147 | 148 | 149 | org.slf4j 150 | slf4j-log4j12 151 | 1.7.12 152 | 153 | 154 | 155 | 156 | commons-io 157 | commons-io 158 | 2.4 159 | 160 | 161 | org.apache.commons 162 | commons-collections4 163 | 4.0 164 | 165 | 166 | 167 | 168 | junit 169 | junit 170 | 4.12 171 | test 172 | 173 | 174 | org.hamcrest 175 | hamcrest-all 176 | 1.3 177 | test 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | org.jvnet.jaxb2.maven2 186 | maven-jaxb2-plugin 187 | 0.12.3 188 | 189 | 190 | 191 | generate 192 | 193 | 194 | 195 | 196 | src/main/resources/xsd 197 | true 198 | true 199 | true 200 | 201 | -Xannotate 202 | -Xinheritance 203 | -Xequals 204 | -XhashCode 205 | -XtoString 206 | -Xfluent-api 207 | 208 | 209 | 210 | net.java.dev.jaxb2-commons 211 | jaxb-fluent-api 212 | 2.1.8 213 | 214 | 215 | org.jvnet.jaxb2_commons 216 | jaxb2-basics 217 | 0.9.5 218 | 219 | 220 | org.jvnet.jaxb2_commons 221 | jaxb2-basics-annotate 222 | 1.0.2 223 | 224 | 225 | 226 | 227 | 228 | org.apache.maven.plugins 229 | maven-source-plugin 230 | 2.1.2 231 | 232 | 233 | 234 | jar-no-fork 235 | 236 | 237 | 238 | 239 | 240 | org.apache.maven.plugins 241 | maven-compiler-plugin 242 | 3.1 243 | 244 | ${compile.version} 245 | ${compile.version} 246 | 247 | 248 | 249 | org.apache.maven.plugins 250 | maven-release-plugin 251 | 2.5.3 252 | 253 | 254 | 255 | 256 | 257 | 258 | -------------------------------------------------------------------------------- /service/src/main/java/ru/qatools/selenograph/ext/SelenographDB.java: -------------------------------------------------------------------------------- 1 | package ru.qatools.selenograph.ext; 2 | 3 | import com.mongodb.MongoClient; 4 | import com.mongodb.bulk.BulkWriteResult; 5 | import com.mongodb.client.MongoCollection; 6 | import com.mongodb.client.MongoDatabase; 7 | import com.mongodb.client.model.DeleteOneModel; 8 | import com.mongodb.client.model.UpdateOneModel; 9 | import com.mongodb.client.model.UpdateOptions; 10 | import org.apache.commons.lang3.tuple.Pair; 11 | import org.bson.Document; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import ru.qatools.gridrouter.ConfigRepository; 15 | import ru.qatools.gridrouter.config.Browsers; 16 | import ru.qatools.selenograph.gridrouter.*; 17 | 18 | import javax.inject.Inject; 19 | import java.util.*; 20 | import java.util.function.Consumer; 21 | 22 | import static com.mongodb.client.model.Filters.*; 23 | import static java.lang.System.currentTimeMillis; 24 | import static java.util.Arrays.asList; 25 | import static java.util.Collections.emptyMap; 26 | import static java.util.Collections.sort; 27 | import static java.util.stream.Collectors.*; 28 | import static org.apache.commons.lang3.tuple.Pair.of; 29 | import static ru.qatools.selenograph.ext.SelenographMongoSerializer.OBJECT_FIELD; 30 | import static ru.qatools.selenograph.gridrouter.Key.browserName; 31 | import static ru.qatools.selenograph.gridrouter.Key.browserVersion; 32 | import static ru.yandex.qatools.camelot.util.MapUtil.map; 33 | 34 | /** 35 | * @author Ilya Sadykov 36 | * WARN: MongoDB extension direct dependency! 37 | */ 38 | @SuppressWarnings("unchecked") 39 | public class SelenographDB { 40 | public static final String ALL = "all"; 41 | private static final Logger LOGGER = LoggerFactory.getLogger(SelenographDB.class); 42 | private static final String SESSIONS_COL_NAME = "sessions"; 43 | private final MongoClient mongo; 44 | private final String dbName; 45 | private final SelenographMongoSerializer serializer; 46 | @Inject 47 | private ConfigRepository config; 48 | 49 | public SelenographDB(MongoClient mongo, String dbName, 50 | SelenographMongoSerializer serializer) { 51 | this.mongo = mongo; 52 | this.dbName = dbName; 53 | this.serializer = serializer; 54 | } 55 | 56 | private static Map> quotaCounts(Map quotaMap) { 57 | Map> res = new LinkedHashMap<>(); 58 | Map, Integer> hubMax = new HashMap<>(); 59 | quotaMap.entrySet().forEach(e -> { 60 | final String quota = e.getKey(); 61 | res.putIfAbsent(quota, new LinkedHashMap<>()); 62 | e.getValue().getBrowsers().forEach(b -> 63 | b.getVersions().forEach(v -> { 64 | final BrowserContext key = new UserBrowser().withBrowser(browserName(b.getName())) 65 | .withVersion(browserVersion(v.getNumber())) 66 | .withTimestamp(0); 67 | v.getRegions().stream() 68 | .flatMap(r -> r.getHosts().stream()) 69 | .forEach(h -> { 70 | final Pair pair = of(key, h.getAddress()); 71 | if (!hubMax.containsKey(pair) || hubMax.get(pair) < h.getCount()) { 72 | hubMax.put(pair, h.getCount()); 73 | } 74 | }); 75 | res.get(quota).putIfAbsent(key, 0); 76 | res.get(quota).put(key, res.get(quota).get(key) + v.getCount()); 77 | })); 78 | }); 79 | res.put(ALL, new HashMap<>()); 80 | hubMax.entrySet().stream().collect(groupingBy(e -> e.getKey().getKey(), summingInt(Map.Entry::getValue))) 81 | .entrySet().forEach(e -> res.get(ALL).putIfAbsent(e.getKey(), e.getValue())); 82 | return res; 83 | } 84 | 85 | private static Map> runningCounts(Map counts) { 86 | Map> res = new LinkedHashMap<>(); 87 | counts.entrySet().forEach(e -> { 88 | final BrowserContext context = e.getKey(); 89 | final String quota = context.getUser(); 90 | final BrowserContext key = new UserBrowser() 91 | .withBrowser(browserName(context.getBrowser())) 92 | .withVersion(browserVersion(context.getVersion())) 93 | .withTimestamp(0); 94 | res.putIfAbsent(ALL, new HashMap<>()); 95 | res.putIfAbsent(quota, new HashMap<>()); 96 | res.get(ALL).putIfAbsent(key, 0); 97 | res.get(quota).putIfAbsent(key, 0); 98 | res.get(ALL).put(key, res.get(ALL).get(key) + e.getValue()); 99 | res.get(quota).put(key, res.get(quota).get(key) + e.getValue()); 100 | }); 101 | return res; 102 | } 103 | 104 | public void init() { 105 | sessions().createIndex(new Document(map( 106 | "object.user", 1, 107 | "object.browser", 1, 108 | "object.version", 1 109 | ))); 110 | sessions().createIndex(new Document(map( 111 | "object.user", 1 112 | ))); 113 | sessions().createIndex(new Document(map( 114 | "object.timestamp", 1 115 | ))); 116 | } 117 | 118 | public Map getQuotasSummary() { 119 | final Map> running = runningCounts(sessionsByUserCount()); 120 | final Map quotaMap = config.getQuotaMap(); 121 | final List quotas = new ArrayList<>(); 122 | quotas.add(ALL); 123 | quotas.addAll(quotaMap.keySet()); 124 | sort(quotas); 125 | final Map> available = quotaCounts(quotaMap); 126 | final Map state = new LinkedHashMap<>(); 127 | quotas.forEach(quota -> { 128 | state.putIfAbsent(quota, new BrowserSummaries()); 129 | state.get(quota).addOrIncrement( 130 | available.getOrDefault(quota, emptyMap()), 131 | running.getOrDefault(quota, emptyMap()) 132 | ); 133 | }); 134 | state.values().forEach(BrowserSummaries::sort); 135 | return state; 136 | } 137 | 138 | public Map sessionsByUserCount() { 139 | Map results = new HashMap<>(); 140 | sessions().aggregate(asList( 141 | new Document("$unwind", new Document(map( 142 | "path", "$object", 143 | "includeArrayIndex", "index" 144 | ))), 145 | new Document("$match", new Document("index", 1)), 146 | new Document("$project", new Document("object", 1)), 147 | new Document("$project", new Document(map( 148 | "user", "$object.user", 149 | "browser", "$object.browser", 150 | "version", "$object.version" 151 | ))), 152 | new Document("$group", new Document(map( 153 | "_id", new Document(map( 154 | "user", "$user", 155 | "browser", "$browser", 156 | "version", "$version" 157 | )), 158 | "user", new Document("$first", "$user"), 159 | "browser", new Document("$first", "$browser"), 160 | "version", new Document("$first", "$version"), 161 | "count", new Document("$sum", 1) 162 | ))) 163 | )).forEach((Consumer) d -> results.put( 164 | new UserBrowser() 165 | .withUser(d.getString("user")) 166 | .withBrowser(d.getString("browser")) 167 | .withVersion(d.getString("version")), 168 | d.getInteger("count") 169 | )); 170 | return results; 171 | } 172 | 173 | public long countSessionsByUser(String user) { 174 | return sessions().count(eq("object.user", user)); 175 | } 176 | 177 | public long countSessionsByUserAndBrowser(String user, String browser, String version) { 178 | return sessions().count(and( 179 | eq("object.user", user), 180 | eq("object.browser", browser), 181 | eq("object.version", version) 182 | )); 183 | } 184 | 185 | public Set sessionsByUser(String user) { 186 | final Set result = new LinkedHashSet<>(); 187 | sessions().find(eq("object.user", user)).forEach((Consumer) document -> 188 | result.add(convertDocument(document, SessionEvent.class))); 189 | return result; 190 | } 191 | 192 | public void deleteSessionsOlderThan(long timeout) { 193 | sessions().deleteMany(lt("object.timestamp", currentTimeMillis() - timeout)); 194 | } 195 | 196 | public Set getActiveSessions() { 197 | final Set result = new LinkedHashSet<>(); 198 | sessions().find().projection(new Document("_id", 1)) 199 | .map(d -> d.getString("_id")) 200 | .forEach((Consumer) result::add); 201 | return result; 202 | } 203 | 204 | public SessionEvent findSessionById(String sessionId) { 205 | return convertDocument( 206 | sessions().find(eq("_id", sessionId)).first(), SessionEvent.class 207 | ); 208 | } 209 | 210 | public void bulkUpsertSessions(Collection events) { 211 | final BulkWriteResult res = sessions().bulkWrite(events.stream().map(event -> { 212 | if (event instanceof DeleteSessionEvent) { 213 | return new DeleteOneModel(new Document("_id", event.getSessionId())); 214 | } else if (event instanceof UpdateSessionEvent) { 215 | return new UpdateOneModel( 216 | new Document("_id", event.getSessionId()).append("object.sessionId", event.getSessionId()), 217 | new Document("$set", new Document("object.$.timestamp", event.getTimestamp())) 218 | ); 219 | } else { 220 | return new UpdateOneModel( 221 | new Document("_id", event.getSessionId()), 222 | new Document("$set", new Document("object", serializer.toDBObject(event).get(OBJECT_FIELD))), 223 | new UpdateOptions().upsert(true) 224 | ); 225 | } 226 | }).collect(toList())); 227 | LOGGER.info("Sessions update in bulk results: {} created, {} updated, {} deleted, {} upserted", 228 | res.getInsertedCount(), res.getModifiedCount(), res.getDeletedCount(), res.getUpserts().size()); 229 | } 230 | 231 | private T convertDocument(Document document, Class clazz) { 232 | try { 233 | return (T) serializer.fromDBObject(document, clazz); 234 | } catch (Exception e) { 235 | LOGGER.error("Failed to conert mongo document", e); 236 | return null; 237 | } 238 | } 239 | 240 | private MongoCollection sessions() { 241 | return mongo.getDatabase(dbName).getCollection(SESSIONS_COL_NAME); 242 | } 243 | 244 | public void clear() { 245 | MongoDatabase db = mongo.getDatabase(dbName); 246 | for (String colName : db.listCollectionNames()) { 247 | db.getCollection(colName).drop(); 248 | } 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /server/typings/main/definitions/webpack/webpack.d.ts: -------------------------------------------------------------------------------- 1 | // Compiled using typings@0.6.8 2 | // Source: https://raw.githubusercontent.com/tkqubo/typed-webpack/10d2cc44a01602b87f8e999eec90192e7e6b3edc/index.d.ts 3 | declare module 'webpack/index' { 4 | // Type definitions for webpack 1.12.9 5 | // Project: https://github.com/webpack/webpack 6 | // Definitions by: Qubo 7 | // Original Definitions: https://github.com/borisyankov/DefinitelyTyped 8 | 9 | function webpack(config: webpack.Configuration, callback?: webpack.compiler.CompilerCallback): webpack.compiler.Compiler; 10 | namespace webpack { 11 | interface Configuration { 12 | context?: string; 13 | entry?: string|string[]|Entry; 14 | /** Choose a developer tool to enhance debugging. */ 15 | devtool?: string; 16 | /** Options affecting the output. */ 17 | output?: Output; 18 | /** Options affecting the normal modules (NormalModuleFactory) */ 19 | module?: Module; 20 | /** Options affecting the resolving of modules. */ 21 | resolve?: Resolve; 22 | /** Like resolve but for loaders. */ 23 | resolveLoader?: ResolveLoader; 24 | /** 25 | * Specify dependencies that shouldn’t be resolved by webpack, but should become dependencies of the resulting bundle. 26 | * The kind of the dependency depends on output.libraryTarget. 27 | */ 28 | externals?: ExternalsElement|ExternalsElement[]; 29 | /** 30 | *
    31 | *
  • "web" Compile for usage in a browser-like environment (default)
  • 32 | *
  • "webworker" Compile as WebWorker
  • 33 | *
  • "node" Compile for usage in a node.js-like environment (use require to load chunks)
  • 34 | *
  • "async-node" Compile for usage in a node.js-like environment (use fs and vm to load chunks async)
  • 35 | *
  • "node-webkit" Compile for usage in webkit, uses jsonp chunk loading but also supports builtin node.js modules plus require(“nw.gui”) (experimental)
  • 36 | *
  • "atom" Compile for usage in electron (formerly known as atom-shell), supports require for modules necessary to run Electron.
  • 37 | *
      38 | */ 39 | target?: string; 40 | /** Report the first error as a hard error instead of tolerating it. */ 41 | bail?: boolean; 42 | /** Capture timing information for each module. */ 43 | profile?: boolean; 44 | /** Cache generated modules and chunks to improve performance for multiple incremental builds. */ 45 | cache?: boolean|any; 46 | /** Enter watch mode, which rebuilds on file change. */ 47 | watch?: boolean; 48 | watchOptions?: WatchOptions; 49 | /** Switch loaders to debug mode. */ 50 | debug?: boolean; 51 | /** Can be used to configure the behaviour of webpack-dev-server when the webpack config is passed to webpack-dev-server CLI. */ 52 | devServer?: any; // TODO: Type this 53 | /** Include polyfills or mocks for various node stuff */ 54 | node?: Node; 55 | /** Set the value of require.amd and define.amd. */ 56 | amd?: { [moduleName: string]: boolean }; 57 | /** Used for recordsInputPath and recordsOutputPath */ 58 | recordsPath?: string; 59 | /** Load compiler state from a json file. */ 60 | recordsInputPath?: string; 61 | /** Store compiler state to a json file. */ 62 | recordsOutputPath?: string; 63 | /** Add additional plugins to the compiler. */ 64 | plugins?: (Plugin|Function)[]; 65 | } 66 | 67 | interface Entry { 68 | [name: string]: string|string[]; 69 | } 70 | 71 | interface Output { 72 | /** The output directory as absolute path (required). */ 73 | path?: string; 74 | /** The filename of the entry chunk as relative path inside the output.path directory. */ 75 | filename?: string; 76 | /** The filename of non-entry chunks as relative path inside the output.path directory. */ 77 | chunkFilename?: string; 78 | /** The filename of the SourceMaps for the JavaScript files. They are inside the output.path directory. */ 79 | sourceMapFilename?: string; 80 | /** Filename template string of function for the sources array in a generated SourceMap. */ 81 | devtoolModuleFilenameTemplate?: string; 82 | /** Similar to output.devtoolModuleFilenameTemplate, but used in the case of duplicate module identifiers. */ 83 | devtoolFallbackModuleFilenameTemplate?: string; 84 | /** 85 | * Enable line to line mapped mode for all/specified modules. 86 | * Line to line mapped mode uses a simple SourceMap where each line of the generated source is mapped to the same line of the original source. 87 | * It’s a performance optimization. Only use it if your performance need to be better and you are sure that input lines match which generated lines. 88 | * true enables it for all modules (not recommended) 89 | */ 90 | devtoolLineToLine?: boolean; 91 | /** The filename of the Hot Update Chunks. They are inside the output.path directory. */ 92 | hotUpdateChunkFilename?: string; 93 | /** The filename of the Hot Update Main File. It is inside the output.path directory. */ 94 | hotUpdateMainFilename?: string; 95 | /** The output.path from the view of the Javascript / HTML page. */ 96 | publicPath?: string; 97 | /** The JSONP function used by webpack for asnyc loading of chunks. */ 98 | jsonpFunction?: string; 99 | /** The JSONP function used by webpack for async loading of hot update chunks. */ 100 | hotUpdateFunction?: string; 101 | /** Include comments with information about the modules. */ 102 | pathinfo?: boolean; 103 | /** If set, export the bundle as library. output.library is the name. */ 104 | library?: boolean; 105 | /** 106 | * Which format to export the library: 107 | *
        108 | *
      • "var" - Export by setting a variable: var Library = xxx (default)
      • 109 | *
      • "this" - Export by setting a property of this: this["Library"] = xxx
      • 110 | *
      • "commonjs" - Export by setting a property of exports: exports["Library"] = xxx
      • 111 | *
      • "commonjs2" - Export by setting module.exports: module.exports = xxx
      • 112 | *
      • "amd" - Export to AMD (optionally named)
      • 113 | *
      • "umd" - Export to AMD, CommonJS2 or as property in root
      • 114 | *
      115 | */ 116 | libraryTarget?: string; 117 | /** If output.libraryTarget is set to umd and output.library is set, setting this to true will name the AMD module. */ 118 | umdNamedDefine?: boolean; 119 | /** Prefixes every line of the source in the bundle with this string. */ 120 | sourcePrefix?: string; 121 | /** This option enables cross-origin loading of chunks. */ 122 | crossOriginLoading?: string|boolean; 123 | } 124 | 125 | interface Module { 126 | /** A array of automatically applied loaders. */ 127 | loaders?: Loader[]; 128 | /** A array of applied pre loaders. */ 129 | preLoaders?: Loader[]; 130 | /** A array of applied post loaders. */ 131 | postLoaders?: Loader[]; 132 | /** A RegExp or an array of RegExps. Don’t parse files matching. */ 133 | noParse?: RegExp|RegExp[]; 134 | unknownContextRequest?: string; 135 | unknownContextRecursive?: boolean; 136 | unknownContextRegExp?: RegExp; 137 | unknownContextCritical?: boolean; 138 | exprContextRequest?: string; 139 | exprContextRegExp?: RegExp; 140 | exprContextRecursive?: boolean; 141 | exprContextCritical?: boolean; 142 | wrappedContextRegExp?: RegExp; 143 | wrappedContextRecursive?: boolean; 144 | wrappedContextCritical?: boolean; 145 | } 146 | 147 | interface Resolve { 148 | /** Replace modules by other modules or paths. */ 149 | alias: { [key: string]: string; }; 150 | /** 151 | * The directory (absolute path) that contains your modules. 152 | * May also be an array of directories. 153 | * This setting should be used to add individual directories to the search path. */ 154 | root?: string|string[]; 155 | /** 156 | * An array of directory names to be resolved to the current directory as well as its ancestors, and searched for modules. 157 | * This functions similarly to how node finds “node_modules” directories. 158 | * For example, if the value is ["mydir"], webpack will look in “./mydir”, “../mydir”, “../../mydir”, etc. 159 | */ 160 | modulesDirectories?: string[]; 161 | /** 162 | * A directory (or array of directories absolute paths), 163 | * in which webpack should look for modules that weren’t found in resolve.root or resolve.modulesDirectories. 164 | */ 165 | fallback?: string|string[]; 166 | /** 167 | * An array of extensions that should be used to resolve modules. 168 | * For example, in order to discover CoffeeScript files, your array should contain the string ".coffee". 169 | */ 170 | extensions?: string[]; 171 | /** Check these fields in the package.json for suitable files. */ 172 | packageMains?: (string|string[])[]; 173 | /** Check this field in the package.json for an object. Key-value-pairs are threaded as aliasing according to this spec */ 174 | packageAlias?: (string|string[])[]; 175 | /** 176 | * Enable aggressive but unsafe caching for the resolving of a part of your files. 177 | * Changes to cached paths may cause failure (in rare cases). An array of RegExps, only a RegExp or true (all files) is expected. 178 | * If the resolved path matches, it’ll be cached. 179 | */ 180 | unsafeCache?: RegExp|RegExp[]|boolean; 181 | } 182 | 183 | interface ResolveLoader extends Resolve { 184 | /** It describes alternatives for the module name that are tried. */ 185 | moduleTemplates?: string[]; 186 | } 187 | 188 | type ExternalsElement = string|RegExp|ExternalsObjectElement|ExternalsFunctionElement; 189 | 190 | interface ExternalsObjectElement { 191 | [key: string]: boolean|string; 192 | } 193 | 194 | interface ExternalsFunctionElement { 195 | (context: any, request: any, callback: (error: any, result: any) => void): any; 196 | } 197 | 198 | interface WatchOptions { 199 | /** Delay the rebuilt after the first change. Value is a time in ms. */ 200 | aggregateTimeout?: number; 201 | /** true: use polling, number: use polling with specified interval */ 202 | poll?: boolean|number; 203 | } 204 | 205 | interface Node { 206 | console?: boolean; 207 | global?: boolean; 208 | process?: boolean; 209 | Buffer?: boolean; 210 | __filename?: boolean|string; 211 | __dirname?: boolean|string; 212 | [nodeBuiltin: string]: boolean|string; 213 | } 214 | 215 | type LoaderCondition = string|RegExp|((absPath: string) => boolean); 216 | 217 | interface Loader { 218 | /** A condition that must not be met */ 219 | exclude?: LoaderCondition|LoaderCondition[]; 220 | /** A condition that must be met */ 221 | include?: LoaderCondition|LoaderCondition[]; 222 | /** A condition that must be met */ 223 | test: LoaderCondition|LoaderCondition[]; 224 | /** A string of “!” separated loaders */ 225 | loader?: string; 226 | /** A array of loaders as string */ 227 | loaders?: string[]; 228 | query?: { 229 | [name: string]: any; 230 | } 231 | } 232 | 233 | interface Plugin { } 234 | 235 | // declare function webpack 236 | 237 | 238 | /** 239 | * Replace resources that matches resourceRegExp with newResource. 240 | * If newResource is relative, it is resolve relative to the previous resource. 241 | * If newResource is a function, it is expected to overwrite the ‘request’ attribute of the supplied object. 242 | */ 243 | export class NormalModuleReplacementPlugin implements Plugin { 244 | constructor(resourceRegExp: any, newResource: any); 245 | } 246 | 247 | /** 248 | * Replaces the default resource, recursive flag or regExp generated by parsing with newContentResource, 249 | * newContentRecursive resp. newContextRegExp if the resource (directory) matches resourceRegExp. 250 | * If newContentResource is relative, it is resolve relative to the previous resource. 251 | * If newContentResource is a function, it is expected to overwrite the ‘request’ attribute of the supplied object. 252 | */ 253 | export class ContextReplacementPlugin extends Plugin { 254 | constructor(resourceRegExp: any, newContentResource?: any, newContentRecursive?: any, newContentRegExp?: any); 255 | } 256 | 257 | /** 258 | * Don’t generate modules for requests matching the provided RegExp. 259 | */ 260 | export class IgnorePlugin extends Plugin { 261 | constructor(requestRegExp: any, contextRegExp?: any); 262 | } 263 | 264 | /** 265 | * A request for a normal module, which is resolved and built even before a require to it occurs. 266 | * This can boost performance. Try to profile the build first to determine clever prefetching points. 267 | */ 268 | export class PrefetchPlugin extends Plugin { 269 | constructor(context: any, request: any); 270 | constructor(request: any); 271 | } 272 | 273 | /** 274 | * Apply a plugin (or array of plugins) to one or more resolvers (as specified in types). 275 | */ 276 | export class ResolverPlugin extends Plugin { 277 | constructor(plugins: Plugin[], files?: string[]); 278 | } 279 | 280 | namespace ResolverPlugin { 281 | export class DirectoryDescriptionFilePlugin extends Plugin { 282 | constructor(file: string, files: string[]); 283 | } 284 | /** 285 | * This plugin will append a path to the module directory to find a match, 286 | * which can be useful if you have a module which has an incorrect “main” entry in its package.json/bower.json etc (e.g. "main": "Gruntfile.js"). 287 | * You can use this plugin as a special case to load the correct file for this module. Example: 288 | */ 289 | export class FileAppendPlugin extends Plugin { 290 | constructor(files: string[]); 291 | } 292 | } 293 | 294 | 295 | /** 296 | * Adds a banner to the top of each generated chunk. 297 | */ 298 | export class BannerPlugin extends Plugin { 299 | constructor(banner: any, options: any); 300 | } 301 | 302 | /** 303 | * Define free variables. Useful for having development builds with debug logging or adding global constants. 304 | */ 305 | export class DefinePlugin extends Plugin { 306 | constructor(definitions: any); 307 | } 308 | 309 | /** 310 | * Automatically loaded modules. 311 | * Module (value) is loaded when the identifier (key) is used as free variable in a module. 312 | * The identifier is filled with the exports of the loaded module. 313 | */ 314 | export class ProvidePlugin extends Plugin { 315 | constructor(definitions: any); 316 | } 317 | 318 | /** 319 | * Adds SourceMaps for assets. 320 | */ 321 | export class SourceMapDevToolPlugin extends Plugin { 322 | constructor(options: any); 323 | } 324 | 325 | export class HotModuleReplacementPlugin extends Plugin { } 326 | 327 | 328 | /** 329 | * Adds useful free vars to the bundle. 330 | */ 331 | export class ExtendedAPIPlugin extends Plugin { } 332 | 333 | /** 334 | * When there are errors while compiling this plugin skips the emitting phase (and recording phase), 335 | * so there are no assets emitted that include errors. The emitted flag in the stats is false for all assets. 336 | */ 337 | export class NoErrorsPlugin extends Plugin { } 338 | 339 | /** 340 | * Does not watch specified files matching provided paths or RegExps. 341 | */ 342 | export class WatchIgnorePlugin extends Plugin { 343 | constructor(paths: RegExp[]); 344 | } 345 | 346 | /** 347 | * optimize namespace 348 | */ 349 | namespace optimize { 350 | /** 351 | * Search for equal or similar files and deduplicate them in the output. 352 | * This comes with some overhead for the entry chunk, but can reduce file size effectively. 353 | * This is experimental and may crash, because of some missing implementations. (Report an issue) 354 | */ 355 | export class DedupePlugin extends Plugin { } 356 | 357 | /** 358 | * Limit the chunk count to a defined value. Chunks are merged until it fits. 359 | */ 360 | export class LimitChunkCountPlugin extends Plugin { 361 | constructor(options: any); 362 | } 363 | 364 | /** 365 | * Merge small chunks that are lower than this min size (in chars). Size is approximated. 366 | */ 367 | export class MinChunkSizePlugin extends Plugin { 368 | constructor(options: any); 369 | } 370 | 371 | /** 372 | * Assign the module and chunk ids by occurrence count. Ids that are used often get lower (shorter) ids. 373 | * This make ids predictable, reduces to total file size and is recommended. 374 | */ 375 | export class OccurenceOrderPlugin extends Plugin { 376 | constructor(preferEntry: boolean); 377 | } 378 | 379 | /** 380 | * Minimize all JavaScript output of chunks. Loaders are switched into minimizing mode. 381 | * You can pass an object containing UglifyJs options. 382 | */ 383 | export class UglifyJsPlugin extends Plugin { 384 | constructor(options?: MinifyOptions); 385 | } 386 | 387 | /** 388 | * Minification options from uglify-js 389 | */ 390 | export interface MinifyOptions { 391 | spidermonkey?: boolean; 392 | outSourceMap?: string; 393 | sourceRoot?: string; 394 | inSourceMap?: string; 395 | fromString?: boolean; 396 | warnings?: boolean; 397 | mangle?: Object; 398 | output?: MinifyOutput, 399 | compress?: Object; 400 | } 401 | 402 | export interface MinifyOutput { 403 | code: string; 404 | map: string; 405 | } 406 | 407 | export class CommonsChunkPlugin extends Plugin { 408 | constructor(chunkName: string, filenames?: string|string[]); 409 | constructor(options?: any); 410 | } 411 | 412 | /** 413 | * A plugin for a more aggressive chunk merging strategy. 414 | * Even similar chunks are merged if the total size is reduced enough. 415 | * As an option modules that are not common in these chunks can be moved up the chunk tree to the parents. 416 | */ 417 | export class AggressiveMergingPlugin extends Plugin { 418 | constructor(options: any); 419 | } 420 | } 421 | 422 | /** 423 | * dependencies namespace 424 | */ 425 | namespace dependencies { 426 | /** 427 | * Support Labeled Modules. 428 | */ 429 | export class LabeledModulesPlugin extends Plugin { } 430 | } 431 | 432 | namespace compiler { 433 | interface Compiler { 434 | /** Builds the bundle(s). */ 435 | run(callback: CompilerCallback): void; 436 | /** 437 | * Builds the bundle(s) then starts the watcher, which rebuilds bundles whenever their source files change. 438 | * Returns a Watching instance. Note: since this will automatically run an initial build, so you only need to run watch (and not run). 439 | */ 440 | watch(watchOptions: WatchOptions, handler: CompilerCallback): Watching; 441 | //TODO: below are some of the undocumented properties. needs typings 442 | outputFileSystem: any; 443 | name: string; 444 | options: Configuration; 445 | } 446 | 447 | interface Watching { 448 | close(callback: () => void): void; 449 | } 450 | 451 | interface WatchOptions { 452 | /** After a change the watcher waits that time (in milliseconds) for more changes. Default: 300. */ 453 | aggregateTimeout?: number; 454 | /** The watcher uses polling instead of native watchers. true uses the default interval, a number specifies a interval in milliseconds. Default: undefined (automatic). */ 455 | poll?: number|boolean; 456 | } 457 | 458 | interface Stats { 459 | /** Returns true if there were errors while compiling */ 460 | hasErrors(): boolean; 461 | /** Returns true if there were warnings while compiling. */ 462 | hasWarnings(): boolean; 463 | /** Return information as json object */ 464 | toJson(options?: StatsToJsonOptions): any; //TODO: type this 465 | /** Returns a formatted string of the result. */ 466 | toString(options?: StatsToStringOptions): string; 467 | } 468 | 469 | interface StatsToJsonOptions { 470 | /** context directory for request shortening */ 471 | context?: boolean; 472 | /** add the hash of the compilation */ 473 | hash?: boolean; 474 | /** add webpack version information */ 475 | version?: boolean; 476 | /** add timing information */ 477 | timings?: boolean; 478 | /** add assets information */ 479 | assets?: boolean; 480 | /** add chunk information */ 481 | chunks?: boolean; 482 | /** add built modules information to chunk information */ 483 | chunkModules?: boolean; 484 | /** add built modules information */ 485 | modules?: boolean; 486 | /** add children information */ 487 | children?: boolean; 488 | /** add also information about cached (not built) modules */ 489 | cached?: boolean; 490 | /** add information about the reasons why modules are included */ 491 | reasons?: boolean; 492 | /** add the source code of modules */ 493 | source?: boolean; 494 | /** add details to errors (like resolving log) */ 495 | errorDetails?: boolean; 496 | /** add the origins of chunks and chunk merging info */ 497 | chunkOrigins?: boolean; 498 | /** sort the modules by that field */ 499 | modulesSort?: string; 500 | /** sort the chunks by that field */ 501 | chunksSort?: string; 502 | /** sort the assets by that field */ 503 | assetsSort?: string; 504 | } 505 | 506 | interface StatsToStringOptions extends StatsToJsonOptions { 507 | /** With console colors */ 508 | colors?: boolean; 509 | } 510 | 511 | type CompilerCallback = (err: Error, stats: Stats) => void 512 | } 513 | } 514 | 515 | export = webpack; 516 | } 517 | declare module 'webpack' { 518 | import main = require('webpack/index'); 519 | export = main; 520 | } --------------------------------------------------------------------------------