-
-
Notifications
You must be signed in to change notification settings - Fork 3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
implementation of global setup/teardown; closes #4308 #4360
Conversation
this will get rebased onto |
the design here is up for discussion btw. we have a new file, was thinking of taking it a step further and moving all of it into some sort of system that, say, uses event listeners to define functionality. so there'd be some implementation of |
fc0e78f
to
8b38307
Compare
const acc = []; | ||
for (const mod of requires) { | ||
const pluginLoader = PluginLoader.create(); | ||
for await (const mod of requires) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this may not be strictly necessary, but I think it's going to help reason about the order in which requires are loaded.
lib/plugin.js
Outdated
const PLUGIN_NAMES = new Set(Object.values(constants)); | ||
|
||
exports.PluginLoader = class PluginLoader { | ||
constructor({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
overriding the defaults here is just for testability at this point.
lib/plugin.js
Outdated
} | ||
|
||
async finalize() { | ||
const mochaHooks = this.pluginMap.get(PLUGIN_ROOT_HOOKS); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I should add this in a code comment, but the idea here is that we call load()
on all required modules, which fishes the special named exports out of those modules, and then calling finalize()
aggregates/flattens everything into a single data structure, which is suitable for passing into Mocha
constructor
lib/plugin.js
Outdated
} | ||
}; | ||
|
||
const PluginValidators = { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I dunno, these are some validator functions to check that the named exports e.g. mochaHooks
are of the correct shape. the createFunctionArrayValidator
one above looks for a function or array thereof. if we're going to be in the business of validating a bunch of code this way, maybe we should use a suitable library (e.g., ajv
or joi
or something) instead of this.
lib/plugin.js
Outdated
* @private | ||
* @returns {MochaRootHookObject} | ||
*/ | ||
const aggregateRootHooks = (exports.aggregateRootHooks = async ( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
moved from run-helpers
module
What I mean by "reporters and interfaces should use this plugin system" above is that it's currently awkward to implement a third-party reporter or interface. You have to essentially set properties on |
rebasing onto branch |
34e9a2b
to
20dd278
Compare
fae370f
to
c8c81fa
Compare
hey, green build finally. looks like it needs some more tests though. |
06e3d33
to
699016f
Compare
throw createUnsupportedError( | ||
'mochaHooks must be an object or a function returning (or fulfilling with) an object' | ||
); | ||
if (requiredModule && typeof requiredModule === 'object') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the logic here is "look in the module we've loaded via --require
for any known named exports and figure out what to do with them". since these could be multiple and spread out over several files, on L102 below, we call pluginLoader.finalize()
which allows aggregation of these into a single object, suitable for passing into the Mocha
constructor.
for example, we might have mochaHooks
in a.js
and mochaHooks
in b.js
; the pluginLoader.load()
call will collect these, and we mash them all together into an object (containing props for each hook type, and arrays of hook callback functions) in pluginLoader.finalize()
.
const plugins = await handleRequires(argv.require); | ||
validateLegacyPlugin(argv, 'reporter', Mocha.reporters); | ||
validateLegacyPlugin(argv, 'ui', Mocha.interfaces); | ||
Object.assign(argv, plugins); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of just a rootHooks
prop on the options object for Mocha
, the plugins
object will have several properties which we want to merge in.
Thanks @craigtaub. Yeah there's really no reasonable way to share a file, db handle, socket etc across spawned worker processes. Each worker process would have its own handle, and this would be suitable for root hook plugins instead of these globals. You can share state between global setup and teardown, but the suites, hooks, and tests themselves can not access that state. "Fixture", I think, is not inappropriate (see wiki). we're using it to mean roughly the same thing in our test "fixture" files--it's some environment or a context we test in. |
9df3cb8
to
a25fa19
Compare
@craigtaub Can you take a look, specifically, at the changes to the "run cycle overview" in |
I'm referencing version 9 in the docs here, but now I can't remember why. I thought I was breaking something, but maybe I'm not. Need to double-check. |
|
||
In a browser, test files are loaded by `<script>` tags, and calling `mocha.run()` begins at step 9 [below](#serial-mode). | ||
|
||
### Serial Mode |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth splitting into parsing and execution phases?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not convinced I can make it any clearer by drawing a line there. do you have any specific suggestions on how to explain it clearly?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not for clarity, just to codify our definition of where these phases start and what's included. Was just imagining a simple divide I.e.
--- Parsing phase
1. ....
...
4. ...
---
--- Execution phase
5. ....
...
----
But they are mainly useful for internal purposes anyway so happy to drop. Leave with you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmmm......
I'm not sure I know what the definition is. certainly, we run our suite callbacks (I think this is what you're referring to with "parsing phase") in their entirety before our hook and test callbacks. In the changes I've made, step 8. refers to this:
- The (default) bdd interface loads the test files in no particular order, which are given an interface-specific
global
context (this is how, e.g.,describe()
ends up as a global in a test file)
- When a test file is loaded, Mocha executes all of its suites and finds--but does not execute--any hooks and tests therein.
- Top-level hooks, tests and suites are all made members of an "invisible" root suite; there is only one root suite for the entire process
after that, we run global setup fixtures, which really has nothing to do with the suites. after that, we run the hook/test callbacks. so to me anyway, there's no clear "logical grouping" of tasks, like you've written above. it's just a sequence of steps.
now... we could make an argument that global setup fixtures should actually run before test files are loaded. that might make more sense, but it would mean that we'd have to expose APIs for programmatic users to trigger setup and teardown execution manually. maybe someone wants this, but a) you can toggle the behavior on and off anyway via the public Mocha#enableGlobalSetup()
and Mocha#enableGlobalTeardown()
methods, and b) it's not very friendly to make programmatic users go thru extra hoops to get standard behavior.
however, this brings up the point that Mocha#runGlobalSetup()
and Mocha#runGlobalTeardown()
should be public, if a programmatic users wants to fiddle with when these run:
const mocha = new Mocha();
mocha.addFile('/some/file.js');
mocha.enableGlobalSetup(false);
// run the global setup early, before mocha.run() is called
await mocha.runGlobalSetup();
// do something else here
mocha.run();
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm interesting.
Sorry I should have said, my understanding is the parsing phase is just options and validation. So it's very short. I think all the loading of files and running comes under execution phase. But that's just my own mental model.
Nice, "run cycle overview" is much improved. |
bab73dd
to
fadff9a
Compare
this has gotten too unwieldy and I'm going to try to break it up |
* @param {Object} [opts] - Options for {@link Runner#run} | ||
* @returns {Promise<number>} Failure count | ||
*/ | ||
Runner.prototype.runAsync = async function runAsync(opts = {}) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like this! 😍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This cleans up the code nicely and is an excellent feature.
e677219
to
ee72712
Compare
ee72712
to
eeb7dae
Compare
I am not going to attempt to break this up, because that's a pain in the ass. suffice to say there are things in here that could technically be in separate PRs. I am sorry. |
This avoids a circular dependency which arises when Mocha is bundled. These are private APIs.
94a2055
to
91eaaf0
Compare
I'm going to assume we're OK with moving |
when AppVeyor passes I'm going to merge this, finally. |
I have some ideas around documenting this further, but I can do it in a subsequent PR. |
FYI as feedback to help you calibrate semver choices for the future, this was a breaking change for reasons summarized here, and should've gone out in 9.0.0. |
This PR adds
mochaGlobalSetup
andmochaGlobalTeardown
fixtures, loaded likemochaHooks
via--require
. the function(s) defined here are guaranteed to run once and only once, in serial, parallel, serial-watch or parallel-watch modes. this should be the preferred way of running global setup/teardown code, and I'll need to update the docs accordingly. the setup and teardown functions will share a single context object, and will only run in the main process.more:
Mocha
class, which needs instance methods to support each. I'm choosing to defer any decisions on this design for nowvalidatePlugin
tovalidateLegacyPlugin
. reporters/interfaces could/should eventually be refactored to use the same system. this would make it less awkward to define a custom reporter, for instance.errors.createPluginError
; directs consumers to useerrors.createLegacyPluginError
instead