Skip to content

Commit

Permalink
Parallel: Report unhandled exceptions/rejections between spec files
Browse files Browse the repository at this point in the history
  • Loading branch information
sgravrock committed Apr 29, 2023
1 parent fd6381a commit 447408e
Show file tree
Hide file tree
Showing 5 changed files with 293 additions and 16 deletions.
7 changes: 5 additions & 2 deletions bin/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@ const cluster = require('node:cluster');
const ParallelWorker = require('../lib/parallel_worker');
const Loader = require('../lib/loader');

const loader = new Loader();
new ParallelWorker({loader, clusterWorker: cluster.worker});
new ParallelWorker({
loader: new Loader(),
process,
clusterWorker: cluster.worker
});
23 changes: 23 additions & 0 deletions lib/parallel_runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,16 @@ class ParallelRunner extends RunnerBase {
runNextSpecFile();
break;

case 'uncaughtException':
this.addTopLevelError_('lateError',
'Uncaught exception in worker process', msg.error);
break;

case 'unhandledRejection':
this.addTopLevelError_('lateError',
'Unhandled promise rejection in worker process', msg.error);
break;

case 'reporterEvent':
this.handleReporterEvent_(msg.eventName, msg.payload);
break;
Expand Down Expand Up @@ -421,6 +431,19 @@ class ParallelRunner extends RunnerBase {
);
});
}

addTopLevelError_(type, msgPrefix, serializedError) {
// Match how jasmine-core reports these in non-parallel situations
this.executionState_.failedExpectations.push({
actual: '',
expected: '',
globalErrorType: 'lateError',
matcherName: '',
message: `${msgPrefix}: ${serializedError.message}`,
passed: false,
stack: serializedError.stack,
});
}
}

function formatErrorFromWorker(error) {
Expand Down
23 changes: 23 additions & 0 deletions lib/parallel_worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,29 @@ class ParallelWorker {
console.error('Jasmine worker got an unrecognized message:', msg);
}
});

// Install global error handlers now, before jasmine-core is booted.
// That allows jasmine-core to override them with its own more specific
// handling. These handlers will take care of errors that occur in between
// spec files.
for (const errorType of ['uncaughtException', 'unhandledRejection']) {
options.process.on(errorType, error => {
if (this.clusterWorker_.isConnected()) {
this.clusterWorker_.send({
type: errorType,
error: serializeError(error)
});
} else {
// Don't try to report errors after disconnect. If we do, it'll cause
// another unhandled exception. The resulting error-and-reporting loop
// can keep the runner from finishing.
console.error(`${errorType} in Jasmine worker process after disconnect:`, error);
console.error('This error cannot be reported properly because it ' +
'happened after the worker process was disconnected.'
);
}
});
}
}

configure(options) {
Expand Down
68 changes: 68 additions & 0 deletions spec/parallel_runner_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,74 @@ describe('ParallelRunner', function() {
}
});

it('reports unhandled exceptions and promise rejections from workers', async function() {
this.testJasmine.numWorkers = 2;
this.testJasmine.loadConfig({
spec_dir: 'some/spec/dir'
});
this.testJasmine.addSpecFile('spec1.js');
this.testJasmine.addSpecFile('spec2.js');
const executePromise = this.testJasmine.execute();
await this.emitAllBooted();

await new Promise(resolve => setTimeout(resolve));
this.emitFileDone(this.cluster.workers[0], {
failedExpectations: ['failed expectation 1'],
deprecationWarnings: [],
});
this.cluster.workers[0].emit('message', {
type: 'uncaughtException',
error: {
message: 'not caught',
stack: 'it happened here'
},
});
this.cluster.workers[0].emit('message', {
type: 'unhandledRejection',
error: {
message: 'not handled',
stack: 'it happened there'
},
});
this.emitFileDone(this.cluster.workers[1], {
failedExpectations: ['failed expectation 2'],
deprecationWarnings: [''],
});

await this.disconnect();
await executePromise;

expect(this.consoleReporter.jasmineDone).toHaveBeenCalledWith(
jasmine.objectContaining({
overallStatus: 'failed',
failedExpectations: [
'failed expectation 1',
// We don't just pass these through from jasmine-core,
// so verify the actual output format.
{
actual: '',
expected: '',
globalErrorType: 'lateError',
matcherName: '',
message: 'Uncaught exception in worker process: not caught',
passed: false,
stack: 'it happened here',
},
{
actual: '',
expected: '',
globalErrorType: 'lateError',
matcherName: '',
message: 'Unhandled promise rejection in worker process: not handled',
passed: false,
stack: 'it happened there',
},
'failed expectation 2',
],
})
);
});

it('handles errors from reporters', async function() {
const reportDispatcher = new StubParallelReportDispatcher();
spyOn(reportDispatcher, 'installGlobalErrors');
Expand Down
Loading

0 comments on commit 447408e

Please sign in to comment.