Permalink
Browse files

Major rewrite (version 0.2.0)

Main script:
* Most importantly: The main run() and updateBrowsers() methods
  are completely rewritten to fix all known logic problems. Such
  as where it started the wrong browsers or didn't account for
  certain limits and race conditions, and it now has a priority
  formula, which (among other things) takes into account all
  online clients (not just the workers it started). So it starts
  needed browsers while also allowing other instances of this
  script (or other scripts or human users) to join the swarm
  and fill in the needed roles instead.

* Make browserstack client version explicit. v1 at this time,
  as v2's structure is still a bit odd. Maybe they'll change it
  (request has been made). If not, we'll do a minor refactoring
  to support the v2 flow.

* Added method to start a worker by swarm user agent id (e.g.
  "Chrome|22" as opposed to { browser: 'chrome', version: 22 } ).

Cli script:
* Now uses config.json for most options. It still has a few
  options to activate "verbose" and "dryRun".

* Added a --run-loop option that will execute "run" in a loop
  in child processes.
  Loop will not start if config is invalid. If config becomes
  invalid during the loop the child process will output an error
  message, but the loop will not stop and try again at the next
  interval.

* Actions removed/changes:
  - worker (new: get worker info)
  - spawn (new: start worker by testswarm ua id)
  - dry-run (replacing '--kill' and the old '--run')
  - getNeeded (not needed, info is now part of --verbose)

Misc:
* Various updates to map.js
* New code base passes JSHint (see .jshintrc)
* Updated dependencies in package.json to their latest versions
  and updated code where needed.

References:
* Fixes #16: "cli: Move defaults to commander declaration"
* Fixes #17: "stackLimit is enforced wrong (too low)"
* Fixes #21: "Online workers shouldn't take priority in the loop"
* Fixes #23: "Implement limit for similar workers"
* Fixes #26: "Optimize for multiple accounts"
  • Loading branch information...
1 parent e026288 commit b626290fa57a68d19b3d1563d768fae64d2e2e4c @Krinkle Krinkle committed Sep 29, 2012
Showing with 986 additions and 587 deletions.
  1. +5 −1 .gitignore
  2. +1 −0 .jshintignore
  3. +28 −0 .jshintrc
  4. +41 −68 README.md
  5. +10 −0 config-sample.json
  6. +0 −79 lib/cli.js
  7. +0 −143 lib/map.js
  8. +0 −261 lib/testswarm-browserstack.js
  9. +33 −25 package.json
  10. +5 −10 run-sample.sh
  11. +123 −0 src/cli.js
  12. +175 −0 src/map.js
  13. +498 −0 src/testswarm-browserstack.js
  14. +67 −0 src/util.js
View
6 .gitignore
@@ -1,5 +1,9 @@
-logrotate.conf
+# Using/installing software
node_modules
+config.json
+logrotate.conf
run.log
run.sh
+
+# Other files
.DS_Store
View
1 .jshintignore
@@ -0,0 +1 @@
+node_modules
View
28 .jshintrc
@@ -0,0 +1,28 @@
+{
+ "bitwise": true,
+ "camelcase": true,
+ "curly": true,
+ "eqeqeq": true,
+ "forin": false,
+ "immed": true,
+ "latedef": true,
+ "newcap": true,
+ "noarg": true,
+ "noempty": true,
+ "nonew": true,
+ "plusplus": false,
+ "quotmark": "single",
+ "regexp": true,
+ "undef": true,
+ "unused": true,
+ "strict": false,
+ "trailing": true,
+
+ "smarttabs": true,
+
+ "node": true,
+
+ "nomen": true,
+ "onevar": true,
+ "white": true
+}
View
109 README.md
@@ -1,4 +1,4 @@
-# [testswarm-browserstack](http://jquery.com/)
+# testswarm-browserstack
This is a light weight integration layer between [TestSwarm](https://github.com/jquery/testswarm) and [BrowserStack](http://www.browserstack.com/). Use it to spawn BrowserStack workers needed by TestSwarm on demand.
This script is currently compatible with:
@@ -9,96 +9,69 @@ This script is currently compatible with:
## How to use CLI:
--------------------------------------
<pre>
-node lib/cli.js --swarmUrl "http://swarm.example.org" --swarmRunUrl "http://swarm.example.org/run/swarmuser/" -u "browserstackUser" -p "browserstackPass" --run --kill
+node src/cli.js --run
</pre>
-This above command will spawn (via `--run`) AND kill (via `--kill`) BrowserStack workers as indicated by the TestSwarm `swarmstate` API. This command should be executed on a regular interval, via cron or other scheduler - for more short term requirements, see [cli-repeat](https://github.com/clarkbox/cli-repeat) (command line utility to repeat a command at regular interval).
-
-In most cases, the `--kill` option should always accompany the `--run` option. This will ensure workers are not running idle (although eventually browserstack will still terminate idle workers after `clientTimeout`).
+This above command will create and terminate BrowserStack workers as needed according to the information the TestSwarm `swarmstate` API provides. This command should be executed on a regular interval, either via a scheduler (such as crontab) or by letting node do a continuous loop (using the `--run-loop` option). Be sure to do start it from a scheduler still in a way that it will only start it if it isn't running anymore (in case of an exception).
If you plan to run it from a scheduler and keep log files, you're recommended to use the `run-sample.sh` file as a start. It contains the basic cli invocation as a template. Fill in the argument values and adjust the script and log paths. Also, as a reminder that log files can run out of hand quickly, we've provided a sample file to use in `logrotate` (e.g. on Ubuntu). To learn about logrotate, checkout [Ubuntu manpages](http://manpages.ubuntu.com/manpages/hardy/man8/logrotate.8.html) or the [Slicehost tutorial](http://articles.slicehost.com/2010/6/30/understanding-logrotate-on-ubuntu-part-1) on the subject. To install it, copy the file to `logrotate.conf` within this directory, adjust the path and (if you want to) set different settings. Then move it to `/etc/logrotate.d/testswarm-browserstack.conf`.
## Main scripts:
--------------------------------------
-1. [testswarm-browserstack.js](https://github.com/clarkbox/testswarm-browserstack/blob/master/lib/testswarm-browserstack.js) - Abstraction of TestSwarm API, and Scott González's BrowserStack API. Use it to spawn BrowserStack workers to keep your TestSwarm populated from your own JS code.
-1. [cli.js](https://github.com/clarkbox/testswarm-browserstack/blob/master/lib/cli.js) - nodejs CLI interface wrapper around it all. Allows for scripted or easier manual invocation of browsers.
+1. [testswarm-browserstack.js](https://github.com/Krinkle/testswarm-browserstack/blob/master/src/testswarm-browserstack.js) - Abstraction of TestSwarm API, and Scott González's BrowserStack API. Use it to automatically spawn BrowserStack workers based on your swarm's needs.
+1. [cli.js](https://github.com/Krinkle/testswarm-browserstack/blob/master/src/cli.js) - nodejs cli wrapper around it all. Allows for scripted or generally easy manual invocation of the script.
## testswarm-browserstack.js
--------------------------------------
-### options([options])
-Call this first! get/set the options required to run. Passing in an object literal will set the options. calling without arguments will return the current options.
+#### Options documentation:
+* `browserstack.user`: BrowserStack username
+* `browserstack.pass`: BrowserStack password
+* `browserstack.workerTimeout`: Maximum lifetime of the worker (in seconds). Use `0` for _indefinitely_ (BrowserStack will terminate the worker after the maximum run time, as of writing that maximum is 30 minutes).
+* `browserstack.dryRun`: Enable to only simulate spawning and termination of browserstack workers.
+* `browserstack.totalLimit`: Maximum number of simultaneous workers allowed under this BrowserStack account.
+* `browserstack.eqLimit`: How many simultaneous workers with the same browser are allowed.
+* `testswarm.root`: URL to the root of the TestSwarm installation. Relative to here should be `./index.php` and `./api.php`.
+* `testswarm.runUrl`: URL to the TestSwarm run page (including client name), for BrowserStack workers to open. If your swarm is protected with a token, this is the place to enter the token.
+* `verbose`: Output debug information (through `console.log`).
-#### Example options:
-<pre>
+
+#### Example `config.json`:
+```json
{
- user: 'myUserId',
- pass: 'myPassWurd',
- swarmUrl: 'http://ci.example.org/testswarm',
- swarmRunUrl: 'http://c.example.org/testswarm/run/_SWARM_USERNAME_',
- verbose: false,
- kill: true,
- clientTimeout: 60
+ "browserstack": {
+ "user": "example",
+ "pass": "*******"
+ },
+ "testswarwm": {
+ "root": "http://ci.example.org/testswarm",
+ "runUrl": "http://c.example.org/testswarm/run/JohnDoe"
+ }
}
-</pre>
-
-#### Option Definition:
-* user - BrowserStack username
-* pass - BrowserStack password
-* swarmHost - Hostname of TestSwarm server (without protocol or slash)
-* swarmPath - Path on the server to TestSwarm (without trailing slash)
-* swarmRunUrl - URL to the TestSwarm run page (including client name), for BrowserStack workers to open
-* verbose - Output more debug messages (all output via console.log)
-* kill - Kill BrowserStack workers if they are no longer needed
-* dryRun - Don't actually execute any browserstack worker "terminate" or "start". Only log what it would do. Intended for debugging or getting statistics.
-* stackLimit - How many workers can be running simultaneously in BrowserStack
-* clientTimeout - Number of *seconds* to keep the worker online. The maximum supported by BrowserStack is 1800 seconds (30 minutes).
-
-### getSwarmState(callback):
-* Get statistics about the TestSwarm, keyed by [browser ID](https://github.com/jquery/testswarm/blob/master/config/useragents.ini)
-* parameters:
- * function callback(error, swarmState)
- * error (object|null) - Object with 'code' and 'info' property (TestSwarm API error codes)
- * swarmState (object|undefined) - Swarm state object with all browsers and their pending runs, and active clients.
-
-### run():
-* Start the needed workers. If `--kill` option is set, will also kill any running workers that are no longer needed. Be sure to set options first via `testswarm-browserstack.options({...my options...})`.
-
-### killWorker(workerId):
-Kill a single worker. Calls BrowserStack.terminateWorker()
-* parameters:
- * workerId (integer) - BrowserStack Worker ID as returned by startWorker.
-
-### killAll()
-Kill all workers running on BrowserStack.
-
+```
## cli.js
--------------------------------------
-This is a nodejs CLI interface wrapper around testswarm-browserstack.js. Use --help for all you need to know (see above for usage example):
+This is a nodejs cli wrapper around testswarm-browserstack.js. Use --help to get all the information you need to know (see above for example usage):
-<pre>
+```
Usage: cli.js [options]
Options:
- -h, --help output usage information
- -V, --version output the version number
- --killAll Kill all BrowserStack workers
- --killWorker [workerid] Kill a BrowserStack worker by its worker ID
- --getNeeded Shows a list of browser IDs that have pending jobs in TestSwarm
- --kill Kill BrowserStack workers if they are no longer needed (Only if --run is also specified)
- --run Start new workers in BrowserStack based on the swarm state
- --dryRun Use this option in combination with --kill, --run and/or --killAll. Will stop any action from taking place and only report what it would do in reality. Intended for debugging or getting statistics.
- -u, --user [username] BrowserStack username
- -p, --pass [password] BrowserStack password
- -v, --verbose Output more debug messages (all output via console.log)
- --swarmUrl [url] URL of TestSwarm root (without trailing slash)
- --swarmRunUrl [url] URL to the TestSwarm run page (including client name), for BrowserStack workers to open
- --stackLimit [workers] How many workers can be running simultaneously in BrowserStack (default: 4 workers)
- --clientTimeout [min] Number of minutes to run each client (default: 10 minutes)
-</pre>
+ -h, --help output usage information
+ -V, --version output the version number
+ --config [path] path to config file with options (defaults to ./config.json)
+ --run Retrieve TestSwarm state and spawn/terminate BrowserStack workers as needed
+ --run-loop <timeout> Execute --run in a non-overlapping loop with set timeout (in seconds) between iterations
+ --worker <id> Get info abuot a specific BrowserStack worker
+ --spawn <uaId> Spwawn a BrowserStack worker by swarm useragent id (joining the swarm)
+ --terminate <id> Terminate a specific BrowserStack worker
+ --terminateAll Terminate all BrowserStack workers
+ -v, --verbose Output debug information (through console.log)
+ --dry, --dry-run Simulate spawning and termination of browserstack workers
+```
View
10 config-sample.json
@@ -0,0 +1,10 @@
+{
+ "browserstack": {
+ "user": "example",
+ "pass": "*******"
+ },
+ "testswarwm": {
+ "root": "http://ci.example.org/testswarm",
+ "runUrl": "http://c.example.org/testswarm/run/JohnDoe"
+ }
+}
View
79 lib/cli.js
@@ -1,79 +0,0 @@
-#!/usr/bin/env node
-var program = require('commander'),
- tsbs = require('./testswarm-browserstack');
-
-program
- .version('1.0')
- .option('--killAll', 'Kill all BrowserStack workers')
- .option('--killWorker [workerid]', 'Kill a BrowserStack worker by its worker ID', parseInt)
- .option('--getNeeded', 'Shows a list of browser IDs that have pending jobs in TestSwarm')
- .option('--kill', 'Kill BrowserStack workers if they are no longer needed (Only if --run is also specified)')
- .option('--run', 'Start new workers in BrowserStack based on the swarm state')
- .option('--dryRun', 'Use this option in combination with --kill, --run and/or --killAll. Will stop any action from taking place and only report what it would do in reality. Intended for debugging or getting statistics.')
- .option('-u, --user [username]', 'BrowserStack username', '')
- .option('-p, --pass [password]', 'BrowserStack password', '')
- .option('-v, --verbose', 'Output more debug messages (all output via console.log)')
- .option('--swarmUrl [url]', 'URL of TestSwarm root (without trailing slash)', '')
- .option('--swarmRunUrl [url]', 'URL to the TestSwarm run page (including client name), for BrowserStack workers to open', '')
- .option('--stackLimit [workers]', 'How many workers can be running simultaneously in BrowserStack (default: 4 workers)', parseInt)
- .option('--clientTimeout [min]', 'Number of minutes to run each client (default: 10 minutes)', parseInt)
- .parse(process.argv);
-
-if (!process.argv[2]) {
- console.log(program.helpInformation());
- return;
-}
-
-console.log('\n--\n-- cli.js: ' + new Date().toString() + '\n--');
-
-tsbs.options(program);
-
-if (program.getNeeded) {
- if (!program.swarmUrl) {
- console.log('please set --swarmUrl. stopping.');
- return;
- }
- tsbs.getSwarmState(function (error, swarmState) {
- var browserID, stats,
- needed = [];
- if (error) {
- console.log('getting swarm state failed:\n', error);
- return;
- }
- for (browserID in swarmState.userAgents) {
- stats = swarmState.userAgents[browserID].stats;
- if (stats.onlineClients === 0 && stats.pendingRuns > 0) {
- needed.push(browserID);
- }
- }
- console.log(needed);
- });
-}
-
-if (program.run || program.killWorker || program.killAll) {
- if (!program.pass || !program.user) {
- console.log('please set --user and --pass for browserstack. stopping.');
- return;
- }
-}
-
-if (program.killAll) {
- tsbs.killAll(program.killWorker);
-}
-
-if (program.killWorker) {
- tsbs.killWorker(program.killWorker);
-}
-
-if (program.run) {
- if (!program.swarmUrl || !program.swarmRunUrl) {
- console.log('please set --swarmUrl and --swarmRunUrl. stopping.');
- return;
- }
-
- // Default options
- program.stackLimit = program.stackLimit || 4;
- program.clientTimeout = program.clientTimeout || 600;
-
- tsbs.run();
-}
View
143 lib/map.js
@@ -1,143 +0,0 @@
-/**
- * We need to map the useragent IDs that TestSwarm uses to browser definitions in BrowserStack.
- * TestSwarm useragents.ini: https://github.com/jquery/testswarm/blob/master/config/useragents.ini
- * BrowserStack API: https://github.com/browserstack/api , http://api.browserstack.com/1/browsers
- */
-var map = {
- 'Chrome|17':{
- name:'chrome',
- version:'17.0'
- },
- 'Chrome|18':{
- name:'chrome',
- version:'18.0'
- },
- 'Chrome|19':{
- name:'chrome',
- version:'19.0'
- },
- 'Chrome|20':{
- name:'chrome',
- version:'20.0'
- },
- // 'Firefox|3|5': Not in browserstack anymore
- 'Firefox|3|6':{
- name:'firefox',
- version:'3.6'
- },
- 'Firefox|4':{
- name:'firefox',
- version:'4.0'
- },
- 'Firefox|5':{
- name:'firefox',
- version:'5.0'
- },
- 'Firefox|6':{
- name:'firefox',
- version:'6.0'
- },
- 'Firefox|7':{
- name:'firefox',
- version:'7.0'
- },
- 'Firefox|8':{
- name:'firefox',
- version:'8.0'
- },
- 'Firefox|9':{
- name:'firefox',
- version:'9.0'
- },
- 'Firefox|10':{
- name:'firefox',
- version:'10.0'
- },
- 'Firefox|11':{
- name:'firefox',
- version:'11.0'
- },
- 'Firefox|12':{
- name:'firefox',
- version:'12.0'
- },
- 'Firefox|13':{
- name:'firefox',
- version:'13.0'
- },
- 'Firefox|14':{
- name:'firefox',
- version:'14.0'
- },
- 'IE|6':{
- name:'ie',
- version:'6.0'
- },
- 'IE|7':{
- name:'ie',
- version:'7.0'
- },
- 'IE|8':{
- name:'ie',
- version:'8.0'
- },
- 'IE|9':{
- name:'ie',
- version:'9.0'
- },
- 'IE|10':{
- name:'ie',
- version:'10.0'
- },
- 'Opera|11|10':{
- name:'opera',
- version:'11.1'
- },
- 'Opera|11|50':{
- name:'opera',
- version:'11.5'
- },
- 'Opera|11|60':{
- name:'opera',
- version:'11.6'
- },
- 'Opera|12|0':{
- name:'opera',
- version:'12.0'
- },
- // 'Opera|12|0': No browserstack yet
- 'Safari|4':{
- name:'safari',
- version:'4.0'
- },
- 'Safari|5|0':{
- name:'safari',
- version:'5.0'
- },
- 'Safari|5|1':{
- name:'safari',
- version:'5.1'
- }
- // TODO: Need BrowserStack API v2 for other platforms (issue #19)
- // 'Android|1|5': {},
- // 'Android|1|6': {},
- // 'Android|2|1': {},
- // 'Android|2|2': {},
- // 'Android|2|3': {},
- // 'Fennec|4': {},
- // 'iPhone|3|2': {},
- // 'iPhone|4|3': {},
- // 'iPhone|5': {},
- // 'iPad|3|2': {},
- // 'iPad|4|3': {},
- // 'iPad|5': {},
- // 'Opera Mobile': {},
- // 'Opera Mini|2': {},
- // 'Palm Web|1': {},
- // 'Palm Web|2': {},
- // 'IEMobile|7': {},
-};
-
-module.exports = {
- map:map
-};
View
261 lib/testswarm-browserstack.js
@@ -1,261 +0,0 @@
-var request = require('request'),
- BrowserStack = require('browserstack'),
- async = require('async'),
- browserMap = require("./map").map;
-var self;
-
-var TestSwarmBrowserStackInteg = {
-
- options:function (options) {
- if (!options) {
- return self._options;
- }
- self._options = options;
- },
-
- client:function () {
- if (self._client) {
- return self._client;
- }
- self._client = BrowserStack.createClient({
- username:self.options().user,
- password:self.options().pass
- });
- return self._client;
- },
-
- /**
- * Get the swarmstate of testswarm (number of active clients and pending runs).
- * @param callback Function
- */
- getSwarmState:function (callback) {
- request.get(self.options().swarmUrl + '/api.php?action=swarmstate', function (error, res, body) {
- var apiData = JSON.parse(body);
- if (apiData) {
- if (apiData.error) {
- callback(apiData.error);
- return;
- }
- if (apiData.swarmstate) {
- callback(null, apiData.swarmstate);
- return;
- }
- }
- callback({
- code:'unknown',
- info:'Malformed API response'
- });
- });
- },
-
- /**
- * Get a browserstack worker id based on a browserMap object
- * @param browser Object: Object with property 'name' and 'version' (from testswarm-browserstack.map)
- * @param currentWorkers Array: List of worker objects (from browserstack.getWorkers)
- * @return Array: Worker ids (can be empty if none found)
- */
- getWorkersByBrowser:function (browser, currentWorkers) {
- var i, worker,
- workers = [],
- len = currentWorkers.length;
- for (i = 0; i < len; i++) {
- worker = currentWorkers[i];
- if (worker.browser.name === browser.name && worker.browser.version === browser.version) {
- workers.push(worker);
- }
- }
- return workers;
- },
-
- startWorker:function (browser, clientTimeout) {
- if (self.options().dryRun) {
- console.log('[dryRun] startWorker', browser, clientTimeout);
- return;
- }
- var client = self.client();
- client.createWorker({
- browser:browser.name,
- version:browser.version,
- url:self.options().swarmRunUrl,
- timeout:clientTimeout
- }, function (err, worker) {
- if (err) {
- console.log('error spawning browser:', browser, err);
- } else {
- console.log('started browser: ', browser, worker);
- }
- });
- },
-
- /**
- * @param currentWorkers Array: Array of browser objects
- * @param swarmState Object: Info about current state of the testswarm
- */
- updateBrowsers:function (currentWorkers, swarmState) {
- var browserID, stats, workers, doKillWorkers, i, len,
- // Browsers needed by TestSwarm, includes browsers that have workers already
- neededBrowsers = [],
- // Browsers to be started. Must not be more than options().stackLimit
- // May contain the same browser multiple times, this is expected behavior
- startBrowsers = [],
- // Workers that are no longer needed
- killWorkers = [];
-
- if (self.options().verbose) {
- console.log('TestSwarm statistics:\n', (function () {
- // Reduce big swarmState to just the user agent stats
- var uaID, uaStats = {};
- for (uaID in swarmState.userAgents) {
- uaStats[uaID] = swarmState.userAgents[uaID].stats;
- }
- return uaStats;
- }()));
- console.log('BrowserStack current workers:\n', currentWorkers);
- }
-
- // Figure out which browsers are needed by TestSwarm and which should be killed
- for (browserID in browserMap) {
- if (!swarmState.userAgents[browserID]) {
- continue;
- }
- stats = swarmState.userAgents[browserID].stats;
- workers = self.getWorkersByBrowser(browserMap[browserID], currentWorkers);
- doKillWorkers = false;
- if (stats.pendingRuns > 0) {
- neededBrowsers.push(browserMap[browserID]);
- if (stats.onlineClients === 0 && workers.length && self.options().kill) {
- // There is an active worker but it is not in the swarm. This can
- // happen if the browser crashed. Kill the lost worker.
- doKillWorkers = true;
- }
- } else if (stats.activeRuns === 0 && stats.pendingRuns === 0 && workers.length && self.options().kill) {
- // Kill workers for browsers for which there are no new runs
- // available and also are not actively busy running something.
- // (we don't want to kill a worker that's working on the last run in the list)
- doKillWorkers = true;
- }
-
- if (doKillWorkers) {
- killWorkers.push.apply(killWorkers, workers);
- }
- }
-
- console.log('TestSwarm wants these browsers:', neededBrowsers);
- console.log('BrowserStack workers to be killed:', killWorkers);
-
- // Figure out which of the needed browsers to start.
- // Summary:
- // * If the limit is lower than the number of needed browsers, then some
- // workers won't be started, yet (if we would start them they'd only be
- // stalled in browserstack's queue).
- // * When the number of needed browsers has become less than limit, the loop
- // will be reset one or more times so that more instances of the same browsers
- // are started (e.g. 2 instances of IE6 and IE7).
- i = currentWorkers.length - killWorkers.length;
-
- if (self.options().verbose) {
- console.log('BrowserStack limit:', self.options().stackLimit);
- console.log('BrowserStack # workers (minus workers to be killed):', i);
- }
-
- for (i = i < 0 ? 0 : i, len = neededBrowsers.length; i < len; i++) {
- if (startBrowsers.length === self.options().stackLimit) {
- break;
- }
-
- startBrowsers.push(neededBrowsers[i]);
-
- // Array index starts at zero, -1 is last key.
- // If this is the last key, reset the loop. We want to keep looping
- // until we reach the stack limit.
- if (i === len-1) {
- // -1 instead of 0. Because after this loop, "i++" will run.
- i = -1;
- }
- }
- console.log('BrowserStack workers to be started:', startBrowsers);
-
- killWorkers.forEach(function (worker, i) {
- self.killWorker(worker);
- });
-
- startBrowsers.forEach(function (browser, i) {
- self.startWorker(browser, self.options().clientTimeout);
- });
- },
-
- run:function () {
- var client = self.client();
- async.parallel({
- currentWorkers:function (callback) {
- client.getWorkers(function (err, resp) {
- if (err) {
- console.log('Error getting workers', err);
- }
- callback(err, resp);
- });
- },
- swarmState:function (callback) {
- self.getSwarmState(function (error, state) {
- if (state) {
- callback(null, state);
- } else {
- console.log('Getting testswarm state failed:\n', error);
- // TODO handle err, for now just continue pretending there are no needs
- // by giving it an empty object.
- callback(null, {
- userAgents:{}
- });
- }
- });
- }
- }, function (err, results) {
- self.updateBrowsers(results.currentWorkers, results.swarmState);
- });
- },
-
- /**
- * @param worker Object|Number: Either a worker object (as given by browerstack.getWorkers,
- * used by tsbs.updateBrowsers), or the worker id directly (used by "cli.js --killWorker")
- */
- killWorker:function (worker) {
- if (self.options().dryRun) {
- console.log('[dryRun] killWorker', worker);
- return;
- }
- var client = self.client();
- client.terminateWorker(worker.id || worker, function (err) {
- if (err) {
- console.log('could not kill worker', worker);
- return;
- }
- console.log('killed worker', worker);
- });
- },
-
- killAll:function () {
- var client = self.client();
- client.getWorkers(function (err, workers) {
- if (err) {
- console.log('could not get workers from browserstack');
- return;
- }
- if (!workers || workers.length < 1) {
- console.log('no workers running or queued');
- }
- workers.forEach(function (worker, i) {
- self.killWorker(worker);
- });
- });
- }
-};
-
-self = TestSwarmBrowserStackInteg;
-
-module.exports = {
- options:TestSwarmBrowserStackInteg.options,
- getSwarmState:TestSwarmBrowserStackInteg.getSwarmState,
- run:TestSwarmBrowserStackInteg.run,
- killWorker:TestSwarmBrowserStackInteg.killWorker,
- killAll:TestSwarmBrowserStackInteg.killAll
-};
View
58 package.json
@@ -1,27 +1,35 @@
{
- "name": "testswarm-browserstack",
- "version": "0.1.1",
- "description": "Integration layer between TestSwarm and BrowserStack",
- "keywords": [
- "testswarm",
- "browserstack"
- ],
- "homepage": "https://github.com/clarkbox/testswarm-browserstack",
- "author": "clarkbox",
- "main": "lib/testswarm-browserstack.js",
- "repository": {
- "type": "git",
- "url": "git://github.com/clarkbox/testswarm-browserstack.git"
- },
- "dependencies": {
- "async": "0.1.x",
- "browserstack": "0.0.2",
- "commander": "0.6.x",
- "request": "2.9.x"
- },
- "devDependencies": {},
- "optionalDependencies": {},
- "engines": {
- "node": "*"
- }
+ "name": "testswarm-browserstack",
+ "version": "0.2.0",
+ "description": "Integration layer between TestSwarm and BrowserStack",
+ "keywords": [
+ "testswarm",
+ "browserstack"
+ ],
+ "homepage": "https://github.com/clarkbox/testswarm-browserstack",
+ "author": {
+ "name": "clarkbox",
+ "web": "https://github.com/clarkbox"
+ },
+ "contributors": [{
+ "name": "Timo Tijhof",
+ "web": "https://github.com/Krinkle"
+ }],
+ "main": "src/testswarm-browserstack.js",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/clarkbox/testswarm-browserstack.git"
+ },
+ "dependencies": {
+ "async": "0.1.x",
+ "browserstack": "~0.1.0",
+ "colors": "~0.6.0",
+ "commander": "0.6.x",
+ "request": "2.9.x"
+ },
+ "devDependencies": {},
+ "optionalDependencies": {},
+ "engines": {
+ "node": "*"
+ }
}
View
15 run-sample.sh
@@ -1,11 +1,6 @@
-# This is an example script that could be run by cron on set interval.
+# This is an example script that could be run by cron on a regular interval.
#
-node /path/to/testswarm-browserstack/lib/cli.js\
- --swarmUrl "http://swarm.example.org"\
- --swarmRunUrl "http://swarm.example.org/run/swarmuser/"\
- -u "browserstackUser"\
- -p "browserstackPass"\
- --run\
- --kill\
- --verbose\
- >> /srv/swarm.example.org/log/testswarm-browserstack/run.log 2>&1;
+node /path/to/testswarm-browserstack/src/cli.js\
+ --config "/path/to/testswarm-browserstack/config.json"\
+ --run-loop\
+ >> /var/log/testswarm-browserstack/run.log 2>&1;
View
123 src/cli.js
@@ -0,0 +1,123 @@
+#!/usr/bin/env node
+var fs = require('fs'),
+ path = require('path'),
+ program = require('commander'),
+ spawn = require('child_process').spawn,
+ tsbs = require('./testswarm-browserstack'),
+ cliConfig,
+ config,
+ child;
+
+function loadAndParseConfigFile(filePath) {
+ return (filePath && fs.existsSync(filePath)) ?
+ JSON.parse(fs.readFileSync(path.resolve(filePath), 'utf-8')) :
+ false;
+}
+
+function confContains(conf, props, prefix) {
+ for (var i = 0, len = props.length; i < len; i += 1) {
+ if (!conf || conf[props[i]] === undefined) {
+ console.log('Configuration file invalid or missing required parameter:', (prefix || '') + props[i]);
+ return false;
+ }
+ }
+ return true;
+}
+
+function runLoop() {
+ if (config.verbose) {
+ console.log('Starting run loop...');
+ }
+ child = spawn('node', [
+ __filename,
+ '--run',
+ '--config', program.config,
+ program.verbose ? '--verbose' : '',
+ program.dryRun ? '--dry-run' : ''
+ ]);
+ child.stdout.on('data', function (data) {
+ console.log(
+ '\t' + String(data).replace(/\n/g, '\n\t')
+ );
+ });
+ child.stderr.on('data', function (data) {
+ console.error(
+ '\t' + String(data).replace(/\n/g, '\n\t')
+ );
+ });
+ child.on('exit', function () {
+ console.log('Next iteration in ' + program.runLoop + ' seconds...');
+ setTimeout(runLoop, program.runLoop * 1000);
+ });
+}
+
+program
+ .version('0.2.0')
+ .option('--config [path]', 'path to config file with options (defaults to ./config.json)', './config.json')
+ .option('--run', 'Retrieve TestSwarm state and spawn/terminate BrowserStack workers as needed')
+ .option('--run-loop <timeout>', 'Execute --run in a non-overlapping loop with set timeout (in seconds) between iterations', Number)
+ .option('--worker <id>', 'Get info abuot a specific BrowserStack worker', Number)
+ .option('--spawn <uaId>', 'Spwawn a BrowserStack worker by swarm useragent id (joining the swarm)')
+ .option('--terminate <id>', 'Terminate a specific BrowserStack worker', Number)
+ .option('--terminateAll', 'Terminate all BrowserStack workers')
+ .option('-v, --verbose', 'Output debug information (through console.log)')
+ .option('--dry, --dry-run', 'Simulate spawning and termination of browserstack workers')
+ .parse(process.argv);
+
+if (!program.run && !program.runLoop && !program.worker && !program.spawn && !program.terminate && !program.terminateAll) {
+ console.log(program.helpInformation());
+ return;
+}
+
+// Process configuration
+
+cliConfig = loadAndParseConfigFile(program.config);
+if (!cliConfig) {
+ console.error('Configuration file missing or invalid.\nSpecify the file path in the --config parameter (defaults to ./config.json, use config-sample.json as template).');
+ process.exit(1);
+}
+
+tsbs.extendConfig(cliConfig);
+
+config = tsbs.getConfig();
+
+if (program.verbose) {
+ config.verbose = true;
+}
+
+if (program.dryRun) {
+ config.browserstack.dryRun = true;
+}
+
+
+if (!confContains(config.browserstack, ['user', 'pass'], 'browserstack.')) {
+ process.exit(1);
+}
+
+
+// Execute actions
+
+if (program.worker) {
+ tsbs.getWorker(program.worker);
+}
+
+if (program.terminateAll) {
+ tsbs.terminateAll();
+} else if (program.terminate) {
+ tsbs.terminateWorker(program.terminate);
+}
+
+if (program.spawn) {
+ tsbs.spawnWorkerByUa(program.spawn);
+}
+
+if (program.run || program.runLoop) {
+ if (!confContains(config.testswarm, ['root', 'runUrl'], 'testswarm.')) {
+ process.exit(1);
+ }
+ if (program.runLoop) {
+ runLoop();
+ } else {
+ tsbs.run();
+ }
+}
View
175 src/map.js
@@ -0,0 +1,175 @@
+/**
+ * We need to map the ua IDs that TestSwarm uses to browser descriptors for BrowserStack.
+ *
+ * Sources:
+ * - TestSwarm useragents.ini: https://github.com/jquery/testswarm/blob/master/config/useragents.ini
+ * - BrowserStack API:
+ * https://github.com/browserstack/api
+ * http://api.browserstack.com/1/browsers (requires authentication)
+ */
+var map = {
+ 'Chrome|17': {
+ name: 'chrome',
+ version: '17.0'
+ },
+ 'Chrome|18': {
+ name: 'chrome',
+ version: '18.0'
+ },
+ 'Chrome|19': {
+ name: 'chrome',
+ version: '19.0'
+ },
+ 'Chrome|20': {
+ name: 'chrome',
+ version: '20.0'
+ },
+ 'Chrome|21': {
+ name: 'chrome',
+ version: '21.0'
+ },
+ 'Chrome|22': {
+ name: 'chrome',
+ version: '22.0'
+ },
+ 'Chrome|23': {
+ name: 'chrome',
+ version: '23.0'
+ },
+ 'Firefox|3|0': {
+ name: 'firefox',
+ version: '3.0'
+ },
+ // 'Firefox|3|5': Not in browserstack anymore
+ 'Firefox|3|6': {
+ name: 'firefox',
+ version: '3.6'
+ },
+ 'Firefox|4': {
+ name: 'firefox',
+ version: '4.0'
+ },
+ 'Firefox|5': {
+ name: 'firefox',
+ version: '5.0'
+ },
+ 'Firefox|6': {
+ name: 'firefox',
+ version: '6.0'
+ },
+ 'Firefox|7': {
+ name: 'firefox',
+ version: '7.0'
+ },
+ 'Firefox|8': {
+ name: 'firefox',
+ version: '8.0'
+ },
+ 'Firefox|9': {
+ name: 'firefox',
+ version: '9.0'
+ },
+ 'Firefox|10': {
+ name: 'firefox',
+ version: '10.0'
+ },
+ 'Firefox|11': {
+ name: 'firefox',
+ version: '11.0'
+ },
+ 'Firefox|12': {
+ name: 'firefox',
+ version: '12.0'
+ },
+ 'Firefox|13': {
+ name: 'firefox',
+ version: '13.0'
+ },
+ 'Firefox|14': {
+ name: 'firefox',
+ version: '14.0'
+ },
+ 'Firefox|15': {
+ name: 'firefox',
+ version: '15.0'
+ },
+ 'Firefox|16': {
+ name: 'firefox',
+ version: '16.0'
+ },
+ 'IE|6': {
+ name: 'ie',
+ version: '6.0'
+ },
+ 'IE|7': {
+ name: 'ie',
+ version: '7.0'
+ },
+ 'IE|8': {
+ name: 'ie',
+ version: '8.0'
+ },
+ 'IE|9': {
+ name: 'ie',
+ version: '9.0'
+ },
+ 'IE|10': {
+ name: 'ie',
+ version: '10.0'
+ },
+ 'Opera|11|10': {
+ name: 'opera',
+ version: '11.1'
+ },
+ 'Opera|11|50': {
+ name: 'opera',
+ version: '11.5'
+ },
+ 'Opera|11|60': {
+ name: 'opera',
+ version: '11.6'
+ },
+ 'Opera|12|0': {
+ name: 'opera',
+ version: '12.0'
+ },
+ // 'Opera|12|5': Not yet supported by browscap
+ 'Safari|4': {
+ name: 'safari',
+ version: '4.0'
+ },
+ 'Safari|5|0': {
+ name: 'safari',
+ version: '5.0'
+ },
+ 'Safari|5|1': {
+ name: 'safari',
+ version: '5.1'
+ }
+
+ // 'Safari|6|0': Safari 6 is Mac-only, and the BrowserStack v1 API is Windows-only
+
+ // TODO: Most of the following ua's are supported by BrowserStack.
+ // However we need to switch to BrowserStack API v2 in order for those to
+ // work. the v1 API is desktop-only for compatibility reasons (issue #19)
+
+ // 'Android|1|5': {},
+ // 'Android|1|6': {},
+ // 'Android|2|1': {},
+ // 'Android|2|2': {},
+ // 'Android|2|3': {},
+ // 'Fennec|4': {},
+ // 'iPhone|3|2': {},
+ // 'iPhone|4|3': {},
+ // 'iPhone|5': {},
+ // 'iPad|3|2': {},
+ // 'iPad|4|3': {},
+ // 'iPad|5': {},
+ // 'Opera Mobile': {},
+ // 'Opera Mini|2': {},
+ // 'Palm Web|1': {},
+ // 'Palm Web|2': {},
+ // 'IEMobile|7': {}
+};
+
+module.exports = map;
View
498 src/testswarm-browserstack.js
@@ -0,0 +1,498 @@
+var async = require('async'),
+ browserstack = require('browserstack'),
+ request = require('request'),
+
+ browserMap = require('./map'),
+ util = require('./util'),
+
+ config = {
+ browserstack: {
+ user: undefined,
+ pass: undefined,
+ // Workers auto-terminate after 15 minutes
+ // if we're ready before that we'll terminate them.
+ // TODO: This is currently rather long, but we don't want to cut
+ // it off too soon if tests take long or if there is a backlog
+ // (better to use one worker for 30 minutes then to timeout and start
+ // new ones all the time). It'd be nice BrowserStack had a ping-system
+ // so we can ping to extend the timeout until it hits the max of 30
+ // or until it is terminated by this script when no longer needed.
+ workerTimeout: 900,
+ dryRun: false,
+ totalLimit: 10,
+ eqLimit: 2
+ },
+ testswarm: {
+ root: undefined,
+ runUrl: undefined
+ },
+ verbose: false
+ },
+ self,
+ bsClient,
+ workerToUaId;
+
+require('colors');
+
+
+/**
+ * Terminology:
+ *
+ * - worker
+ * A (virtual) machine instance at BrowserStack that runs a certain
+ * os and browser. Identified by a numerical ID. Every new a new VM
+ * is spawned, it gets a new unique ID (usually higer than the
+ * previous one).
+ *
+ * - browser
+ * Description of a worker. To spawn a worker at BrowserStack, the
+ * BrowserStack client is given a browser descriptor. Based on that
+ * a VM is spawned on-demand.
+ *
+ * - ua
+ * User-Agent identifier (string) as known to TestSwarm.
+ * Is not specific to an actual computer.
+ * ./map.js contains a map between `ua` and `browser`.
+ */
+
+self = {
+ /**
+ * Set configuration variables
+ * @param {Object} options Overrides keys in config with given values.
+ */
+ extendConfig: function (options) {
+ if (options) {
+ util.extendObject(config, options, /* deep = */ true);
+ }
+ },
+
+ /**
+ * Get configuration object (by reference)
+ * @return {Object}
+ */
+ getConfig: function () {
+ return config;
+ },
+
+ /**
+ * Create/get the singleton Client instance for BrowserStack.
+ * return {Client}
+ */
+ getBsClient: function () {
+ // Lazy init
+ if (!bsClient) {
+ bsClient = browserstack.createClient({
+ version: 1,
+ username: config.browserstack.user,
+ password: config.browserstack.pass
+ });
+ }
+ return bsClient;
+ },
+
+ /**
+ * Get the swarmstate of testswarm (number of active clients and pending runs).
+ * @param {Function} callback
+ */
+ getSwarmState: function (callback) {
+ request.get(config.testswarm.root + '/api.php?action=swarmstate', function (err, res, body) {
+ var apiData;
+ if (err) {
+ callback({
+ code: 'node-request',
+ info: err
+ });
+ }
+ apiData = JSON.parse(body);
+ if (apiData) {
+ if (apiData.error) {
+ callback(apiData.error);
+ return;
+ }
+ if (apiData.swarmstate) {
+ callback(null, apiData.swarmstate);
+ return;
+ }
+ }
+ callback({
+ code: 'testswarm-response',
+ info: 'Invalid API response'
+ });
+ });
+ },
+
+ /**
+ * @param {Object} worker From Client.getWorkers.
+ * @return {string|undefined} The uaID or undefined.
+ */
+ getUaIdFromWorker: function (worker) {
+ var key;
+ if (!workerToUaId) {
+ // Lazy-init
+ workerToUaId = util.generateReverseMap(browserMap);
+ }
+
+ key = JSON.stringify(worker.browser);
+
+ return workerToUaId[key];
+ },
+
+ /**
+ * Spawn a new BrowserStack worker.
+ * @param {Object} browser
+ */
+ spawnWorker: function (browser) {
+ if (config.browserstack.dryRun) {
+ console.log('[spawnWorker] Dry run:'.cyan, browser);
+ return;
+ }
+ var client = self.getBsClient();
+ client.createWorker({
+ browser: browser.name,
+ version: browser.version,
+ url: config.testswarm.runUrl,
+ timeout: config.browserstack.workerTimeout
+ }, function (err, worker) {
+ if (err) {
+ console.error('[spawnWorker] Error:'.red + ' Browser', browser, err);
+ return;
+ }
+ console.log('[spawnWorker]'.green, browser, worker);
+ });
+ },
+
+ /**
+ * Spawn a new BrowserStack worker, by uaId.
+ * @param {string} ua
+ */
+ spawnWorkerByUa: function (ua) {
+ if (browserMap[ua]) {
+ self.spawnWorker(browserMap[ua]);
+ } else {
+ console.error('[spawnWorkerByUa] Error:'.red + ' Unknown uaId: ' + ua);
+ }
+ },
+
+ /**
+ * @param {number} worker The worker id.
+ */
+ terminateWorker: function (worker) {
+ if (config.browserstack.dryRun) {
+ console.log('[terminateWorker] Dry run:'.cyan + ' Terminate #' + worker);
+ return;
+ }
+ var client = self.getBsClient();
+ client.terminateWorker(worker, function (err) {
+ if (err) {
+ console.error('[terminateWorker] Error:'.red + ' Worker #' + worker + '\n', err);
+ return;
+ }
+ console.log('[terminateWorker]'.yellow + ' Terminated worker #' + worker);
+ });
+ },
+
+ /**
+ * Main process of the library. This is where we process the swarmstate
+ * and create and terminate workers accordingly.
+ * @param liveWorkers Array: List of workers
+ * @param liveSwarmState Object: Info about current state of the testswarm
+ */
+ updateBrowsers: function (liveWorkers, liveSwarmState) {
+ var
+ uaId,
+ ua,
+ workerId,
+ worker,
+ stats,
+ result,
+
+ /**
+ * @var {Object}
+ * Keyed by ID, contains:
+ * - status (queue, running or terminated)
+ * - browser
+ */
+ percWorkers,
+
+ /**
+ * @var {Object}
+ * Keyed by uaId, contains the numer of matching workers.
+ */
+ workersByUa,
+
+ /**
+ * @var {Object}
+ * Keyed by uaId, contains:
+ * onlineClients, activeRuns, pendingRuns.
+ */
+ percSwarmStats;
+
+ // Because the creation and termination is done asynchronous, and we
+ // want to save http reqests, we are going to make a copy of the live
+ // statuses of browserstack and testswarm. This copy is called the
+ // "perception", which we'll update based on what we expect/predict the
+ // status will be (e.g. we call spawnWorker and then change the status
+ // in percWorkers to 'queued').
+
+ // Task 0: Initialize perception.
+ // Also, simplify our data and make it easier to access.
+ if (config.verbose) {
+ console.log('\n== Task 0 ==\n'.white.bold);
+ }
+
+ percSwarmStats = {};
+ percWorkers = {};
+ workersByUa = {};
+
+ for (ua in liveSwarmState.userAgents) {
+ if (browserMap[ua]) {
+ percSwarmStats[ua] = util.copy(liveSwarmState.userAgents[ua].stats);
+ workersByUa[ua] = 0;
+ }
+ }
+
+ liveWorkers.forEach(function (worker) {
+ percWorkers[worker.id] = {
+ status: worker.status,
+ browser: worker.browser
+ };
+
+ uaId = self.getUaIdFromWorker(worker);
+ if (uaId) {
+ workersByUa[uaId] += 1;
+ }
+ });
+
+ console.log('Summary:', (function () {
+ var ua, summary = {};
+ for (ua in workersByUa) {
+ if (workersByUa[ua]) {
+ summary[ua] = workersByUa[ua];
+ }
+ }
+ return summary;
+ }()));
+ console.log('Live workers:\n', percWorkers);
+ if (config.verbose) {
+ console.log('Live swarm state:\n', percSwarmStats, '\n');
+
+ console.log('\n== Task 1 ==\n'.white.bold);
+ }
+
+ // Task 1: Terminate no longer needed workers
+ // - Workers for browers that have no pending and no active runs
+ // (a job can have 0 pending runs, which means nothing is
+ // waiting for a cliient, but runs that were already distributed may not be
+ // finished yet. Those currently being ran by cleints are the active runs).
+ // - Workers for browsers that have 0 online clients in the swarm.
+ // (These are workers of which the browser likely crashed or had issues
+ // joining the swarm).
+
+ for (workerId in percWorkers) {
+ worker = percWorkers[workerId];
+ uaId = self.getUaIdFromWorker(worker);
+ if (!uaId) {
+ if (config.verbose) {
+ // This worker was either created by a different script or by a another version
+ // of this script with different ua map.
+ console.log('Found worker for which there is no match in UA map', worker);
+ }
+ continue;
+ }
+
+ stats = percSwarmStats[uaId];
+
+ if (stats.pendingRuns === 0 && stats.activeRuns === 0) {
+ if (config.verbose) {
+ console.log('No longer needed worker:', {
+ worker: worker,
+ stats: stats
+ });
+ }
+
+ self.terminateWorker(workerId);
+ worker.status = 'terminated';
+
+ // Update perception
+ if (stats.onlineClients > 0) {
+ stats.onlineClients -= 1;
+ }
+ workersByUa[uaId] -= 1;
+
+ // Don't terminate workers stil in the queue that aren't in the swarm yet
+ // TODO: This could terminate a worker that just got started (so status
+ // isn't 'queue' anymore) but hasn't loaded the browser yet. In the future
+ // with event-based testswarm we'll be able to more closely determine this.
+ } else if (worker.status === 'running' && stats.onlineClients === 0) {
+ if (config.verbose) {
+ console.log('Running worker disconnected from the swarm:', {
+ worker: worker,
+ stats: stats
+ });
+ }
+
+ self.terminateWorker(workerId);
+ worker.status = 'terminated';
+
+ // Update perception
+ workersByUa[uaId] -= 1;
+
+ }
+ }
+
+ // Task 2: Start workers for browsers with pending tests but 0 online clients
+ // and 0 workers (this last bit is important as we don't want to spawncr another
+ // worker here if there is one queued but not in the swarm yet).
+ // Note: This is the only case where we ignore the total limit to use the 'queue'
+ // system of browserstack to start all browsers that are needed without question.
+ // It also bypasses priority because it is more important to get at least 1 of
+ // each online so that they can work asynchronous. And then, in Task 2, we'll
+ // fill in extra workers if possible.
+ if (config.verbose) {
+ console.log('\n== Task 2 ==\n'.white.bold);
+ }
+
+ for (ua in percSwarmStats) {
+ stats = percSwarmStats[ua];
+ if (stats.pendingRuns > 0 && stats.onlineClients === 0 && workersByUa[ua] === 0) {
+ self.spawnWorker(browserMap[ua]);
+
+ // Update perception
+ stats.onlineClients += 1;
+ workersByUa[ua] += 1;
+ }
+ }
+
+ // Task 3: Compute the neediness of browsers and spawncr the most
+ // needed browser. Keep doing so until the available slots are filled.
+ if (config.verbose) {
+ console.log('\n== Task 3 ==\n'.white.bold);
+ }
+
+ function workerTotal() {
+ var ua, total = 0;
+ for (ua in workersByUa) {
+ total += workersByUa[ua];
+ }
+ return total;
+ }
+
+ function getNeediest() {
+ var ua, stats, priority, neediest;
+
+ neediest = {
+ priority: 0,
+ ua: undefined
+ };
+
+ for (ua in percSwarmStats) {
+ stats = percSwarmStats[ua];
+ if (workersByUa[ua] >= config.browserstack.eqLimit) {
+ // We've reached the number of max for this ua.
+ priority = 0;
+ } else if (stats.onlineClients === 0) {
+ // We already dealt with this category. If it is still 0,
+ // it means we can't help this one (see Task 1).
+ // Also, x/0 is NaN or Infinity in javascript, which we don't want.
+ priority = 0;
+ } else {
+ // This is the priority formula.
+ // The more runs and the less clients, the higher the priority.
+ // No runs? Priority becomes 0 (0/anything=0).
+ priority = stats.pendingRuns / stats.onlineClients;
+ }
+ if (priority > neediest.priority) {
+ neediest = {
+ priority: priority,
+ ua: ua
+ };
+ }
+ }
+
+ return neediest;
+ }
+
+
+ console.log('Status... (workers: ' + workerTotal() + ' / limit: ' + config.browserstack.totalLimit + ')');
+ while (workerTotal() < config.browserstack.totalLimit) {
+ result = getNeediest();
+ if (result.priority <= 0) {
+ console.log('Neediness exhausted, done!');
+ break;
+ } else {
+ if (config.verbose) {
+ console.log('Most needed:', result);
+ }
+ self.spawnWorker(browserMap[result.ua]);
+
+ // Update perception
+ percSwarmStats[result.ua].onlineClients += 1;
+ workersByUa[result.ua] += 1;
+ }
+ console.log('Looping... (workers: ' + workerTotal() + ' / limit: ' + config.browserstack.totalLimit + ')');
+ }
+
+ },
+
+ run: function () {
+ async.parallel({
+ currentWorkers: function (callback) {
+ self.getBsClient().getWorkers(function (err, resp) {
+ if (err) {
+ console.error('Failed to get list of workers', err);
+ }
+ callback(err, resp);
+ });
+ },
+ swarmState: function (callback) {
+ self.getSwarmState(function (err, state) {
+ if (state) {
+ callback(null, state);
+ } else {
+ console.error('Failed to get testswarm state', err);
+ // TODO handle err, for now just continue pretending there are no needs
+ // by giving it an empty object.
+ callback(null, {
+ userAgents: {}
+ });
+ }
+ });
+ }
+ }, function (err, results) {
+ self.updateBrowsers(results.currentWorkers, results.swarmState);
+ });
+ },
+
+ terminateAll: function () {
+ var client = self.getBsClient();
+ client.getWorkers(function (err, workers) {
+ if (err) {
+ console.error('Could not get workers from browserstack');
+ return;
+ }
+ if (!workers || workers.length < 1) {
+ console.log('No workers running or queued');
+ }
+ workers.forEach(function (worker) {
+ self.terminateWorker(worker);
+ });
+ });
+ },
+
+ getWorker: function (id) {
+ var client = self.getBsClient();
+ client.getWorker(id, function (err, worker) {
+ if (err) {
+ console.error('Could not get worker info from browserstack');
+ return;
+ }
+ if (!worker) {
+ console.log('No worker info available');
+ }
+ console.log('Worker #' + id + ':\n', worker);
+ });
+ }
+};
+
+
+module.exports = self;
View
67 src/util.js
@@ -0,0 +1,67 @@
+/**
+ * Like typeof === 'object' but more accurate.
+ * (null is not an object, arrays and functions are objects).
+ */
+function isObject(a) {
+ return Object(a) === a;
+}
+
+/**
+ * Extend a plain object with another plain object.
+ */
+function extendObject(target, options, deep) {
+ var prop, option, targetValue;
+ for (prop in options) {
+ option = options[prop];
+ if (deep && isObject(option)) {
+ // If the target is not an object we need to clone it by extending
+ // an empty object. If we would add `if isObject target[prop]` then
+ // we would move original objects, which is very bad.
+ targetValue = isObject(target[prop]) ? target[prop] : {};
+
+ target[prop] = extendObject(targetValue, option);
+ } else {
+ target[prop] = option;
+ }
+ }
+
+ return target;
+}
+
+/**
+ * Recursively clone a plain object or an array.
+ */
+function copy(a) {
+ var b, key, len;
+ if (Array.isArray(a)) {
+ b = [];
+ for (key = 0, len = a.length; key < len; key += 1) {
+ b[key] = isObject(a[key]) ? copy(a[key]) : a[key];
+ }
+
+ } else {
+ b = {};
+ for (key in a) {
+ b[key] = isObject(a[key]) ? copy(a[key]) : a[key];
+ }
+ }
+ return b;
+}
+
+/**
+ * Generate an object keyed by the JSON representation
+ * of the object values with the key as its value.
+ */
+function generateReverseMap(map) {
+ var key, rev = {};
+ for (key in map) {
+ rev[JSON.stringify(map[key])] = key;
+ }
+ return rev;
+}
+
+module.exports = {
+ extendObject: extendObject,
+ copy: copy,
+ generateReverseMap: generateReverseMap
+};

0 comments on commit b626290

Please sign in to comment.