import logger from '@wdio/logger'
import { filterPackageName, getLauncher, runServiceHook, getRunnerName } from '../src/utils'
jest.mock('@wdio/config', () => {
class LauncherMock {
constructor () {
this.onPrepare = jest.fn()
this.onComplete = jest.fn()
}
}
return {
initialisePlugin: jest.fn()
.mockImplementationOnce(
() => LauncherMock)
.mockImplementationOnce(
() => undefined)
.mockImplementationOnce(
() => { throw new Error('Couldn\'t find plugin') })
.mockImplementationOnce(() => { throw new Error('buhh') })
}
})
test('filterPackageName', () => {
const reporter = [
' dot - https://www.npmjs.com/package/@wdio/dot-reporter',
' spec - https://www.npmjs.com/package/@wdio/spec-reporter',
' junit - https://www.npmjs.com/package/@wdio/junit-reporter',
' random - https://www.npmjs.com/package/wdio-random-reporter'
]
expect(filterPackageName('reporter')(reporter)).toEqual([
'@wdio/dot-reporter',
'@wdio/spec-reporter',
'@wdio/junit-reporter',
'wdio-random-reporter'
])
})
test('getLauncher', () => {
expect(getLauncher({})).toEqual([])
const inlineService = {
onPrepare: jest.fn(),
beforeTest: jest.fn()
}
expect(getLauncher({
services: [
inlineService,
'unscoped',
'scoped',
'non-existing'
]
})).toHaveLength(2)
})
test('getLauncher not failing on syntax error', () => {
expect(() => getLauncher({
services: ['other-unscoped']
})).toHaveLength(0)
expect(logger().error).toBeCalledTimes(1)
})
test('getLauncher sets correct service scope', () => {
const hookSuccess = jest.fn()
const inlineService = {
onPrepare() {
this._otherMethod()
},
_otherMethod: hookSuccess
}
const launcher = getLauncher({
services: [
inlineService
]
})
runServiceHook(launcher, 'onPrepare', 1, true, 'abc')
expect(hookSuccess).toBeCalledTimes(1)
})
test('runServiceHook', () => {
const hookSuccess = jest.fn()
const hookFailing = jest.fn().mockImplementation(() => { throw new Error('buhh') })
runServiceHook([
{ onPrepare: hookSuccess },
{ onPrepare: 'foobar' },
{ onPrepare: hookFailing },
{ onComplete: hookSuccess }
], 'onPrepare', 1, true, 'abc')
expect(hookSuccess).toBeCalledTimes(1)
expect(hookFailing).toBeCalledTimes(1)
})
test('getRunnerName', () => {
expect(getRunnerName({ browserName: 'foobar' })).toBe('foobar')
expect(getRunnerName({ appPackage: 'foobar' })).toBe('foobar')
expect(getRunnerName({ appWaitActivity: 'foobar' })).toBe('foobar')
expect(getRunnerName({ app: 'foobar' })).toBe('foobar')
expect(getRunnerName({ platformName: 'foobar' })).toBe('foobar')
expect(getRunnerName({})).toBe('undefined')
})
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
var _toConsumableArray2 = require('babel-runtime/helpers/toConsumableArray');
var _toConsumableArray3 = _interopRequireDefault(_toConsumableArray2);
var _regenerator = require('babel-runtime/regenerator');
var _regenerator2 = _interopRequireDefault(_regenerator);
var _promise = require('babel-runtime/core-js/promise');
var _promise2 = _interopRequireDefault(_promise);
var _asyncToGenerator2 = require('babel-runtime/helpers/asyncToGenerator');
var _asyncToGenerator3 = _interopRequireDefault(_asyncToGenerator2);
var _assign = require('babel-runtime/core-js/object/assign');
var _assign2 = _interopRequireDefault(_assign);
var _keys = require('babel-runtime/core-js/object/keys');
var _keys2 = _interopRequireDefault(_keys);
var _getIterator2 = require('babel-runtime/core-js/get-iterator');
var _getIterator3 = _interopRequireDefault(_getIterator2);
var _classCallCheck2 = require('babel-runtime/helpers/classCallCheck');
var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);
var _createClass2 = require('babel-runtime/helpers/createClass');
var _createClass3 = _interopRequireDefault(_createClass2);
var _path = require('path');
var _path2 = _interopRequireDefault(_path);
var _child_process = require('child_process');
var _child_process2 = _interopRequireDefault(_child_process);
var _ConfigParser = require('./utils/ConfigParser');
var _ConfigParser2 = _interopRequireDefault(_ConfigParser);
var _BaseReporter = require('./utils/BaseReporter');
var _BaseReporter2 = _interopRequireDefault(_BaseReporter);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
var Launcher = function () {
function Launcher(configFile, argv) {
(0, _classCallCheck3.default)(this, Launcher);
this.configParser = new _ConfigParser2.default();
this.configParser.addConfigFile(configFile);
this.configParser.merge(argv);
this.reporters = this.initReporters();
this.argv = argv;
this.configFile = configFile;
this.exitCode = 0;
this.hasTriggeredExitRoutine = false;
this.hasStartedAnyProcess = false;
this.processes = [];
this.schedule = [];
this.rid = [];
this.processesStarted = 0;
this.runnerFailed = 0;
}
/**
* check if multiremote or wdio test
*/
(0, _createClass3.default)(Launcher, [{
key: 'isMultiremote',
value: function isMultiremote() {
var caps = this.configParser.getCapabilities();
return !Array.isArray(caps);
}
/**
* initialise reporters
*/
}, {
key: 'initReporters',
value: function initReporters() {
var reporter = new _BaseReporter2.default();
var config = this.configParser.getConfig();
/**
* if no reporter is set or config property is in a wrong format
* just use the dot reporter
*/
if (!config.reporters || !Array.isArray(config.reporters) || !config.reporters.length) {
config.reporters = ['dot'];
}
var reporters = {};
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = (0, _getIterator3.default)(config.reporters), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var reporterName = _step.value;
var Reporter = void 0;
if (typeof reporterName === 'function') {
Reporter = reporterName;
if (!Reporter.reporterName) {
throw new Error('Custom reporters must export a unique \'reporterName\' property');
}
reporters[Reporter.reporterName] = Reporter;
} else if (typeof reporterName === 'string') {
try {
Reporter = require('wdio-' + reporterName + '-reporter');
} catch (e) {
throw new Error('reporter "wdio-' + reporterName + '-reporter" is not installed. Error: ' + e.stack);
}
reporters[reporterName] = Reporter;
}
if (!Reporter) {
throw new Error('config.reporters must be an array of strings or functions, but got \'' + typeof reporterName + '\': ' + reporterName);
}
}
/**
* if no reporter options are set or property is in a wrong format default to
* empty object
*/
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
if (!config.reporterOptions || typeof config.reporterOptions !== 'object') {
config.reporterOptions = {};
}
for (var _reporterName in reporters) {
var Reporter = reporters[_reporterName];
var reporterOptions = {};
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = (0, _getIterator3.default)((0, _keys2.default)(config.reporterOptions)), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var option = _step2.value;
if (option === _reporterName && typeof config.reporterOptions[_reporterName] === 'object') {
// Copy over options specifically for this reporter type
reporterOptions = (0, _assign2.default)(reporterOptions, config.reporterOptions[_reporterName]);
} else if (reporters[option]) {
// Don't copy options for other reporters
continue;
} else {
// Copy over generic options
reporterOptions[option] = config.reporterOptions[option];
}
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
reporter.add(new Reporter(reporter, config, reporterOptions));
}
return reporter;
}
/**
* run sequence
* @return {Promise} that only gets resolves with either an exitCode or an error
*/
}, {
key: 'run',
value: function () {
var _ref = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee() {
var _this = this;
var config, caps, launcher, _exitCode, cid, _iteratorNormalCompletion3, _didIteratorError3, _iteratorError3, _iterator3, _step3, capabilities, exitCode;
return _regenerator2.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
config = this.configParser.getConfig();
caps = this.configParser.getCapabilities();
launcher = this.getLauncher(config);
this.reporters.handleEvent('start', {
isMultiremote: this.isMultiremote(),
capabilities: caps,
config: config
});
/**
* run onPrepare hook
*/
_context.next = 6;
return config.onPrepare(config, caps);
case 6:
_context.next = 8;
return this.runServiceHook(launcher, 'onPrepare', config, caps);
case 8:
if (!this.isMultiremote()) {
_context.next = 17;
break;
}
_context.next = 11;
return new _promise2.default(function (resolve) {
_this.resolve = resolve;
_this.startInstance(_this.configParser.getSpecs(), caps, 0);
});
case 11:
_exitCode = _context.sent;
_context.next = 14;
return this.runServiceHook(launcher, 'onComplete', _exitCode);
case 14:
_context.next = 16;
return config.onComplete(_exitCode);
case 16:
return _context.abrupt('return', _exitCode);
case 17:
/**
* schedule test runs
*/
cid = 0;
_iteratorNormalCompletion3 = true;
_didIteratorError3 = false;
_iteratorError3 = undefined;
_context.prev = 21;
for (_iterator3 = (0, _getIterator3.default)(caps); !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
capabilities = _step3.value;
this.schedule.push({
cid: cid++,
caps: capabilities,
specs: this.configParser.getSpecs(capabilities.specs, capabilities.exclude),
availableInstances: capabilities.maxInstances || config.maxInstancesPerCapability,
runningInstances: 0,
seleniumServer: { host: config.host, port: config.port, protocol: config.protocol }
});
}
/**
* catches ctrl+c event
*/
_context.next = 29;
break;
case 25:
_context.prev = 25;
_context.t0 = _context['catch'](21);
_didIteratorError3 = true;
_iteratorError3 = _context.t0;
case 29:
_context.prev = 29;
_context.prev = 30;
if (!_iteratorNormalCompletion3 && _iterator3.return) {
_iterator3.return();
}
case 32:
_context.prev = 32;
if (!_didIteratorError3) {
_context.next = 35;
break;
}
throw _iteratorError3;
case 35:
return _context.finish(32);
case 36:
return _context.finish(29);
case 37:
process.on('SIGINT', this.exitHandler.bind(this));
/**
* make sure the program will not close instantly
*/
if (process.stdin.isPaused()) {
process.stdin.resume();
}
_context.next = 41;
return new _promise2.default(function (resolve) {
_this.resolve = resolve;
/**
* return immediatelly if no spec was run
*/
if (_this.runSpecs()) {
resolve(0);
}
});
case 41:
exitCode = _context.sent;
_context.next = 44;
return this.runServiceHook(launcher, 'onComplete', exitCode);
case 44:
_context.next = 46;
return config.onComplete(exitCode);
case 46:
return _context.abrupt('return', exitCode);
case 47:
case 'end':
return _context.stop();
}
}
}, _callee, this, [[21, 25, 29, 37], [30,, 32, 36]]);
}));
function run() {
return _ref.apply(this, arguments);
}
return run;
}()
/**
* run service launch sequences
*/
}, {
key: 'runServiceHook',
value: function () {
var _ref2 = (0, _asyncToGenerator3.default)(_regenerator2.default.mark(function _callee2(launcher, hookName) {
for (var _len = arguments.length, args = Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
args[_key - 2] = arguments[_key];
}
return _regenerator2.default.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
_context2.prev = 0;
_context2.next = 3;
return _promise2.default.all(launcher.map(function (service) {
if (typeof service[hookName] === 'function') {
return service[hookName].apply(service, args);
}
}));
case 3:
return _context2.abrupt('return', _context2.sent);
case 6:
_context2.prev = 6;
_context2.t0 = _context2['catch'](0);
console.error('A service failed in the \'' + hookName + '\' hook\n' + _context2.t0.stack + '\n\nContinue...');
case 9:
case 'end':
return _context2.stop();
}
}
}, _callee2, this, [[0, 6]]);
}));
function runServiceHook(_x, _x2) {
return _ref2.apply(this, arguments);
}
return runServiceHook;
}()
/**
* run multiple single remote tests
* @return {Boolean} true if all specs have been run and all instances have finished
*/
}, {
key: 'runSpecs',
value: function runSpecs() {
var _this2 = this;
var config = this.configParser.getConfig();
/**
* stop spawning new processes when CTRL+C was triggered
*/
if (this.hasTriggeredExitRoutine) {
return true;
}
while (this.getNumberOfRunningInstances() < config.maxInstances) {
var schedulableCaps = this.schedule
/**
* bail if number of errors exceeds allowed
*/
.filter(function () {
var filter = typeof config.bail !== 'number' || config.bail < 1 || config.bail > _this2.runnerFailed;
/**
* clear number of specs when filter is false
*/
if (!filter) {
_this2.schedule.forEach(function (t) {
t.specs = [];
});
}
return filter;
})
/**
* make sure complete number of running instances is not higher than general maxInstances number
*/
.filter(function (a) {
return _this2.getNumberOfRunningInstances() < config.maxInstances;
})
/**
* make sure the capabiltiy has available capacities
*/
.filter(function (a) {
return a.availableInstances > 0;
})
/**
* make sure capabiltiy has still caps to run
*/
.filter(function (a) {
return a.specs.length > 0;
})
/**
* make sure we are running caps with less running instances first
*/
.sort(function (a, b) {
return a.runningInstances > b.runningInstances;
});
/**
* continue if no capabiltiy were schedulable
*/
if (schedulableCaps.length === 0) {
break;
}
this.startInstance([schedulableCaps[0].specs.pop()], schedulableCaps[0].caps, schedulableCaps[0].cid, schedulableCaps[0].seleniumServer);
schedulableCaps[0].availableInstances--;
schedulableCaps[0].runningInstances++;
}
return this.getNumberOfRunningInstances() === 0 && this.getNumberOfSpecsLeft() === 0;
}
/**
* gets number of all running instances
* @return {number} number of running instances
*/
}, {
key: 'getNumberOfRunningInstances',
value: function getNumberOfRunningInstances() {
return this.schedule.map(function (a) {
return a.runningInstances;
}).reduce(function (a, b) {
return a + b;
});
}
/**
* get number of total specs left to complete whole suites
* @return {number} specs left to complete suite
*/
}, {
key: 'getNumberOfSpecsLeft',
value: function getNumberOfSpecsLeft() {
return this.schedule.map(function (a) {
return a.specs.length;
}).reduce(function (a, b) {
return a + b;
});
}
/**
* Start instance in a child process.
* @param {Array} specs Specs to run
* @param {Number} cid Capabilities ID
*/
}, {
key: 'startInstance',
value: function startInstance(specs, caps, cid, server) {
var config = this.configParser.getConfig();
var debug = caps.debug || config.debug;
cid = this.getRunnerId(cid);
var processNumber = this.processesStarted + 1;
// process.debugPort defaults to 5858 and is set even when process
// is not being debugged.
var debugArgs = debug ? ['--debug=' + (process.debugPort + processNumber)] : [];
// if you would like to add --debug-brk, use a different port, etc...
var capExecArgs = [].concat((0, _toConsumableArray3.default)(config.execArgv || []), (0, _toConsumableArray3.default)(caps.execArgv || []));
// The default value for child.fork execArgs is process.execArgs,
// so continue to use this unless another value is specified in config.
var defaultArgs = capExecArgs.length ? process.execArgv : [];
// If an arg appears multiple times the last occurence is used
var execArgv = [].concat((0, _toConsumableArray3.default)(defaultArgs), debugArgs, (0, _toConsumableArray3.default)(capExecArgs));
var childProcess = _child_process2.default.fork(_path2.default.join(__dirname, '/runner.js'), process.argv.slice(2), {
cwd: process.cwd(),
execArgv: execArgv
});
this.processes.push(childProcess);
childProcess.on('message', this.messageHandler.bind(this, cid)).on('exit', this.endHandler.bind(this, cid));
childProcess.send({
cid: cid,
command: 'run',
configFile: this.configFile,
argv: this.argv,
caps: caps,
processNumber: processNumber,
specs: specs,
server: server,
isMultiremote: this.isMultiremote()
});
this.processesStarted++;
}
/**
* generates a runner id
* @param {Number} cid capability id (unique identifier for a capability)
* @return {String} runner id (combination of cid and test id e.g. 0a, 0b, 1a, 1b ...)
*/
}, {
key: 'getRunnerId',
value: function getRunnerId(cid) {
if (!this.rid[cid]) {
this.rid[cid] = 0;
}
return cid + '-' + this.rid[cid]++;
}
/**
* emit event from child process to reporter
* @param {String} cid
* @param {Object} m event object
*/
}, {
key: 'messageHandler',
value: function messageHandler(cid, m) {
this.hasStartedAnyProcess = true;
if (!m.cid) {
m.cid = cid;
}
if (m.event === 'runner:error') {
this.reporters.handleEvent('error', m);
}
this.reporters.handleEvent(m.event, m);
}
/**
* Close test runner process once all child processes have exited
* @param {Number} cid Capabilities ID
* @param {Number} childProcessExitCode exit code of child process
*/
}, {
key: 'endHandler',
value: function endHandler(cid, childProcessExitCode) {
this.exitCode = this.exitCode || childProcessExitCode;
this.runnerFailed += this.exitCode !== 0 ? 1 : 0;
// Update schedule now this process has ended
if (!this.isMultiremote()) {
// get cid (capability id) from rid (runner id)
cid = parseInt(cid, 10);
this.schedule[cid].availableInstances++;
this.schedule[cid].runningInstances--;
}
if (!this.isMultiremote() && !this.runSpecs()) {
return;
}
this.reporters.handleEvent('end', {
sigint: this.hasTriggeredExitRoutine,
exitCode: this.exitCode,
isMultiremote: this.isMultiremote(),
capabilities: this.configParser.getCapabilities(),
config: this.configParser.getConfig()
});
if (this.exitCode === 0) {
return this.resolve(this.exitCode);
}
/**
* finish with exit code 1
*/
return this.resolve(1);
}
/**
* Make sure all started selenium sessions get closed properly and prevent
* having dead driver processes. To do so let the runner end its Selenium
* session first before killing
*/
}, {
key: 'exitHandler',
value: function exitHandler() {
if (this.hasTriggeredExitRoutine || !this.hasStartedAnyProcess) {
console.log('\nKilling process, bye!');
// When spawned as a subprocess,
// SIGINT will not be forwarded to childs.
// Thus for the child to exit cleanly, we must force send SIGINT
if (!process.stdin.isTTY) {
this.processes.forEach(function (p) {
return p.kill('SIGINT');
});
}
/**
* finish with exit code 1
*/
return this.resolve(1);
}
// When spawned as a subprocess,
// SIGINT will not be forwarded to childs.
// Thus for the child to exit cleanly, we must force send SIGINT
if (!process.stdin.isTTY) {
this.processes.forEach(function (p) {
return p.kill('SIGINT');
});
}
console.log('\n\nEnd selenium sessions properly ...\n(press ctrl+c again to hard kill the runner)\n');
this.hasTriggeredExitRoutine = true;
}
/**
* loads launch services
*/
}, {
key: 'getLauncher',
value: function getLauncher(config) {
var launchServices = [];
if (!Array.isArray(config.services)) {
return launchServices;
}
var _iteratorNormalCompletion4 = true;
var _didIteratorError4 = false;
var _iteratorError4 = undefined;
try {
for (var _iterator4 = (0, _getIterator3.default)(config.services), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) {
var serviceName = _step4.value;
var service = void 0;
/**
* allow custom services
*/
if (typeof serviceName === 'object') {
launchServices.push(serviceName);
continue;
}
try {
service = require('wdio-' + serviceName + '-service/launcher');
} catch (e) {
if (!e.message.match('Cannot find module \'wdio-' + serviceName + '-service/launcher\'')) {
throw new Error('Couldn\'t initialise launcher from service "' + serviceName + '".\n' + e.stack);
}
}
if (service && typeof service.onPrepare === 'function') {
launchServices.push(service);
}
}
} catch (err) {
_didIteratorError4 = true;
_iteratorError4 = err;
} finally {
try {
if (!_iteratorNormalCompletion4 && _iterator4.return) {
_iterator4.return();
}
} finally {
if (_didIteratorError4) {
throw _iteratorError4;
}
}
}
return launchServices;
}
}]);
return Launcher;
}();
exports.default = Launcher;
module.exports = exports['default'];