UNPKG

@wdio/cli

Version:
404 lines (403 loc) 16.6 kB
"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;