From 6d7d736a8c8629d18fc9cda1ae9e4b9f993722bd Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Thu, 21 May 2020 14:36:27 -0700 Subject: [PATCH] reorganize Node.js-specific sources - moved all non-CLI Node.js-specific sources into `lib/nodejs/` - renamed `WorkerPool` to `BufferedWorkerPool` and udpated filename accordingly. This is in anticipation of eventually adding a "streaming" worker pool that would communicate via IPC (e.g., `process.send()` and `process.on("message")`); see https://github.com/josdejong/workerpool/issues/51 Signed-off-by: Christopher Hiller --- .eslintrc.yml | 20 +-- karma.conf.js | 12 +- lib/mocha.js | 4 +- lib/{pool.js => nodejs/buffered-pool.js} | 10 +- lib/{ => nodejs}/buffered-runner.js | 12 +- lib/{ => nodejs}/growl.js | 2 +- lib/nodejs/ipc.js | 128 ++++++++++++++++++ lib/{ => nodejs}/reporters/buffered.js | 4 +- lib/{ => nodejs}/serializer.js | 4 +- lib/{ => nodejs}/worker.js | 6 +- package-scripts.js | 2 +- package.json | 10 +- .../{pool.spec.js => buffered-pool.spec.js} | 53 +++++--- test/node-unit/buffered-runner.spec.js | 12 +- .../reporters/buffered.spec.js | 8 +- test/node-unit/serializer.spec.js | 2 +- test/node-unit/worker.spec.js | 6 +- 17 files changed, 213 insertions(+), 82 deletions(-) rename lib/{pool.js => nodejs/buffered-pool.js} (94%) rename lib/{ => nodejs}/buffered-runner.js (96%) rename lib/{ => nodejs}/growl.js (98%) create mode 100644 lib/nodejs/ipc.js rename lib/{ => nodejs}/reporters/buffered.js (97%) rename lib/{ => nodejs}/serializer.js (99%) rename lib/{ => nodejs}/worker.js (97%) rename test/node-unit/{pool.spec.js => buffered-pool.spec.js} (73%) rename test/{ => node-unit}/reporters/buffered.spec.js (96%) diff --git a/.eslintrc.yml b/.eslintrc.yml index df14a1baa5..e185b97711 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -25,24 +25,18 @@ overrides: env: node: false - files: - - 'scripts/**/*.js' + - '.eleventy.js' + - '.wallaby.js' - 'package-scripts.js' - 'karma.conf.js' - - '.wallaby.js' - - '.eleventy.js' - 'bin/*' + - 'docs/_data/**/*.js' - 'lib/cli/**/*.js' - - 'test/node-unit/**/*.js' - - 'test/integration/options/watch.spec.js' + - 'lib/nodejs/**/*.js' + - 'scripts/**/*.js' - 'test/integration/helpers.js' - - 'lib/growl.js' - - 'lib/buffered-runner.js' - - 'lib/worker.js' - - 'lib/reporters/buffered.js' - - 'lib/serializer.js' - - 'lib/pool.js' - - 'test/reporters/buffered.spec.js' - - 'docs/_data/**/*.js' + - 'test/integration/options/watch.spec.js' + - 'test/node-unit/**/*.js' parserOptions: ecmaVersion: 2018 env: diff --git a/karma.conf.js b/karma.conf.js index 16292c472c..0aec4006a5 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -30,18 +30,14 @@ module.exports = config => { browserify: { debug: true, configure: function configure(b) { - b.ignore('./lib/cli/*.js') - .ignore('chokidar') + b.ignore('chokidar') .ignore('fs') .ignore('glob') - .ignore('./lib/esm-utils.js') .ignore('path') .ignore('supports-color') - .ignore('./lib/buffered-runner.js') - .ignore('./lib/reporters/buffered.js') - .ignore('./lib/serializer.js') - .ignore('./lib/worker.js') - .ignore('./lib/pool.js') + .ignore('./lib/esm-utils.js') + .ignore('./lib/cli/*.js') + .ignore('./lib/nodejs/*.js') .on('bundled', (err, content) => { if (err) { throw err; diff --git a/lib/mocha.js b/lib/mocha.js index 2b5fbb89c6..8c5f5dc18a 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -9,7 +9,7 @@ var escapeRe = require('escape-string-regexp'); var path = require('path'); var builtinReporters = require('./reporters'); -var growl = require('./growl'); +var growl = require('./nodejs/growl'); var utils = require('./utils'); var mocharc = require('./mocharc.json'); var errors = require('./errors'); @@ -175,7 +175,7 @@ function Mocha(options) { this.rootHooks(options.rootHooks); } if (options.parallel && options.jobs > 1) { - this._runner = require('./buffered-runner'); + this._runner = require('./nodejs/buffered-runner'); this.lazyLoadFiles = true; } else { this._runner = exports.Runner; diff --git a/lib/pool.js b/lib/nodejs/buffered-pool.js similarity index 94% rename from lib/pool.js rename to lib/nodejs/buffered-pool.js index bcf77019fb..1535b5dd81 100644 --- a/lib/pool.js +++ b/lib/nodejs/buffered-pool.js @@ -5,7 +5,7 @@ const workerpool = require('workerpool'); const {deserialize} = require('./serializer'); const debug = require('debug')('mocha:parallel:pool'); const {cpus} = require('os'); -const {createInvalidArgumentTypeError} = require('./errors'); +const {createInvalidArgumentTypeError} = require('../errors'); const WORKER_PATH = require.resolve('./worker.js'); @@ -48,7 +48,7 @@ const WORKER_POOL_DEFAULT_OPTS = { /** * A wrapper around a third-party worker pool implementation. */ -class WorkerPool { +class BufferedWorkerPool { constructor(opts = WORKER_POOL_DEFAULT_OPTS) { const maxWorkers = Math.max(1, opts.maxWorkers); @@ -102,7 +102,7 @@ class WorkerPool { 'string' ); } - const serializedOptions = WorkerPool.serializeOptions(options); + const serializedOptions = BufferedWorkerPool.serializeOptions(options); const result = await this._pool.exec('run', [filepath, serializedOptions]); return deserialize(result); } @@ -122,7 +122,7 @@ class WorkerPool { * Instantiates a {@link WorkerPool}. */ static create(...args) { - return new WorkerPool(...args); + return new BufferedWorkerPool(...args); } /** @@ -160,4 +160,4 @@ class WorkerPool { } } -exports.WorkerPool = WorkerPool; +exports.BufferedWorkerPool = BufferedWorkerPool; diff --git a/lib/buffered-runner.js b/lib/nodejs/buffered-runner.js similarity index 96% rename from lib/buffered-runner.js rename to lib/nodejs/buffered-runner.js index a99ec03ed4..f2e436cf03 100644 --- a/lib/buffered-runner.js +++ b/lib/nodejs/buffered-runner.js @@ -1,16 +1,16 @@ 'use strict'; const allSettled = require('promise.allsettled'); -const Runner = require('./runner'); +const Runner = require('../runner'); const {EVENT_RUN_BEGIN, EVENT_RUN_END} = Runner.constants; const debug = require('debug')('mocha:parallel:buffered-runner'); -const {WorkerPool} = require('./pool'); +const {BufferedWorkerPool} = require('./buffered-pool'); const {setInterval, clearInterval} = global; -const {createMap} = require('./utils'); +const {createMap} = require('../utils'); /** * Outputs a debug statement with worker stats - * @param {WorkerPool} pool - Worker pool + * @param {BufferedWorkerPool} pool - Worker pool */ const debugStats = pool => { const {totalWorkers, busyWorkers, idleWorkers, pendingTasks} = pool.stats(); @@ -104,12 +104,12 @@ class BufferedRunner extends Runner { let debugInterval; /** - * @type {WorkerPool} + * @type {BufferedWorkerPool} */ let pool; try { - pool = WorkerPool.create({maxWorkers: options.jobs}); + pool = BufferedWorkerPool.create({maxWorkers: options.jobs}); sigIntListener = async () => { if (this._state !== ABORTING) { diff --git a/lib/growl.js b/lib/nodejs/growl.js similarity index 98% rename from lib/growl.js rename to lib/nodejs/growl.js index 53164563bb..8bb4f46678 100644 --- a/lib/growl.js +++ b/lib/nodejs/growl.js @@ -8,7 +8,7 @@ const os = require('os'); const path = require('path'); const {sync: which} = require('which'); -const {EVENT_RUN_END} = require('./runner').constants; +const {EVENT_RUN_END} = require('../runner').constants; /** * @summary diff --git a/lib/nodejs/ipc.js b/lib/nodejs/ipc.js new file mode 100644 index 0000000000..2cc04c8381 --- /dev/null +++ b/lib/nodejs/ipc.js @@ -0,0 +1,128 @@ +'use strict'; +/** + * @module IPC + */ +/** + * Module dependencies. + */ + +const { + EVENT_SUITE_BEGIN, + EVENT_SUITE_END, + EVENT_TEST_FAIL, + EVENT_TEST_PASS, + EVENT_TEST_PENDING, + EVENT_TEST_BEGIN, + EVENT_TEST_END, + EVENT_TEST_RETRY, + EVENT_DELAY_BEGIN, + EVENT_DELAY_END, + EVENT_HOOK_BEGIN, + EVENT_HOOK_END, + EVENT_RUN_END +} = require('../runner').constants; +const {SerializableEvent, SerializableWorkerResult} = require('../serializer'); +const debug = require('debug')('mocha:reporters:ipc'); +const Base = require('../reporters/base'); + +/** + * List of events to listen to; these will be buffered and sent + * when `Mocha#run` is complete (via {@link IPC#done}). + */ +const EVENT_NAMES = [ + EVENT_SUITE_BEGIN, + EVENT_SUITE_END, + EVENT_TEST_BEGIN, + EVENT_TEST_PENDING, + EVENT_TEST_FAIL, + EVENT_TEST_PASS, + EVENT_TEST_RETRY, + EVENT_TEST_END, + EVENT_HOOK_BEGIN, + EVENT_HOOK_END +]; + +/** + * Like {@link EVENT_NAMES}, except we expect these events to only be emitted + * by the `Runner` once. + */ +const ONCE_EVENT_NAMES = [EVENT_DELAY_BEGIN, EVENT_DELAY_END]; + +/** + * The `IPC` reporter is for use by concurrent runs. Instead of outputting + * to `STDOUT`, etc., it retains a list of events it receives and hands these + * off to the callback passed into {@link Mocha#run}. That callback will then + * return the data to the main process. + */ +class IPC extends Base { + /** + * Listens for {@link Runner} events and retains them in an `events` instance prop. + * @param {Runner} runner + */ + constructor(runner, opts) { + super(runner, opts); + + /** + * Retained list of events emitted from the {@link Runner} instance. + * @type {IPCEvent[]} + * @memberOf IPC + */ + const events = (this.events = []); + + /** + * mapping of event names to listener functions we've created, + * so we can cleanly _remove_ them from the runner once it's completed. + */ + const listeners = new Map(); + + /** + * Creates a listener for event `eventName` and adds it to the `listeners` + * map. This is a defensive measure, so that we don't a) leak memory or b) + * remove _other_ listeners that may not be associated with this reporter. + * @param {string} eventName - Event name + */ + const createListener = eventName => + listeners + .set(eventName, (runnable, err) => { + events.push(SerializableEvent.create(eventName, runnable, err)); + }) + .get(eventName); + + EVENT_NAMES.forEach(evt => { + runner.on(evt, createListener(evt)); + }); + ONCE_EVENT_NAMES.forEach(evt => { + runner.once(evt, createListener(evt)); + }); + + runner.once(EVENT_RUN_END, () => { + debug('received EVENT_RUN_END'); + listeners.forEach((listener, evt) => { + runner.removeListener(evt, listener); + listeners.delete(evt); + }); + }); + } + + /** + * Calls the {@link Mocha#run} callback (`callback`) with the test failure + * count and the array of {@link IPCEvent} objects. Resets the array. + * @param {number} failures - Number of failed tests + * @param {Function} callback - The callback passed to {@link Mocha#run}. + */ + done(failures, callback) { + callback(SerializableWorkerResult.create(this.events, failures)); + this.events = []; // defensive + } +} + +/** + * Serializable event data from a `Runner`. Keys of the `data` property + * beginning with `__` will be converted into a function which returns the value + * upon deserialization. + * @typedef {Object} IPCEvent + * @property {string} name - Event name + * @property {object} data - Event parameters + */ + +module.exports = IPC; diff --git a/lib/reporters/buffered.js b/lib/nodejs/reporters/buffered.js similarity index 97% rename from lib/reporters/buffered.js rename to lib/nodejs/reporters/buffered.js index 1bbca61d36..3623cf0254 100644 --- a/lib/reporters/buffered.js +++ b/lib/nodejs/reporters/buffered.js @@ -20,10 +20,10 @@ const { EVENT_HOOK_BEGIN, EVENT_HOOK_END, EVENT_RUN_END -} = require('../runner').constants; +} = require('../../runner').constants; const {SerializableEvent, SerializableWorkerResult} = require('../serializer'); const debug = require('debug')('mocha:reporters:buffered'); -const Base = require('./base'); +const Base = require('../../reporters/base'); /** * List of events to listen to; these will be buffered and sent diff --git a/lib/serializer.js b/lib/nodejs/serializer.js similarity index 99% rename from lib/serializer.js rename to lib/nodejs/serializer.js index db2166a415..9de7467fb2 100644 --- a/lib/serializer.js +++ b/lib/nodejs/serializer.js @@ -1,7 +1,7 @@ 'use strict'; -const {type} = require('./utils'); -const {createInvalidArgumentTypeError} = require('./errors'); +const {type} = require('../utils'); +const {createInvalidArgumentTypeError} = require('../errors'); const debug = require('debug')('mocha:serializer'); const SERIALIZABLE_RESULT_NAME = 'SerializableWorkerResult'; diff --git a/lib/worker.js b/lib/nodejs/worker.js similarity index 97% rename from lib/worker.js rename to lib/nodejs/worker.js index 835b060491..d50fc835db 100644 --- a/lib/worker.js +++ b/lib/nodejs/worker.js @@ -3,14 +3,14 @@ const { createInvalidArgumentTypeError, createInvalidArgumentValueError -} = require('./errors'); +} = require('../errors'); const workerpool = require('workerpool'); -const Mocha = require('./mocha'); +const Mocha = require('../mocha'); const { handleRequires, validatePlugin, loadRootHooks -} = require('./cli/run-helpers'); +} = require('../cli/run-helpers'); const d = require('debug'); const debug = d.debug(`mocha:parallel:worker:${process.pid}`); const isDebugEnabled = d.enabled(`mocha:parallel:worker:${process.pid}`); diff --git a/package-scripts.js b/package-scripts.js index bc94d8b3bf..a008691e08 100644 --- a/package-scripts.js +++ b/package-scripts.js @@ -34,7 +34,7 @@ function test(testName, mochaParams) { module.exports = { scripts: { build: { - script: `browserify -e browser-entry.js --plugin ./scripts/dedefine --ignore './lib/cli/*.js' --ignore "./lib/esm-utils.js" --ignore 'chokidar' --ignore 'fs' --ignore 'glob' --ignore 'path' --ignore 'supports-color' --ignore './lib/buffered-runner.js' --ignore './lib/serializer.js' --ignore './lib/reporters/buffered.js' --ignore './lib/worker.js' --ignore './lib/pool.js' -o mocha.js`, + script: `browserify -e browser-entry.js --plugin ./scripts/dedefine --ignore './lib/cli/*.js' --ignore "./lib/esm-utils.js" --ignore 'chokidar' --ignore 'fs' --ignore 'glob' --ignore 'path' --ignore 'supports-color' --ignore './lib/nodejs/*.js' -o mocha.js`, description: 'Build browser bundle' }, lint: { diff --git a/package.json b/package.json index 63913eadf1..e611fda542 100644 --- a/package.json +++ b/package.json @@ -164,11 +164,11 @@ "glob": false, "path": false, "supports-color": false, - "./lib/serializer.js": false, - "./lib/reporters/buffered.js": false, - "./lib/buffered-reporter.js": false, - "./lib/worker.js": false, - "./lib/pool.js": false + "./lib/nodejs/serializer.js": false, + "./lib/nodejs/reporters/buffered.js": false, + "./lib/nodejs/buffered-reporter.js": false, + "./lib/nodejs/worker.js": false, + "./lib/nodejs/buffered-pool.js": false }, "prettier": { "singleQuote": true, diff --git a/test/node-unit/pool.spec.js b/test/node-unit/buffered-pool.spec.js similarity index 73% rename from test/node-unit/pool.spec.js rename to test/node-unit/buffered-pool.spec.js index 75b7d8da9c..afd96e6734 100644 --- a/test/node-unit/pool.spec.js +++ b/test/node-unit/buffered-pool.spec.js @@ -3,8 +3,8 @@ const rewiremock = require('rewiremock/node'); const {createSandbox} = require('sinon'); -describe('class WorkerPool', function() { - let WorkerPool; +describe('class BufferedWorkerPool', function() { + let BufferedWorkerPool; let sandbox; let pool; let stats; @@ -26,16 +26,19 @@ describe('class WorkerPool', function() { }; serializeJavascript = sandbox.spy(require('serialize-javascript')); - WorkerPool = rewiremock.proxy(require.resolve('../../lib/pool'), { - workerpool: { - pool: sandbox.stub().returns(pool) - }, - '../../lib/serializer': serializer, - 'serialize-javascript': serializeJavascript - }).WorkerPool; + BufferedWorkerPool = rewiremock.proxy( + require.resolve('../../lib/nodejs/buffered-pool'), + { + workerpool: { + pool: sandbox.stub().returns(pool) + }, + '../../lib/nodejs/serializer': serializer, + 'serialize-javascript': serializeJavascript + } + ).BufferedWorkerPool; // reset cache - WorkerPool.resetOptionsCache(); + BufferedWorkerPool.resetOptionsCache(); }); afterEach(function() { @@ -44,13 +47,17 @@ describe('class WorkerPool', function() { describe('static method', function() { describe('create()', function() { - it('should return a WorkerPool instance', function() { - expect(WorkerPool.create({foo: 'bar'}), 'to be a', WorkerPool); + it('should return a BufferedWorkerPool instance', function() { + expect( + BufferedWorkerPool.create({foo: 'bar'}), + 'to be a', + BufferedWorkerPool + ); }); describe('when passed no arguments', function() { it('should not throw', function() { - expect(WorkerPool.create, 'not to throw'); + expect(BufferedWorkerPool.create, 'not to throw'); }); }); }); @@ -58,28 +65,32 @@ describe('class WorkerPool', function() { describe('serializeOptions()', function() { describe('when passed no arguments', function() { it('should not throw', function() { - expect(WorkerPool.serializeOptions, 'not to throw'); + expect(BufferedWorkerPool.serializeOptions, 'not to throw'); }); }); it('should return a serialized string', function() { - expect(WorkerPool.serializeOptions({foo: 'bar'}), 'to be a', 'string'); + expect( + BufferedWorkerPool.serializeOptions({foo: 'bar'}), + 'to be a', + 'string' + ); }); describe('when called multiple times with the same object', function() { it('should not perform serialization twice', function() { const obj = {foo: 'bar'}; - WorkerPool.serializeOptions(obj); - WorkerPool.serializeOptions(obj); + BufferedWorkerPool.serializeOptions(obj); + BufferedWorkerPool.serializeOptions(obj); expect(serializeJavascript, 'was called once'); }); it('should return the same value', function() { const obj = {foo: 'bar'}; expect( - WorkerPool.serializeOptions(obj), + BufferedWorkerPool.serializeOptions(obj), 'to be', - WorkerPool.serializeOptions(obj) + BufferedWorkerPool.serializeOptions(obj) ); }); }); @@ -88,7 +99,7 @@ describe('class WorkerPool', function() { describe('constructor', function() { it('should apply defaults', function() { - expect(new WorkerPool(), 'to satisfy', { + expect(new BufferedWorkerPool(), 'to satisfy', { options: { workerType: 'process', forkOpts: {execArgv: process.execArgv}, @@ -102,7 +113,7 @@ describe('class WorkerPool', function() { let workerPool; beforeEach(function() { - workerPool = WorkerPool.create(); + workerPool = BufferedWorkerPool.create(); }); describe('stats()', function() { diff --git a/test/node-unit/buffered-runner.spec.js b/test/node-unit/buffered-runner.spec.js index 05371447f5..17272466ef 100644 --- a/test/node-unit/buffered-runner.spec.js +++ b/test/node-unit/buffered-runner.spec.js @@ -8,7 +8,9 @@ const { EVENT_SUITE_BEGIN } = require('../../lib/runner').constants; const rewiremock = require('rewiremock/node'); -const BUFFERED_RUNNER_PATH = require.resolve('../../lib/buffered-runner.js'); +const BUFFERED_RUNNER_PATH = require.resolve( + '../../lib/nodejs/buffered-runner.js' +); const Suite = require('../../lib/suite'); const {createSandbox} = require('sinon'); @@ -16,7 +18,7 @@ describe('buffered-runner', function() { describe('BufferedRunner', function() { let sandbox; let run; - let WorkerPool; + let BufferedWorkerPool; let terminate; let BufferedRunner; let suite; @@ -32,7 +34,7 @@ describe('buffered-runner', function() { // tests will want to further define the behavior of these. run = sandbox.stub(); terminate = sandbox.stub(); - WorkerPool = { + BufferedWorkerPool = { create: sandbox.stub().returns({ run, terminate, @@ -40,8 +42,8 @@ describe('buffered-runner', function() { }) }; BufferedRunner = rewiremock.proxy(BUFFERED_RUNNER_PATH, r => ({ - '../../lib/pool': { - WorkerPool + '../../lib/nodejs/buffered-pool': { + BufferedWorkerPool }, os: { cpus: sandbox.stub().callsFake(() => new Array(cpuCount)) diff --git a/test/reporters/buffered.spec.js b/test/node-unit/reporters/buffered.spec.js similarity index 96% rename from test/reporters/buffered.spec.js rename to test/node-unit/reporters/buffered.spec.js index 981b939fd8..bf7914a126 100644 --- a/test/reporters/buffered.spec.js +++ b/test/node-unit/reporters/buffered.spec.js @@ -17,7 +17,7 @@ const { EVENT_HOOK_BEGIN, EVENT_HOOK_END, EVENT_RUN_END -} = require('../../lib/runner').constants; +} = require('../../../lib/runner').constants; const {EventEmitter} = require('events'); const {createSandbox} = require('sinon'); const rewiremock = require('rewiremock/node'); @@ -31,9 +31,9 @@ describe('Buffered', function() { sandbox = createSandbox(); runner = new EventEmitter(); Buffered = rewiremock.proxy( - require.resolve('../../lib/reporters/buffered.js'), + require.resolve('../../../lib/nodejs/reporters/buffered'), { - '../../lib/serializer': { + '../../../lib/nodejs/serializer': { SerializableEvent: { create: (eventName, runnable, err) => ({ eventName, @@ -50,7 +50,7 @@ describe('Buffered', function() { }) } }, - '../../lib/reporters/base': class MockBase {} + '../../../lib/reporters/base': class MockBase {} } ); }); diff --git a/test/node-unit/serializer.spec.js b/test/node-unit/serializer.spec.js index 437f133a4e..79f0093d11 100644 --- a/test/node-unit/serializer.spec.js +++ b/test/node-unit/serializer.spec.js @@ -6,7 +6,7 @@ const { deserialize, SerializableEvent, SerializableWorkerResult -} = require('../../lib/serializer'); +} = require('../../lib/nodejs/serializer'); describe('serializer', function() { let sandbox; diff --git a/test/node-unit/worker.spec.js b/test/node-unit/worker.spec.js index f84013e1f0..7edf5727ee 100644 --- a/test/node-unit/worker.spec.js +++ b/test/node-unit/worker.spec.js @@ -1,11 +1,11 @@ 'use strict'; const serializeJavascript = require('serialize-javascript'); -const {SerializableWorkerResult} = require('../../lib/serializer'); const rewiremock = require('rewiremock/node'); +const {SerializableWorkerResult} = require('../../lib/nodejs/serializer'); const {createSandbox} = require('sinon'); -const WORKER_PATH = require.resolve('../../lib/worker.js'); +const WORKER_PATH = require.resolve('../../lib/nodejs/worker.js'); describe('worker', function() { let worker; @@ -64,7 +64,7 @@ describe('worker', function() { worker = rewiremock.proxy(WORKER_PATH, { workerpool: stubs.workerpool, '../../lib/mocha': stubs.Mocha, - '../../lib/serializer': stubs.serializer, + '../../lib/nodejs/serializer': stubs.serializer, '../../lib/cli/run-helpers': stubs.runHelpers }); });