jasmine
Version:
CLI for Jasmine, a simple JavaScript testing framework for browsers and Node
218 lines (184 loc) • 5.76 kB
JavaScript
const regexSpecFilter = require("./filters/regex_spec_filter");
class ParallelWorker {
constructor(options) {
this.loader_= options.loader;
this.clusterWorker_ = options.clusterWorker;
this.clusterWorker_.on('message', msg => {
switch (msg.type) {
case 'configure':
this.configure(msg.configuration);
break;
case 'runSpecFile':
this.runSpecFile(msg.filePath);
break;
default:
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 => {
const sent = sendIfConnected(this.clusterWorker_, {
type: errorType,
error: serializeError(error)
});
if (!sent) {
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.'
);
}
});
}
this.clusterWorker_.send({type: 'readyForConfig'});
}
configure(options) {
this.loader_.alwaysImport = options.jsLoader !== 'require';
this.envPromise_ = this.loader_.load(options.jasmineCorePath || 'jasmine-core')
.then(core => {
let env;
if (options.globals === false) {
env = core.noGlobals().jasmine.getEnv();
} else {
env = core.boot().getEnv();
}
env.addReporter(forwardingReporter(this.clusterWorker_));
env.addReporter({
jasmineDone: event => this.jasmineDoneEvent_ = event
});
if (options.env) {
env.configure(options.env);
}
if (options.filter) {
env.configure({
specFilter: regexSpecFilter(options.filter)
});
}
return this.loadFiles_(options.requires || [])
.then(() => {
env.setParallelLoadingState('helpers');
return this.loadFiles_(options.helpers);
})
.then(() => {
env.setParallelLoadingState('specs');
this.clusterWorker_.send({type: 'booted'});
return env;
});
})
.catch(error => {
this.clusterWorker_.send({
type: 'fatalError',
error: serializeError(error)
});
});
}
async loadFiles_(files) {
for (const file of files) {
await this.loader_.load(file);
}
}
runSpecFile(specFilePath) {
(async () => {
this.jasmineDoneEvent_ = null;
let env = await this.envPromise_;
env.parallelReset();
try {
await this.loader_.load(specFilePath);
} catch (error) {
this.clusterWorker_.send({
type: 'specFileLoadError',
filePath: specFilePath,
error: serializeError(error)
});
return;
}
await env.execute();
if (!this.clusterWorker_.isConnected()) {
return;
}
this.clusterWorker_.send({
type: 'specFileDone',
overallStatus: this.jasmineDoneEvent_.overallStatus,
incompleteCode: this.jasmineDoneEvent_.incompleteCode,
incompleteReason: this.jasmineDoneEvent_.incompleteReason,
failedExpectations: this.jasmineDoneEvent_.failedExpectations,
deprecationWarnings: this.jasmineDoneEvent_.deprecationWarnings,
});
})();
}
}
function serializeError(error) {
return {
message: error.message,
stack: error.stack
};
}
function forwardingReporter(clusterWorker) {
const reporter = {};
const eventNames = ['suiteStarted', 'suiteDone', 'specStarted', 'specDone'];
for (const eventName of eventNames) {
reporter[eventName] = function (payload) {
const msg = {
type: 'reporterEvent',
eventName,
payload: {
...payload,
// IDs we get from -core are only unique within this process.
// Make them globally unique by prepending the worker ID.
id: `${clusterWorker.id}-${payload.id}`
}
};
try {
sendIfConnected(clusterWorker, msg);
} catch (e) {
if (e instanceof TypeError) {
msg.payload = fixNonSerializableReporterEvent(payload);
sendIfConnected(clusterWorker, msg);
} else {
throw e;
}
}
};
}
return reporter;
}
function sendIfConnected(clusterWorker, msg) {
if (clusterWorker.isConnected()) {
try {
clusterWorker.send(msg);
return true;
} catch (e) {
// EPIPE may be thrown if the worker receives a disconnect between
// the calls to isConnected() and send() above.
if (e.code !== 'EPIPE') {
throw e;
}
}
}
return false;
}
function fixNonSerializableReporterEvent(payload) {
payload = {...payload};
for (const ak of ['passedExpectations', 'failedExpectations']) {
if (payload[ak]) {
payload[ak] = payload[ak].map(function(expectation) {
expectation = {...expectation};
for (const vk of ['expected', 'actual']) {
try {
JSON.stringify(expectation[vk]);
// eslint-disable-next-line no-unused-vars
} catch (ex) {
expectation[vk] = '<not serializable>';
}
}
return expectation;
});
}
}
return payload;
}
module.exports = ParallelWorker;