🚀 Feature: runner.on('end', ...) doesn't respect pending Promises
my current annoyance with runner.on('end', ...) in my custom reporter is that mocha doesn't wait for any async/promises to complete. my reporter writes to an xlsx file and this gets aborted on the end handler, prompting me to add the dreaded setTimeout kludge--e.g.:
var reportDone = false; // set to true in an async xlsx writer when it's done.
function isReportDone() {
if (!reportDone) {
setTimeout(1000, isReportDone())
}
}
// ...
runner.on('end', function() {
// ...
setTimeout(1000, isReportDone());
})
with a test suite, end() handler can be optionally passed with done callback which prevents mocha from ending the suite until done() is called.
i propose that runner.on('end', ...) could optionally have the same structure:
runner.on('end', function(done) {
// ...
aPromiseCalled.then(function() {
console.log('really done!');
done();
});
});
with the same behavior as end() test suite: prevent the reporter to end until done() callback is called, thereby not requiring the setTimeout kludgy fix.
Hmm... I don't quite understand why this is necessary. Can you show more code?
Could this be because _mocha#L467 isn't wrapped in process.on('exit', like _mocha#L461 is (both passed to run at _mocha#L458 based on a condition that I can't figure out what would set it)?
@boneskull, here is a simple example derived from the minimalistic sample in mocha.js documentation on third-party reporters:
var Promise = require('bluebird')
function MyReporter(runner) {
// ...
runner.on('end', function() {
winston.info('[SUMMARY] -- %d/%d', passes, passes + failures);
Promise.delay(500).then(function() {
console.log('i am spartacus') // this is not called unless ...
})
// ...this next line is uncommented
//setTimeout(1000, function() {})
process.exit(failures);
});
}
@ScottFreeCode That program.exit is the --no-exit option, which sounds applicable here. @dalegaspi Please see if running Mocha with --no-exit gives better results, and is a viable workaround for the time being.
--no-exit may result in a hanging process in case of an uncaught exception or when used with --bail, depending on how well your tests and sources (and Mocha, for that matter) recover gracefully.
So it's weird to use an EventEmitter in this manner:
runner.on('end', function (done) {
someAsyncStuff(function(err) {
done(err);
});
});
Not that the handler shouldn't be allowed to do async work, but that the EventEmitter--in this case, the Runner instance--shouldn't be waiting on its listeners to complete. The idea, of course, is that the EventEmitter will emit an event without concern as to what is listening.
Any given reporter is not necessarily the only listener on the Runner's events. This adds not-insignificant complexity, because:
- For every event, before emitting, the
Runnerneeds to inspect its listeners via EventEmitter#listeners().- Take a count of how many listeners request a
donefunction. This may even be impossible without breaking things, because:- Not every event has the same amount of arguments; e.g.
passwill emit aTestinstance, butfailwill emit aTestand anError. - There's no guarantee any given reporter's listener for any given event has an arity equal to the argument count.
- Not every event has the same amount of arguments; e.g.
- Once we have that, the
Runnermay emit its event - When a listener calls
done(), subtract from the count, and finally move on when the count is0, and all listeners have completed.
- Take a count of how many listeners request a
On a large suite of tests, this could impact performance. We shouldn't abuse EventEmitters in this manner.
I'm open to other solutions, but here's one using the same semaphore-like idea:
- The
Runnerhas anrunningJobscount property. - Instead of a
donecallback, theRunnerinstance can expose a method,enqueueJob(func). -
enqueueJob()will immediately incrementrunningJobs. - Function
funcis executed thereafter, and is passed a standard Node.js-style callback. - This callback decrements
runningJobs. -
mocha's "exit" behavior would then wait forrunningJobsto reach 0 before exiting.
This way, we can wait on internal operations before exiting, but not any operations which might come from tests, nor code under test.
The above implementation seems more straightforward if we used Promises. Portability concerns would mean we'd have to add a shim or compatible library. Mocha doesn't use them internally, so there's no precedence for this.
I can't envision having the time to do this, but if someone wants to send a PR along these lines--at minimum, which doesn't abuse events--that'd be helpful.
@boneskull the --no-exit works without having to call setTimeout() as long as i don't make an explicit call to process.exit(failures) in the 'end' handler...which, i guess, is fine for our purpose.
For everybody who faced the same issue and found this issue: as a workaround you can convert async calls to sync via deasync
In my condition, I need to send the result of mocha to emails after run all test in our continuous integration flow. I wrote a reporter to do this. But the end listenter can not wait for the sending finish, exit at once when end function is called.
We can't use the option --no-exit, it will hang our CI till its timeout. So I think giving an done callback to end listener is a good idea to resolve it.
Kindly refer to this alternative solution #https://github.com/cypress-io/cypress/issues/7139#issuecomment-1074747820