@wdio/cli
Version:
WebdriverIO testrunner command line interface
404 lines (403 loc) • 16.6 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const path_1 = __importDefault(require("path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const async_exit_hook_1 = __importDefault(require("async-exit-hook"));
const logger_1 = __importDefault(require("@wdio/logger"));
const config_1 = require("@wdio/config");
const utils_1 = require("@wdio/utils");
const interface_1 = __importDefault(require("./interface"));
const utils_2 = require("./utils");
const log = logger_1.default('@wdio/cli:launcher');
class Launcher {
constructor(_configFilePath, _args = {}, _isWatchMode = false) {
var _a, _b;
this._configFilePath = _configFilePath;
this._args = _args;
this._isWatchMode = _isWatchMode;
this._exitCode = 0;
this._hasTriggeredExitRoutine = false;
this._schedule = [];
this._rid = [];
this._runnerStarted = 0;
this._runnerFailed = 0;
this.configParser = new config_1.ConfigParser();
/**
* autocompile before parsing configs so we support ES6 features in configs, only if
*/
if (
/**
* the auto compile option is not define in this case we automatically compile
*/
typeof ((_a = _args.autoCompileOpts) === null || _a === void 0 ? void 0 : _a.autoCompile) === 'undefined' ||
/**
* or it was define and its value is not false
*/
((_b = _args.autoCompileOpts) === null || _b === void 0 ? void 0 : _b.autoCompile) !== 'false') {
this.configParser.autoCompile();
}
this.configParser.addConfigFile(_configFilePath);
this.configParser.merge(_args);
const config = this.configParser.getConfig();
/**
* assign parsed autocompile options into args so it can be used within the worker
* without having to read the config again
*/
this._args.autoCompileOpts = config.autoCompileOpts;
const capabilities = this.configParser.getCapabilities();
this.isMultiremote = !Array.isArray(capabilities);
if (config.outputDir) {
fs_extra_1.default.ensureDirSync(path_1.default.join(config.outputDir));
process.env.WDIO_LOG_PATH = path_1.default.join(config.outputDir, 'wdio.log');
}
logger_1.default.setLogLevelsConfig(config.logLevels, config.logLevel);
const totalWorkerCnt = Array.isArray(capabilities)
? capabilities
.map((c) => this.configParser.getSpecs(c.specs, c.exclude).length)
.reduce((a, b) => a + b, 0)
: 1;
const Runner = utils_1.initialisePlugin(config.runner, 'runner').default;
this.runner = new Runner(_configFilePath, config);
this.interface = new interface_1.default(config, totalWorkerCnt, this._isWatchMode);
config.runnerEnv.FORCE_COLOR = Number(this.interface.hasAnsiSupport);
}
/**
* run sequence
* @return {Promise} that only gets resolves with either an exitCode or an error
*/
async run() {
/**
* catches ctrl+c event
*/
async_exit_hook_1.default(this.exitHandler.bind(this));
let exitCode = 0;
let error = undefined;
try {
const config = this.configParser.getConfig();
const caps = this.configParser.getCapabilities();
const { ignoredWorkerServices, launcherServices } = utils_1.initialiseLauncherService(config, caps);
this._launcher = launcherServices;
this._args.ignoredWorkerServices = ignoredWorkerServices;
/**
* run pre test tasks for runner plugins
* (e.g. deploy Lambda function to AWS)
*/
await this.runner.initialise();
/**
* run onPrepare hook
*/
log.info('Run onPrepare hook');
await utils_2.runLauncherHook(config.onPrepare, config, caps);
await utils_2.runServiceHook(this._launcher, 'onPrepare', config, caps);
exitCode = await this.runMode(config, caps);
/**
* run onComplete hook
* even if it fails we still want to see result and end logger stream
*/
log.info('Run onComplete hook');
await utils_2.runServiceHook(this._launcher, 'onComplete', exitCode, config, caps);
const onCompleteResults = await utils_2.runOnCompleteHook(config.onComplete, config, caps, exitCode, this.interface.result);
// if any of the onComplete hooks failed, update the exit code
exitCode = onCompleteResults.includes(1) ? 1 : exitCode;
await logger_1.default.waitForBuffer();
this.interface.finalise();
}
catch (err) {
error = err;
}
finally {
if (!this._hasTriggeredExitRoutine) {
this._hasTriggeredExitRoutine = true;
await this.runner.shutdown();
}
}
if (error) {
throw error;
}
return exitCode;
}
/**
* run without triggering onPrepare/onComplete hooks
*/
runMode(config, caps) {
/**
* fail if no caps were found
*/
if (!caps) {
return new Promise((resolve) => {
log.error('Missing capabilities, exiting with failure');
return resolve(1);
});
}
/**
* avoid retries in watch mode
*/
const specFileRetries = this._isWatchMode ? 0 : config.specFileRetries;
/**
* schedule test runs
*/
let cid = 0;
if (this.isMultiremote) {
/**
* Multiremote mode
*/
this._schedule.push({
cid: cid++,
caps: caps,
specs: this.configParser.getSpecs(caps.specs, caps.exclude).map(s => ({ files: [s], retries: specFileRetries })),
availableInstances: config.maxInstances || 1,
runningInstances: 0
});
}
else {
/**
* Regular mode
*/
for (let capabilities of caps) {
this._schedule.push({
cid: cid++,
caps: capabilities,
specs: this.configParser.getSpecs(capabilities.specs, capabilities.exclude).map(s => ({ files: [s], retries: specFileRetries })),
availableInstances: capabilities.maxInstances || config.maxInstancesPerCapability,
runningInstances: 0
});
}
}
return new Promise((resolve) => {
this._resolve = resolve;
/**
* fail if no specs were found or specified
*/
if (Object.values(this._schedule).reduce((specCnt, schedule) => specCnt + schedule.specs.length, 0) === 0) {
log.error('No specs found to run, exiting with failure');
return resolve(1);
}
/**
* return immediately if no spec was run
*/
if (this.runSpecs()) {
resolve(0);
}
});
}
/**
* run multiple single remote tests
* @return {Boolean} true if all specs have been run and all instances have finished
*/
runSpecs() {
let config = this.configParser.getConfig();
/**
* stop spawning new processes when CTRL+C was triggered
*/
if (this._hasTriggeredExitRoutine) {
return true;
}
while (this.getNumberOfRunningInstances() < config.maxInstances) {
let schedulableCaps = this._schedule
/**
* bail if number of errors exceeds allowed
*/
.filter(() => {
const filter = typeof config.bail !== 'number' || config.bail < 1 ||
config.bail > this._runnerFailed;
/**
* clear number of specs when filter is false
*/
if (!filter) {
this._schedule.forEach((t) => { t.specs = []; });
}
return filter;
})
/**
* make sure complete number of running instances is not higher than general maxInstances number
*/
.filter(() => this.getNumberOfRunningInstances() < config.maxInstances)
/**
* make sure the capability has available capacities
*/
.filter((a) => a.availableInstances > 0)
/**
* make sure capability has still caps to run
*/
.filter((a) => a.specs.length > 0)
/**
* make sure we are running caps with less running instances first
*/
.sort((a, b) => a.runningInstances - b.runningInstances);
/**
* continue if no capability were schedulable
*/
if (schedulableCaps.length === 0) {
break;
}
let specs = schedulableCaps[0].specs.shift();
this.startInstance(specs.files, schedulableCaps[0].caps, schedulableCaps[0].cid, specs.rid, specs.retries);
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
*/
getNumberOfRunningInstances() {
return this._schedule.map((a) => a.runningInstances).reduce((a, b) => a + b);
}
/**
* get number of total specs left to complete whole suites
* @return {number} specs left to complete suite
*/
getNumberOfSpecsLeft() {
return this._schedule.map((a) => a.specs.length).reduce((a, b) => a + b);
}
/**
* Start instance in a child process.
* @param {Array} specs Specs to run
* @param {Number} cid Capabilities ID
* @param {String} rid Runner ID override
* @param {Number} retries Number of retries remaining
*/
async startInstance(specs, caps, cid, rid, retries) {
let config = this.configParser.getConfig();
// wait before retrying the spec file
if (typeof config.specFileRetriesDelay === 'number' && config.specFileRetries > 0 && config.specFileRetries !== retries) {
await utils_1.sleep(config.specFileRetriesDelay * 1000);
}
// Retried tests receive the cid of the failing test as rid
// so they can run with the same cid of the failing test.
const runnerId = rid || this.getRunnerId(cid);
let processNumber = this._runnerStarted + 1;
// process.debugPort defaults to 5858 and is set even when process
// is not being debugged.
let debugArgs = [];
let debugType;
let debugHost = '';
let debugPort = process.debugPort;
for (let i in process.execArgv) {
const debugArgs = process.execArgv[i].match('--(debug|inspect)(?:-brk)?(?:=(.*):)?');
if (debugArgs) {
let [, type, host] = debugArgs;
if (type) {
debugType = type;
}
if (host) {
debugHost = `${host}:`;
}
}
}
if (debugType) {
debugArgs.push(`--${debugType}=${debugHost}${(debugPort + processNumber)}`);
}
// if you would like to add --debug-brk, use a different port, etc...
let capExecArgs = [...(config.execArgv || [])];
// The default value for child.fork execArgs is process.execArgs,
// so continue to use this unless another value is specified in config.
let defaultArgs = (capExecArgs.length) ? process.execArgv : [];
// If an arg appears multiple times the last occurrence is used
let execArgv = [...defaultArgs, ...debugArgs, ...capExecArgs];
// bump up worker count
this._runnerStarted++;
// run worker hook to allow modify runtime and capabilities of a specific worker
log.info('Run onWorkerStart hook');
await utils_2.runLauncherHook(config.onWorkerStart, runnerId, caps, specs, this._args, execArgv);
await utils_2.runServiceHook(this._launcher, 'onWorkerStart', runnerId, caps, specs, this._args, execArgv);
// prefer launcher settings in capabilities over general launcher
const worker = this.runner.run({
cid: runnerId,
command: 'run',
configFile: this._configFilePath,
args: { ...this._args, ...((config === null || config === void 0 ? void 0 : config.autoCompileOpts) ? { autoCompileOpts: config.autoCompileOpts } : {}) },
caps,
specs,
execArgv,
retries
});
worker.on('message', this.interface.onMessage.bind(this.interface));
worker.on('error', this.interface.onMessage.bind(this.interface));
worker.on('exit', this.endHandler.bind(this));
}
/**
* 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 ...)
*/
getRunnerId(cid) {
if (!this._rid[cid]) {
this._rid[cid] = 0;
}
return `${cid}-${this._rid[cid]++}`;
}
/**
* Close test runner process once all child processes have exited
* @param {Number} cid Capabilities ID
* @param {Number} exitCode exit code of child process
* @param {Array} specs Specs that were run
* @param {Number} retries Number or retries remaining
*/
endHandler({ cid: rid, exitCode, specs, retries }) {
const passed = this._isWatchModeHalted() || exitCode === 0;
if (!passed && retries > 0) {
// Default is true, so test for false explicitly
const requeue = this.configParser.getConfig().specFileRetriesDeferred !== false ? 'push' : 'unshift';
this._schedule[parseInt(rid, 10)].specs[requeue]({ files: specs, retries: retries - 1, rid });
}
else {
this._exitCode = this._isWatchModeHalted() ? 0 : this._exitCode || exitCode;
this._runnerFailed += !passed ? 1 : 0;
}
/**
* avoid emitting job:end if watch mode has been stopped by user
*/
if (!this._isWatchModeHalted()) {
this.interface.emit('job:end', { cid: rid, passed, retries });
}
/**
* Update schedule now this process has ended
*/
// get cid (capability id) from rid (runner id)
const cid = parseInt(rid, 10);
this._schedule[cid].availableInstances++;
this._schedule[cid].runningInstances--;
/**
* do nothing if
* - there are specs to be executed
* - we are running watch mode
*/
const shouldRunSpecs = this.runSpecs();
if (!shouldRunSpecs || (this._isWatchMode && !this._hasTriggeredExitRoutine)) {
return;
}
if (this._resolve) {
this._resolve(passed ? this._exitCode : 1);
}
}
/**
* We need exitHandler to catch SIGINT / SIGTERM events.
* 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
*/
exitHandler(callback) {
if (!callback) {
return;
}
if (this._hasTriggeredExitRoutine) {
return callback();
}
this._hasTriggeredExitRoutine = true;
this.interface.sigintTrigger();
return this.runner.shutdown().then(callback);
}
/**
* returns true if user stopped watch mode, ex with ctrl+c
* @returns {boolean}
*/
_isWatchModeHalted() {
return this._isWatchMode && this._hasTriggeredExitRoutine;
}
}
exports.default = Launcher;