- package.json
binprop definesmochaas bin/mocha.js
- bin/mocha.js uses lib/cli/options.js to parse command line args
- parses v8 options
- conditionally disables test timeout
- does some more stuff {/* todo */}
- If args are provided, calls lib/cli/cli.js in a separate process
- Else calls lib/cli/cli.js in the same process, printing help and exiting
We assume non-help args are provided for this example
cli.js does more parsing and then goes to lib/cli/run.js via yargs().command(commands.run). See also commands.js.
- run.js exports a yargs object with metadata, specifically a
builderandhandlerproperty- This includes middleware calling
run-helpers#handleRequiresfor the--requirearg values
- This includes middleware calling
builderhandles options: mutually exclusive options, deprecated options, etc.handlercreates a new instance of Mocha and callslib/cli/run-helpers.js#runMochawith it- run-helpers.js
runMochareads options and delegates to one of four run functions watchParallelRun,watchRun,parallelRun, andsingleRunare documented in source- We will go to
watchRunfor this example
- We will go to
watch-run.jsexports thewatchRunfunctionwatchRunreturns a call tocreateWatcherwith abeforeRuncallback, which- calls
mocha.unloadFile() - creates
rootSuite = mocha.suite.clone() - calls
mocha.dispose() - creates
Mocha = require("../mocha") - PR 5409: creates
plugins = runHelpers.handleRequires(...) - creates
newMocha = new Mocha(...)
- calls
- watches all files if there was no
watchFilesarg passed in- else use
watchFiles
- else use
createWatchercreatesallowedviacreatePathFilteron the watch globs with the current working directory as the base pathcreatePathFiltercreates aPathPatternobject, which has the following structure:createPathFilterreduces the globs to theirGlob.patternsvia theglobpackage- For each pattern,
createPathFilter:- finds the first glob entry in the pattern (or null if there isn't one)
- adds the non-glob segments/entries to the base path, then adds that to
dir.paths - does the same with
/**/*todir.globs - adds absolute path to either
match.globs(if it found a glob entry) ormatch.paths(otherwise) - ensure
match.globsincludes absolute path +/**/*
createPathFilterreturns tocreateWatchercreateWatchercreatesignoredviacreatePathFilteron the ignore globs with cwd as the base path- See above for details of
createPathFilter
- See above for details of
createWatchercallscreatePathMatcheron the return values of the previouscreatePathFiltercalls with cwdcreatePathMatcherreturns aPathMatcherobject:/** * Object for matching paths to either allow or ignore them. * @private * @typedef {Object} PathMatcher * @property {AllowMatchFunction} allow Checks if the file path matches the allowed patterns. * @property {chokidar.MatchFunction} ignore The chokidar `ignored` match function. */
createPathMatcher().allowchecks a cache, then callsmatchPattern(filePath, allowed.match)if neededmatchPatternshort-circuits totrueif the file is one of the directories (likely very rare)matchPatternskips theisPathInside(is-path-inside) check because its third parammatchParentwas not truthy- This block checks of the
patternentries for one inside thefilePathentry
- This block checks of the
matchPatternthen uses (minimatch) to check if the file matches- Chokidar 3 uses
anymatch, which usespicomatchfor globs. Bothminiandpicoare popular, we could considerpicofor bug 5374 if needed
- Chokidar 3 uses
createPathMatcher().ignorecallsmatchPatternonfilePathcreateWatchercreates awatcherobject- main:
chokidar.watch - PR 5409: if
TEST_MODEuseEventEmitter, else copy main- use
globalSetupBarrierto ensure test watcher doesn't fire too early? (left comment)
- use
chokidar.watchinstantly starts readying the watcher- tracking all files to be matched
- watcher emits
readyevent when its ready to watch for changes
- main:
- creates
rerunner = createRerunner(...)- creates
runcallback- calls
beforeRuncallback - creates
runnerviamocha.run(...)- sets
runnertonull - calls
blastCache(watcher) - calls
rerun()ifrerunScheduled- else logs "waiting for changes"
- sets
- catches errors and logs the stack to console.error
- calls
- creates
scheduleRuncallback- short-circuits if
rerunScheduled - sets
rerunScheduledtotrue - if there is an existing runner, calls
runner.abort()- else calls
rerun()
- else calls
- short-circuits if
- creates
reruncallback- sets
rerunScheduledtofalse - calls
eraseLine() - calls
run()
- sets
- creates
- PR 5409: creates
ready = false(main does not havereadyvalue) - calls
watcher.on("ready", ...)- main:
- if
globalFixtureContextis falsy, sets it to the result of callingmocha.runGlobalSetup() - calls
rerunner.run()
- if
- PR 5409:
- sets
ready = true - if
TEST_MODE, callssendMessage({ received: ["ready" ]})
- sets
- main:
- calls
watcher.on("all", ...)- main:
watcher.on('all', (_event, filePath) => { // only allow file paths that match the allowed patterns if (matcher.allow(filePath)) { rerunner.scheduleRun(); } });
- PR 5409:
- if test mode, call
sendMessageand continue - if matcher does not allow file path, debug and return
- call
doHandle(which might call itself recursively)- if
exiting(based onSIGINTreceived), return - if unlink event and we should handle the event
- (should handle == stat available and file has been modified since we started, or stat not available but watcher is ready)
- 🔑 note that we can now schedule a run before the watcher is ready, this is a key change!
- if globalFixtureContext is created, call
rerunner.scheduleRun- else do nothing
- return
- (should handle == stat available and file has been modified since we started, or stat not available but watcher is ready)
- if not ready and stat not available
- call
fs.stat(async) and if it returns a stat, calldoHandlerecursively with the returnedstat
- call
- if
- if test mode, call
- main:
- PR 5409: calls
sendMessage({ listening: true })ifTEST_MODE - PR 5409: calls
mocha.runGlobalSetup().thencall throughglobalSetupBarrier.thencall through to setglobalFixtureContext- and return
rerunner.run()
- and return
- calls
hideCursor() - calls
process.on("exit", () => { showCursor(); }) createWatcherreturns the watcher object
watcher.on("ready", ...)is called once Chokidar readies the watcher- calls
mocha.runGlobalSetup()(no args provided)- If there is anything to do, lib/mocha.js
runGlobalSetupawaits_runGlobalFixtures(context is empty obj in this case) _runGlobalFixturesloops through each fixture function defined inoptions.globalSetup(ref https://mochajs.org/next/features/global-fixtures/#global-setup-fixtures)runGlobalSetupreturns the updated context object, which the on-ready handler saves to ensure it doesn't callrunGlobalSetuptwice
- If there is anything to do, lib/mocha.js
- calls
rerunner.run()- calls
options.beforeRun({mocha, watcher}) - calls
mocha.run(...)(Mocha.prototype.run) with an anonymous done-callback
- calls
- Top-level calls
require('./interfaces')which includeslib/interfaces/exports.js(seeEVENT_FILE_REQUIRE, {/* todo confirm with debug */}) runcalls_guardRunningStateTransitionto ensure a new test run is validrunsets state torunning- If previous runner exists,
rundisposes it and resets the suite - If there are files and it's not set to lazy load, it eagerly loads files with
loadFiles- This emits the
EVENT_FILE_PRE_REQUIRE,EVENT_FILE_REQUIRE, andEVENT_FILE_POST_REQUIREevents EVENT_FILE_REQUIREhas a listener defined in lib/interfaces/exports.js to populate the Suite and Test objects viavisit
- This emits the
runcalls_runnerClass,- which is either
- a lib/nodejs/parallel-buffered-runner.js
ParallelBufferedRunneror - a lib/runner.js
Runner
- a lib/nodejs/parallel-buffered-runner.js
- we use
Runnerfor this example since we have assumed non-parallel earlier (watchRuninstead ofwatchParallelRun) - creating the runner just returns an object
- which is either
runcallscreateStatsCollectorwhich listens to various events:- EVENT_RUN_BEGIN
- EVENT_SUITE_BEGIN
- EVENT_TEST_PASS
- EVENT_TEST_FAIL
- EVENT_TEST_PENDING
- EVENT_TEST_END
- EVENT_RUN_END
runcalls_reporterwhich creates a new reporter based on options (done in Mocha constructor)runsets some properties on the runner:- checkLeaks
- fullStackTrace
- asyncOnly
- allowUncaught
- forbidOnly
- forbidPending
- If
options.grep,runcallsrunner.grepwhich grep each test title - If
options.global,runcallsrunner.globalswhich appends the provided globals to the runner's_globalsarray runsets up reporter options:- useColors
- inlineDiffs
- hideDiff
rundefinesdonewhich wraps around the passed-in done-callbackrundefinesrunAsync: 3. callscontext = runGlobalSetup(runner)if appropriate 4. callsrunner.runAsync5. callsrunGlobalTeardown(runner, {context})if appropriateruncallsrunAsync(runner).then(done)runreturnsrunner
runAsynccallsrun- If
options.cleanReferencesAfterRun,runsets up anon(EVENT_SUITE_END, ...)handler to callsuite.cleanReferences runsets anon(EVENT_RUN_END, ...)handler to set state to stopped and call the done-callback with failuresruncalls_removeEventListeneronuncaughtExceptionandunhandledRejectionruncalls_addEventListeneron the same events- These two steps are likely deduping and fixing bookkeeping
runwaits foroptions.delayif needed (we'll skip these details for now) {/* todo */}runcalls inner functionpreparepreparefilters for anyonlysuitespreparecallsbegin, another function inner torunbeginemitsEVENT_RUN_BEGINbegincallsrunSuite(rootSuite, end), whereendis another function inner torunrunSuitecallsgrepTotalon the suite- If no tests to run (or there was a failure and
options.bailis truthy),runSuitereturns a call to itsfn(end) - Else,
runSuiteemitsEVENT_SUITE_BEGIN runSuitedefinesnext, a function that determines the next suiterunSuitedefinesdone, a function that starts executing the next suiterunSuitecallshookwithHOOK_TYPE_BEFORE_ALLand a function that callsrunTests- If
options.dryRunis truthy,hookshort-circuits and calls itsfn hookgets the hooks of the given name (HOOK_TYPE_BEFORE_ALL) usingsuite.getHookshookexecutes all the hooks and callsfn(we are skipping these details too) {/* todo */}hook#fn(defined inrunSuite) callsrunTestswithrunSuite#nextas itsfnrunTestsdefinesnextwhich handles hooks, errors, and running the next testrunTestscallsnext- If
self._abortis truthy,nextreturns a call torunTests#fn(in this case it'srunSuite#next) - If there's an error,
nextreturns a call tohookErr - If there are no more tests,
nextreturns a call torunTests#fn - If the current test doesn't match the grep,
nextcalls itself, then returns - If
test.isPending(),nexthandles that, calls itself, then returns {/* todo */} nextemitsEVENT_TEST_BEGINnextcallshookDownonHOOK_TYPE_BEFORE_EACHwith a function that callsrunTestand handles edge caseshookDowncalls all hooks matching that type, then calls itsfnhookDown#fn(defined inrunTests#next):- If the test is pending, handles that, emits events, and returns a call to
hookUpwith a function that callsrunTests#next - If there's an error, returns a call to
hookErr - Calls
runTestwith a big function runTestcalls_addEventListenerfor theerrorevent to fail the test- If
allowUncaughtis truthy,runTestreturnstest.run(fn) - Else
runTestcallstest.run(fn)in atryand returnsfn(err)in a catch
Testinherits fromRunnableviautils.inherits(yuck!)Runnabledefinesrun
runcallsfn.call(ctx), which actually runs the test!
- How does a delayed run start?
{ "dir": { // raw directories "paths": "Set<string>", // treat the provided paths as literals "glob": "Set<`${string}/**/*`>" // if a dir, watch all its children }, "match": { // match globs to check against "paths": "Set<string>", // treat the provided paths as literals "glob": "Set<`${string}/**/*`>" // if a dir, watch all its children } }