@qunitjs/browserstack-runner
Version:
[Hotfix] run browser tests over BrowserStack
442 lines (372 loc) • 13.8 kB
JavaScript
var Log = require('../lib/logger'),
logger = new Log(global.logLevel || 'info'),
BrowserStack = require('browserstack'),
qs = require('querystring'),
chalk = require('chalk'),
utils = require('../lib/utils'),
Server = require('../lib/server').Server,
Tunnel = require('../lib/local').Tunnel,
tunnel = require('tunnel'),
http = require('http'),
ConfigParser = require('../lib/configParser').ConfigParser,
config,
server,
timeout,
activityTimeout,
ackTimeout,
client,
workers = {},
workerKeys = {},
tunnelingAgent;
function terminateAllWorkers(callback) {
logger.trace('terminateAllWorkers');
var cleanWorker = function(id, key) {
logger.trace('cleanWorker(%s, %s)', id, key);
client.terminateWorker(id, function() {
var worker = workers[key];
if(worker) {
logger.debug('[%s] Terminated', worker.string);
clearTimeout(worker.ackTimeout);
clearTimeout(worker.activityTimeout);
clearTimeout(worker.testActivityTimeout);
delete workers[key];
delete workerKeys[worker.id];
}
if (utils.objectSize(workers) === 0) {
logger.trace('terminateAllWorkers: done');
callback();
}
});
};
if (utils.objectSize(workers) === 0) {
logger.trace('terminateAllWorkers: done');
callback();
} else {
for (var key in workers){
var worker = workers[key];
if (worker.id) {
cleanWorker(worker.id, key);
} else {
delete workers[key];
if (utils.objectSize(workers) === 0) {
logger.trace('terminateAllWorkers: done');
callback();
}
}
}
}
}
function cleanUpAndExit(signal, error, report, callback) {
ConfigParser.finalBrowsers = [];
callback = callback || function() {};
report = report || [];
logger.trace('cleanUpAndExit: signal: %s', signal);
try {
server.close();
} catch (e) {
logger.debug('Server already closed');
}
if (statusPoller) {
statusPoller.stop();
}
try {
process.kill(tunnel.process.pid, 'SIGTERM');
} catch (e) {
logger.debug('Non existent tunnel');
}
if (signal === 'SIGTERM') {
logger.debug('Exiting');
callback(error, report);
} else {
terminateAllWorkers(function() {
logger.debug('Exiting');
callback(error, report);
});
}
}
function getTestBrowserInfo(browserString, path) {
var info = browserString;
if (config.multipleTest) {
info += ', ' + path;
}
logger.trace('getTestBrowserInfo(%s, %s): %s', browserString, path, info);
return info;
}
function buildTestUrl(test_path, worker_key, browser) {
var host;
// https://github.com/browserstack/browserstack-runner/issues/263
if (browser.os.toLowerCase() === 'ios' || browser.os === 'OS X') {
host = 'bs-local.com';
} else {
host = 'localhost';
}
var url = 'http://'+host+':' + config.test_server_port + '/' + test_path;
var browser_string = utils.browserString(browser);
var querystring = qs.stringify({
_worker_key: worker_key,
_browser_string: browser_string
});
url += ((url.indexOf('?') > 0) ? '&' : '?') + querystring;
logger.trace('buildTestUrl:', url);
return url;
}
function launchServer(config, callback) {
logger.trace('launchServer:', config.test_server_port);
logger.debug('Launching server on port:', config.test_server_port);
server = new Server(client, workers, config, callback);
server.listen(parseInt(config.test_server_port, 10));
}
function launchBrowser(browser, path) {
var key = utils.uuid();
var browserString = utils.browserString(browser);
var browserInfo = getTestBrowserInfo(browserString, path);
logger.debug('[%s] Launching', browserInfo);
browser.url = buildTestUrl(path.replace(/\\/g, '/'), key, browser);
if (config.project) {
browser.project = config.project;
}
if (config.build) {
browser.build = config.build;
}
if(config.tunnelIdentifier) {
browser['tunnel_identifier'] = config.tunnelIdentifier;
}
timeout = parseInt(config.timeout);
if (!isNaN(timeout)) {
browser.timeout = timeout;
} else {
timeout = 300;
}
activityTimeout = timeout - 10;
ackTimeout = parseInt(config.ackTimeout) || 60;
logger.trace('[%s] client.createWorker', browserInfo, browser);
client.createWorker(browser, function (err, worker) {
logger.trace('[%s] client.createWorker | response:', browserInfo, worker, err);
if (err || typeof worker !== 'object') {
logger.info('Error from BrowserStack: ', err);
return;
}
worker.config = browser;
worker.string = browserString;
worker.test_path = path;
worker.path_index = 0;
// attach helper methods to manage worker state
attachWorkerHelpers(worker);
workers[key] = worker;
workerKeys[worker.id] = {key: key, marked: false};
});
}
function launchBrowsers(config, browser) {
setTimeout(function () {
logger.trace('launchBrowsers', browser);
if (Array.isArray(config.test_path)){
config.multipleTest = config.test_path.length > 1? true : false;
launchBrowser(browser, config.test_path[0]);
} else {
config.multipleTest = false;
launchBrowser(browser, config.test_path);
}
}, 100);
}
function attachWorkerHelpers(worker) {
// TODO: Consider creating instances of a proper 'Worker' class
worker.buildUrl = function buildUrl(test_path) {
var workerKey = workerKeys[this.id] ? workerKeys[this.id].key : null;
var url = buildTestUrl(test_path || this.test_path, workerKey, this.config);
logger.trace('[%s] worker.buildUrl: %s', this.id, url);
return url;
};
worker.getTestBrowserInfo = function getTestBrowserInfo(test_path) {
var info = this.string;
if (config.multipleTest) {
info += ', ' + (test_path || this.test_path);
}
return info;
};
worker.awaitAck = function awaitAck() {
var self = this;
if (this.ackTimeout) {
logger.trace('[%s] worker.awaitAck: already awaiting ack, or awaited ack once and failed', self.id);
return;
}
logger.trace('[%s] worker.awaitAck: timeout in %d secs', self.id, ackTimeout);
this.ackTimeout = setTimeout(function () {
if (self.isAckd) {
logger.trace('[%s] worker.awaitAck: already ackd', self.id);
return;
}
var url = self.buildUrl();
logger.trace('[%s] worker.awaitAck: client.changeUrl: %s', self.id, url);
client.changeUrl(self.id, { url: url }, function (err, data) {
logger.trace('[%s] worker.awaitAck: client.changeUrl: %s | response:', self.id, url, data, err);
logger.debug('[%s] Sent Request to reload url', self.getTestBrowserInfo());
});
}, ackTimeout * 1000);
logger.debug('[%s] Awaiting ack', this.getTestBrowserInfo());
};
worker.markAckd = function markAckd() {
this.resetAck();
this.isAckd = true;
logger.trace('[%s] worker.markAckd', this.id);
logger.debug('[%s] Received ack', this.getTestBrowserInfo());
};
worker.resetAck = function resetAck() {
logger.trace('[%s] worker.resetAck', this.id);
clearTimeout(this.ackTimeout);
this.ackTimeout = null;
this.isAckd = false;
};
return worker;
}
var statusPoller = {
poller: null,
start: function(callback) {
logger.trace('statusPoller.start');
statusPoller.poller = setInterval(function () {
client.getWorkers(function (err, _workers) {
logger.trace('client.getWorkers | response: worker count: %d', (_workers || []).length, err);
if (!_workers) {
logger.info(chalk.red('Error found: ' + err));
return;
}
_workers.filter(function(currentValue) {
return currentValue.status === 'running' && workerKeys[currentValue.id] && !workerKeys[currentValue.id].marked;
}).forEach(function(_worker) {
var workerData = workerKeys[_worker.id];
var worker = workers[workerData.key];
if (!worker || worker.launched) {
return;
}
if (_worker.status === 'running') {
//clearInterval(statusPoller);
logger.debug('[%s] Launched', worker.getTestBrowserInfo());
worker.launched = true;
workerData.marked = true;
// Await ack from browser-worker
worker.awaitAck();
logger.trace('[%s] worker.activityTimeout: timeout in %d secs', worker.id, activityTimeout);
worker.activityTimeout = setTimeout(function () {
if (!worker.isAckd) {
logger.trace('[%s] worker.activityTimeout', worker.id);
delete workers[workerData.key];
delete workerKeys[worker.id];
config.status += 1;
if (utils.objectSize(workers) === 0) {
var color = config.status > 0 ? 'red' : 'green';
logger.info(chalk[color]('All tests done, failures: %d.'), config.status);
if (config.status > 0) {
config.status = 1;
}
logger.trace('[%s] worker.activityTimeout: all tests done', worker.id, config.status && 'with failures');
var testsFailedError = utils.createTestsFailedError(config);
if(server && server.reports) {
callback(testsFailedError, server.reports);
} else {
callback(testsFailedError, {});
}
}
} else {
logger.trace('[%s] worker.activityTimeout: already ackd', worker.id);
}
}, activityTimeout * 1000);
logger.trace('[%s] worker.testActivityTimeout: timeout in %d secs', worker.id, activityTimeout);
worker.testActivityTimeout = setTimeout(function () {
if (worker.isAckd) {
logger.trace('[%s] worker.testActivityTimeout', worker.id);
delete workers[workerData.key];
delete workerKeys[worker.id];
config.status += 1;
if (utils.objectSize(workers) === 0) {
var color = config.status > 0 ? 'red' : 'green';
logger.info(chalk[color]('All tests done, failures: %d.'), config.status);
if (config.status > 0) {
config.status = 1;
}
logger.trace('[%s] worker.testActivityTimeout: all tests done', worker.id, config.status && 'with failures');
var testsFailedError = utils.createTestsFailedError(config);
if(server && server.reports) {
callback(testsFailedError, server.reports);
} else {
callback(testsFailedError, {});
}
}
} else {
logger.trace('[%s] worker.testActivityTimeout: not ackd', worker.id);
}
}, (activityTimeout * 1000));
}
});
});
}, 2000);
},
stop: function() {
logger.trace('statusPoller.poller');
clearInterval(statusPoller.poller);
}
};
function runTests(config, callback) {
var runTestsCallback = function(error, report) {
ConfigParser.finalBrowsers = [];
callback(error, report);
};
if (config.proxy) {
logger.trace('runTests: with proxy', config.proxy);
tunnelingAgent = tunnel.httpOverHttp({
proxy: config.proxy
});
var oldhttpreq = http.request;
http.request = function (options, reqCallback) {
options.agent = tunnelingAgent;
return oldhttpreq.call(null, options, reqCallback);
};
}
if (config.browsers && config.browsers.length > 0) {
ConfigParser.parse(client, config.browsers, function(browsers){
launchServer(config, runTestsCallback);
logger.trace('runTests: creating tunnel');
tunnel = new Tunnel(config.key, config.test_server_port, config.tunnelIdentifier, config, function (err) {
if(err) {
cleanUpAndExit(null, err, [], callback);
} else {
logger.trace('runTests: created tunnel');
statusPoller.start(runTestsCallback);
var total_runs = config.browsers.length * (Array.isArray(config.test_path) ? config.test_path.length : 1);
logger.info('Launching ' + config.browsers.length + ' worker(s) for ' + total_runs + ' run(s).');
browsers.forEach(function(browser) {
if (browser.browser_version === 'latest') {
logger.debug('[%s] Finding version.', utils.browserString(browser));
logger.trace('runTests: client.getLatest');
client.getLatest(browser, function(err, version) {
logger.trace('runTests: client.getLatest | response:', version, err);
logger.debug('[%s] Version is %s.',
utils.browserString(browser), version);
browser.browser_version = version;
// So that all latest logs come in together
launchBrowsers(config, browser);
});
} else {
launchBrowsers(config, browser);
}
});
}
});
});
} else {
launchServer(config, callback);
}
}
exports.run = function(userConfig, callback) {
callback = callback || function() {};
try {
config = new (require('../lib/config').config)(userConfig);
client = BrowserStack.createClient({
username: config.username,
password: config.key
});
runTests(config, function(error, report) {
cleanUpAndExit('SIGTERM', error, report, callback);
});
} catch (e) {
callback(e);
}
};