Skip to content

Commit 447408e

Browse files
committed
Parallel: Report unhandled exceptions/rejections between spec files
1 parent fd6381a commit 447408e

File tree

5 files changed

+293
-16
lines changed

5 files changed

+293
-16
lines changed

bin/worker.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,8 @@ const cluster = require('node:cluster');
22
const ParallelWorker = require('../lib/parallel_worker');
33
const Loader = require('../lib/loader');
44

5-
const loader = new Loader();
6-
new ParallelWorker({loader, clusterWorker: cluster.worker});
5+
new ParallelWorker({
6+
loader: new Loader(),
7+
process,
8+
clusterWorker: cluster.worker
9+
});

lib/parallel_runner.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,16 @@ class ParallelRunner extends RunnerBase {
322322
runNextSpecFile();
323323
break;
324324

325+
case 'uncaughtException':
326+
this.addTopLevelError_('lateError',
327+
'Uncaught exception in worker process', msg.error);
328+
break;
329+
330+
case 'unhandledRejection':
331+
this.addTopLevelError_('lateError',
332+
'Unhandled promise rejection in worker process', msg.error);
333+
break;
334+
325335
case 'reporterEvent':
326336
this.handleReporterEvent_(msg.eventName, msg.payload);
327337
break;
@@ -421,6 +431,19 @@ class ParallelRunner extends RunnerBase {
421431
);
422432
});
423433
}
434+
435+
addTopLevelError_(type, msgPrefix, serializedError) {
436+
// Match how jasmine-core reports these in non-parallel situations
437+
this.executionState_.failedExpectations.push({
438+
actual: '',
439+
expected: '',
440+
globalErrorType: 'lateError',
441+
matcherName: '',
442+
message: `${msgPrefix}: ${serializedError.message}`,
443+
passed: false,
444+
stack: serializedError.stack,
445+
});
446+
}
424447
}
425448

426449
function formatErrorFromWorker(error) {

lib/parallel_worker.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,29 @@ class ParallelWorker {
1919
console.error('Jasmine worker got an unrecognized message:', msg);
2020
}
2121
});
22+
23+
// Install global error handlers now, before jasmine-core is booted.
24+
// That allows jasmine-core to override them with its own more specific
25+
// handling. These handlers will take care of errors that occur in between
26+
// spec files.
27+
for (const errorType of ['uncaughtException', 'unhandledRejection']) {
28+
options.process.on(errorType, error => {
29+
if (this.clusterWorker_.isConnected()) {
30+
this.clusterWorker_.send({
31+
type: errorType,
32+
error: serializeError(error)
33+
});
34+
} else {
35+
// Don't try to report errors after disconnect. If we do, it'll cause
36+
// another unhandled exception. The resulting error-and-reporting loop
37+
// can keep the runner from finishing.
38+
console.error(`${errorType} in Jasmine worker process after disconnect:`, error);
39+
console.error('This error cannot be reported properly because it ' +
40+
'happened after the worker process was disconnected.'
41+
);
42+
}
43+
});
44+
}
2245
}
2346

2447
configure(options) {

spec/parallel_runner_spec.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,74 @@ describe('ParallelRunner', function() {
655655
}
656656
});
657657

658+
it('reports unhandled exceptions and promise rejections from workers', async function() {
659+
this.testJasmine.numWorkers = 2;
660+
this.testJasmine.loadConfig({
661+
spec_dir: 'some/spec/dir'
662+
});
663+
this.testJasmine.addSpecFile('spec1.js');
664+
this.testJasmine.addSpecFile('spec2.js');
665+
const executePromise = this.testJasmine.execute();
666+
await this.emitAllBooted();
667+
668+
await new Promise(resolve => setTimeout(resolve));
669+
this.emitFileDone(this.cluster.workers[0], {
670+
failedExpectations: ['failed expectation 1'],
671+
deprecationWarnings: [],
672+
});
673+
this.cluster.workers[0].emit('message', {
674+
type: 'uncaughtException',
675+
error: {
676+
message: 'not caught',
677+
stack: 'it happened here'
678+
},
679+
});
680+
this.cluster.workers[0].emit('message', {
681+
type: 'unhandledRejection',
682+
error: {
683+
message: 'not handled',
684+
stack: 'it happened there'
685+
},
686+
});
687+
this.emitFileDone(this.cluster.workers[1], {
688+
failedExpectations: ['failed expectation 2'],
689+
deprecationWarnings: [''],
690+
});
691+
692+
await this.disconnect();
693+
await executePromise;
694+
695+
expect(this.consoleReporter.jasmineDone).toHaveBeenCalledWith(
696+
jasmine.objectContaining({
697+
overallStatus: 'failed',
698+
failedExpectations: [
699+
'failed expectation 1',
700+
// We don't just pass these through from jasmine-core,
701+
// so verify the actual output format.
702+
{
703+
actual: '',
704+
expected: '',
705+
globalErrorType: 'lateError',
706+
matcherName: '',
707+
message: 'Uncaught exception in worker process: not caught',
708+
passed: false,
709+
stack: 'it happened here',
710+
},
711+
{
712+
actual: '',
713+
expected: '',
714+
globalErrorType: 'lateError',
715+
matcherName: '',
716+
message: 'Unhandled promise rejection in worker process: not handled',
717+
passed: false,
718+
stack: 'it happened there',
719+
},
720+
'failed expectation 2',
721+
],
722+
})
723+
);
724+
});
725+
658726
it('handles errors from reporters', async function() {
659727
const reportDispatcher = new StubParallelReportDispatcher();
660728
spyOn(reportDispatcher, 'installGlobalErrors');

0 commit comments

Comments
 (0)