Spaces:
Running
Running
; | |
const logSymbols = require('log-symbols'); | |
const debug = require('debug')('mocha:cli:watch'); | |
const path = require('path'); | |
const chokidar = require('chokidar'); | |
const Context = require('../context'); | |
const collectFiles = require('./collect-files'); | |
/** | |
* Exports the `watchRun` function that runs mocha in "watch" mode. | |
* @see module:lib/cli/run-helpers | |
* @module | |
* @private | |
*/ | |
/** | |
* Run Mocha in parallel "watch" mode | |
* @param {Mocha} mocha - Mocha instance | |
* @param {Object} opts - Options | |
* @param {string[]} [opts.watchFiles] - List of paths and patterns to | |
* watch. If not provided all files with an extension included in | |
* `fileCollectionParams.extension` are watched. See first argument of | |
* `chokidar.watch`. | |
* @param {string[]} opts.watchIgnore - List of paths and patterns to | |
* exclude from watching. See `ignored` option of `chokidar`. | |
* @param {FileCollectionOptions} fileCollectParams - Parameters that control test | |
* @private | |
*/ | |
exports.watchParallelRun = ( | |
mocha, | |
{watchFiles, watchIgnore}, | |
fileCollectParams | |
) => { | |
debug('creating parallel watcher'); | |
return createWatcher(mocha, { | |
watchFiles, | |
watchIgnore, | |
beforeRun({mocha}) { | |
// I don't know why we're cloning the root suite. | |
const rootSuite = mocha.suite.clone(); | |
// ensure we aren't leaking event listeners | |
mocha.dispose(); | |
// this `require` is needed because the require cache has been cleared. the dynamic | |
// exports set via the below call to `mocha.ui()` won't work properly if a | |
// test depends on this module (see `required-tokens.spec.js`). | |
const Mocha = require('../mocha'); | |
// ... and now that we've gotten a new module, we need to use it again due | |
// to `mocha.ui()` call | |
const newMocha = new Mocha(mocha.options); | |
// don't know why this is needed | |
newMocha.suite = rootSuite; | |
// nor this | |
newMocha.suite.ctx = new Context(); | |
// reset the list of files | |
newMocha.files = collectFiles(fileCollectParams); | |
// because we've swapped out the root suite (see the `run` inner function | |
// in `createRerunner`), we need to call `mocha.ui()` again to set up the context/globals. | |
newMocha.ui(newMocha.options.ui); | |
// we need to call `newMocha.rootHooks` to set up rootHooks for the new | |
// suite | |
newMocha.rootHooks(newMocha.options.rootHooks); | |
// in parallel mode, the main Mocha process doesn't actually load the | |
// files. this flag prevents `mocha.run()` from autoloading. | |
newMocha.lazyLoadFiles(true); | |
return newMocha; | |
}, | |
fileCollectParams | |
}); | |
}; | |
/** | |
* Run Mocha in "watch" mode | |
* @param {Mocha} mocha - Mocha instance | |
* @param {Object} opts - Options | |
* @param {string[]} [opts.watchFiles] - List of paths and patterns to | |
* watch. If not provided all files with an extension included in | |
* `fileCollectionParams.extension` are watched. See first argument of | |
* `chokidar.watch`. | |
* @param {string[]} opts.watchIgnore - List of paths and patterns to | |
* exclude from watching. See `ignored` option of `chokidar`. | |
* @param {FileCollectionOptions} fileCollectParams - Parameters that control test | |
* file collection. See `lib/cli/collect-files.js`. | |
* @private | |
*/ | |
exports.watchRun = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => { | |
debug('creating serial watcher'); | |
return createWatcher(mocha, { | |
watchFiles, | |
watchIgnore, | |
beforeRun({mocha}) { | |
mocha.unloadFiles(); | |
// I don't know why we're cloning the root suite. | |
const rootSuite = mocha.suite.clone(); | |
// ensure we aren't leaking event listeners | |
mocha.dispose(); | |
// this `require` is needed because the require cache has been cleared. the dynamic | |
// exports set via the below call to `mocha.ui()` won't work properly if a | |
// test depends on this module (see `required-tokens.spec.js`). | |
const Mocha = require('../mocha'); | |
// ... and now that we've gotten a new module, we need to use it again due | |
// to `mocha.ui()` call | |
const newMocha = new Mocha(mocha.options); | |
// don't know why this is needed | |
newMocha.suite = rootSuite; | |
// nor this | |
newMocha.suite.ctx = new Context(); | |
// reset the list of files | |
newMocha.files = collectFiles(fileCollectParams); | |
// because we've swapped out the root suite (see the `run` inner function | |
// in `createRerunner`), we need to call `mocha.ui()` again to set up the context/globals. | |
newMocha.ui(newMocha.options.ui); | |
// we need to call `newMocha.rootHooks` to set up rootHooks for the new | |
// suite | |
newMocha.rootHooks(newMocha.options.rootHooks); | |
return newMocha; | |
}, | |
fileCollectParams | |
}); | |
}; | |
/** | |
* Bootstraps a chokidar watcher. Handles keyboard input & signals | |
* @param {Mocha} mocha - Mocha instance | |
* @param {Object} opts | |
* @param {BeforeWatchRun} [opts.beforeRun] - Function to call before | |
* `mocha.run()` | |
* @param {string[]} [opts.watchFiles] - List of paths and patterns to watch. If | |
* not provided all files with an extension included in | |
* `fileCollectionParams.extension` are watched. See first argument of | |
* `chokidar.watch`. | |
* @param {string[]} [opts.watchIgnore] - List of paths and patterns to exclude | |
* from watching. See `ignored` option of `chokidar`. | |
* @param {FileCollectionOptions} opts.fileCollectParams - List of extensions to watch if `opts.watchFiles` is not given. | |
* @returns {FSWatcher} | |
* @ignore | |
* @private | |
*/ | |
const createWatcher = ( | |
mocha, | |
{watchFiles, watchIgnore, beforeRun, fileCollectParams} | |
) => { | |
if (!watchFiles) { | |
watchFiles = fileCollectParams.extension.map(ext => `**/*.${ext}`); | |
} | |
debug('ignoring files matching: %s', watchIgnore); | |
let globalFixtureContext; | |
// we handle global fixtures manually | |
mocha.enableGlobalSetup(false).enableGlobalTeardown(false); | |
const watcher = chokidar.watch(watchFiles, { | |
ignored: watchIgnore, | |
ignoreInitial: true | |
}); | |
const rerunner = createRerunner(mocha, watcher, { | |
beforeRun | |
}); | |
watcher.on('ready', async () => { | |
if (!globalFixtureContext) { | |
debug('triggering global setup'); | |
globalFixtureContext = await mocha.runGlobalSetup(); | |
} | |
rerunner.run(); | |
}); | |
watcher.on('all', () => { | |
rerunner.scheduleRun(); | |
}); | |
hideCursor(); | |
process.on('exit', () => { | |
showCursor(); | |
}); | |
// this is for testing. | |
// win32 cannot gracefully shutdown via a signal from a parent | |
// process; a `SIGINT` from a parent will cause the process | |
// to immediately exit. during normal course of operation, a user | |
// will type Ctrl-C and the listener will be invoked, but this | |
// is not possible in automated testing. | |
// there may be another way to solve this, but it too will be a hack. | |
// for our watch tests on win32 we must _fork_ mocha with an IPC channel | |
if (process.connected) { | |
process.on('message', msg => { | |
if (msg === 'SIGINT') { | |
process.emit('SIGINT'); | |
} | |
}); | |
} | |
let exiting = false; | |
process.on('SIGINT', async () => { | |
showCursor(); | |
console.error(`${logSymbols.warning} [mocha] cleaning up, please wait...`); | |
if (!exiting) { | |
exiting = true; | |
if (mocha.hasGlobalTeardownFixtures()) { | |
debug('running global teardown'); | |
try { | |
await mocha.runGlobalTeardown(globalFixtureContext); | |
} catch (err) { | |
console.error(err); | |
} | |
} | |
process.exit(130); | |
} | |
}); | |
// Keyboard shortcut for restarting when "rs\n" is typed (ala Nodemon) | |
process.stdin.resume(); | |
process.stdin.setEncoding('utf8'); | |
process.stdin.on('data', data => { | |
const str = data | |
.toString() | |
.trim() | |
.toLowerCase(); | |
if (str === 'rs') rerunner.scheduleRun(); | |
}); | |
return watcher; | |
}; | |
/** | |
* Create an object that allows you to rerun tests on the mocha instance. | |
* | |
* @param {Mocha} mocha - Mocha instance | |
* @param {FSWatcher} watcher - chokidar `FSWatcher` instance | |
* @param {Object} [opts] - Options! | |
* @param {BeforeWatchRun} [opts.beforeRun] - Function to call before `mocha.run()` | |
* @returns {Rerunner} | |
* @ignore | |
* @private | |
*/ | |
const createRerunner = (mocha, watcher, {beforeRun} = {}) => { | |
// Set to a `Runner` when mocha is running. Set to `null` when mocha is not | |
// running. | |
let runner = null; | |
// true if a file has changed during a test run | |
let rerunScheduled = false; | |
const run = () => { | |
try { | |
mocha = beforeRun ? beforeRun({mocha, watcher}) || mocha : mocha; | |
runner = mocha.run(() => { | |
debug('finished watch run'); | |
runner = null; | |
blastCache(watcher); | |
if (rerunScheduled) { | |
rerun(); | |
} else { | |
console.error(`${logSymbols.info} [mocha] waiting for changes...`); | |
} | |
}); | |
} catch (e) { | |
console.error(e.stack); | |
} | |
}; | |
const scheduleRun = () => { | |
if (rerunScheduled) { | |
return; | |
} | |
rerunScheduled = true; | |
if (runner) { | |
runner.abort(); | |
} else { | |
rerun(); | |
} | |
}; | |
const rerun = () => { | |
rerunScheduled = false; | |
eraseLine(); | |
run(); | |
}; | |
return { | |
scheduleRun, | |
run | |
}; | |
}; | |
/** | |
* Return the list of absolute paths watched by a chokidar watcher. | |
* | |
* @param watcher - Instance of a chokidar watcher | |
* @return {string[]} - List of absolute paths | |
* @ignore | |
* @private | |
*/ | |
const getWatchedFiles = watcher => { | |
const watchedDirs = watcher.getWatched(); | |
return Object.keys(watchedDirs).reduce( | |
(acc, dir) => [ | |
...acc, | |
...watchedDirs[dir].map(file => path.join(dir, file)) | |
], | |
[] | |
); | |
}; | |
/** | |
* Hide the cursor. | |
* @ignore | |
* @private | |
*/ | |
const hideCursor = () => { | |
process.stdout.write('\u001b[?25l'); | |
}; | |
/** | |
* Show the cursor. | |
* @ignore | |
* @private | |
*/ | |
const showCursor = () => { | |
process.stdout.write('\u001b[?25h'); | |
}; | |
/** | |
* Erases the line on stdout | |
* @private | |
*/ | |
const eraseLine = () => { | |
process.stdout.write('\u001b[2K'); | |
}; | |
/** | |
* Blast all of the watched files out of `require.cache` | |
* @param {FSWatcher} watcher - chokidar FSWatcher | |
* @ignore | |
* @private | |
*/ | |
const blastCache = watcher => { | |
const files = getWatchedFiles(watcher); | |
files.forEach(file => { | |
delete require.cache[file]; | |
}); | |
debug('deleted %d file(s) from the require cache', files.length); | |
}; | |
/** | |
* Callback to be run before `mocha.run()` is called. | |
* Optionally, it can return a new `Mocha` instance. | |
* @callback BeforeWatchRun | |
* @private | |
* @param {{mocha: Mocha, watcher: FSWatcher}} options | |
* @returns {Mocha} | |
*/ | |
/** | |
* Object containing run control methods | |
* @typedef {Object} Rerunner | |
* @private | |
* @property {Function} run - Calls `mocha.run()` | |
* @property {Function} scheduleRun - Schedules another call to `run` | |
*/ | |