413 lines
13 KiB
JavaScript
413 lines
13 KiB
JavaScript
|
/**
|
||
|
* Serialization/deserialization classes and functions for communication between a main Mocha process and worker processes.
|
||
|
* @module serializer
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
'use strict';
|
||
|
|
||
|
const {type} = require('../utils');
|
||
|
const {createInvalidArgumentTypeError} = require('../errors');
|
||
|
// this is not named `mocha:parallel:serializer` because it's noisy and it's
|
||
|
// helpful to be able to write `DEBUG=mocha:parallel*` and get everything else.
|
||
|
const debug = require('debug')('mocha:serializer');
|
||
|
|
||
|
const SERIALIZABLE_RESULT_NAME = 'SerializableWorkerResult';
|
||
|
const SERIALIZABLE_TYPES = new Set(['object', 'array', 'function', 'error']);
|
||
|
|
||
|
/**
|
||
|
* The serializable result of a test file run from a worker.
|
||
|
* @private
|
||
|
*/
|
||
|
class SerializableWorkerResult {
|
||
|
/**
|
||
|
* Creates instance props; of note, the `__type` prop.
|
||
|
*
|
||
|
* Note that the failure count is _redundant_ and could be derived from the
|
||
|
* list of events; but since we're already doing the work, might as well use
|
||
|
* it.
|
||
|
* @param {SerializableEvent[]} [events=[]] - Events to eventually serialize
|
||
|
* @param {number} [failureCount=0] - Failure count
|
||
|
*/
|
||
|
constructor(events = [], failureCount = 0) {
|
||
|
/**
|
||
|
* The number of failures in this run
|
||
|
* @type {number}
|
||
|
*/
|
||
|
this.failureCount = failureCount;
|
||
|
/**
|
||
|
* All relevant events emitted from the {@link Runner}.
|
||
|
* @type {SerializableEvent[]}
|
||
|
*/
|
||
|
this.events = events;
|
||
|
|
||
|
/**
|
||
|
* Symbol-like value needed to distinguish when attempting to deserialize
|
||
|
* this object (once it's been received over IPC).
|
||
|
* @type {Readonly<"SerializableWorkerResult">}
|
||
|
*/
|
||
|
Object.defineProperty(this, '__type', {
|
||
|
value: SERIALIZABLE_RESULT_NAME,
|
||
|
enumerable: true,
|
||
|
writable: false
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Instantiates a new {@link SerializableWorkerResult}.
|
||
|
* @param {...any} args - Args to constructor
|
||
|
* @returns {SerializableWorkerResult}
|
||
|
*/
|
||
|
static create(...args) {
|
||
|
return new SerializableWorkerResult(...args);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Serializes each {@link SerializableEvent} in our `events` prop;
|
||
|
* makes this object read-only.
|
||
|
* @returns {Readonly<SerializableWorkerResult>}
|
||
|
*/
|
||
|
serialize() {
|
||
|
this.events.forEach(event => {
|
||
|
event.serialize();
|
||
|
});
|
||
|
return Object.freeze(this);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Deserializes a {@link SerializedWorkerResult} into something reporters can
|
||
|
* use; calls {@link SerializableEvent.deserialize} on each item in its
|
||
|
* `events` prop.
|
||
|
* @param {SerializedWorkerResult} obj
|
||
|
* @returns {SerializedWorkerResult}
|
||
|
*/
|
||
|
static deserialize(obj) {
|
||
|
obj.events.forEach(event => {
|
||
|
SerializableEvent.deserialize(event);
|
||
|
});
|
||
|
return obj;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Returns `true` if this is a {@link SerializedWorkerResult} or a
|
||
|
* {@link SerializableWorkerResult}.
|
||
|
* @param {*} value - A value to check
|
||
|
* @returns {boolean} If true, it's deserializable
|
||
|
*/
|
||
|
static isSerializedWorkerResult(value) {
|
||
|
return (
|
||
|
value instanceof SerializableWorkerResult ||
|
||
|
(type(value) === 'object' && value.__type === SERIALIZABLE_RESULT_NAME)
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Represents an event, emitted by a {@link Runner}, which is to be transmitted
|
||
|
* over IPC.
|
||
|
*
|
||
|
* Due to the contents of the event data, it's not possible to send them
|
||
|
* verbatim. When received by the main process--and handled by reporters--these
|
||
|
* objects are expected to contain {@link Runnable} instances. This class
|
||
|
* provides facilities to perform the translation via serialization and
|
||
|
* deserialization.
|
||
|
* @private
|
||
|
*/
|
||
|
class SerializableEvent {
|
||
|
/**
|
||
|
* Constructs a `SerializableEvent`, throwing if we receive unexpected data.
|
||
|
*
|
||
|
* Practically, events emitted from `Runner` have a minumum of zero (0)
|
||
|
* arguments-- (for example, {@link Runnable.constants.EVENT_RUN_BEGIN}) and a
|
||
|
* maximum of two (2) (for example,
|
||
|
* {@link Runnable.constants.EVENT_TEST_FAIL}, where the second argument is an
|
||
|
* `Error`). The first argument, if present, is a {@link Runnable}. This
|
||
|
* constructor's arguments adhere to this convention.
|
||
|
* @param {string} eventName - A non-empty event name.
|
||
|
* @param {any} [originalValue] - Some data. Corresponds to extra arguments
|
||
|
* passed to `EventEmitter#emit`.
|
||
|
* @param {Error} [originalError] - An error, if there's an error.
|
||
|
* @throws If `eventName` is empty, or `originalValue` is a non-object.
|
||
|
*/
|
||
|
constructor(eventName, originalValue, originalError) {
|
||
|
if (!eventName) {
|
||
|
throw createInvalidArgumentTypeError(
|
||
|
'Empty `eventName` string argument',
|
||
|
'eventName',
|
||
|
'string'
|
||
|
);
|
||
|
}
|
||
|
/**
|
||
|
* The event name.
|
||
|
* @memberof SerializableEvent
|
||
|
*/
|
||
|
this.eventName = eventName;
|
||
|
const originalValueType = type(originalValue);
|
||
|
if (originalValueType !== 'object' && originalValueType !== 'undefined') {
|
||
|
throw createInvalidArgumentTypeError(
|
||
|
`Expected object but received ${originalValueType}`,
|
||
|
'originalValue',
|
||
|
'object'
|
||
|
);
|
||
|
}
|
||
|
/**
|
||
|
* An error, if present.
|
||
|
* @memberof SerializableEvent
|
||
|
*/
|
||
|
Object.defineProperty(this, 'originalError', {
|
||
|
value: originalError,
|
||
|
enumerable: false
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* The raw value.
|
||
|
*
|
||
|
* We don't want this value sent via IPC; making it non-enumerable will do that.
|
||
|
*
|
||
|
* @memberof SerializableEvent
|
||
|
*/
|
||
|
Object.defineProperty(this, 'originalValue', {
|
||
|
value: originalValue,
|
||
|
enumerable: false
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* In case you hated using `new` (I do).
|
||
|
*
|
||
|
* @param {...any} args - Args for {@link SerializableEvent#constructor}.
|
||
|
* @returns {SerializableEvent} A new `SerializableEvent`
|
||
|
*/
|
||
|
static create(...args) {
|
||
|
return new SerializableEvent(...args);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Used internally by {@link SerializableEvent#serialize}.
|
||
|
* @ignore
|
||
|
* @param {Array<object|string>} pairs - List of parent/key tuples to process; modified in-place. This JSDoc type is an approximation
|
||
|
* @param {object} parent - Some parent object
|
||
|
* @param {string} key - Key to inspect
|
||
|
* @param {WeakSet<Object>} seenObjects - For avoiding circular references
|
||
|
*/
|
||
|
static _serialize(pairs, parent, key, seenObjects) {
|
||
|
let value = parent[key];
|
||
|
if (seenObjects.has(value)) {
|
||
|
parent[key] = Object.create(null);
|
||
|
return;
|
||
|
}
|
||
|
let _type = type(value);
|
||
|
if (_type === 'error') {
|
||
|
// we need to reference the stack prop b/c it's lazily-loaded.
|
||
|
// `__type` is necessary for deserialization to create an `Error` later.
|
||
|
// `message` is apparently not enumerable, so we must handle it specifically.
|
||
|
value = Object.assign(Object.create(null), value, {
|
||
|
stack: value.stack,
|
||
|
message: value.message,
|
||
|
__type: 'Error'
|
||
|
});
|
||
|
parent[key] = value;
|
||
|
// after this, set the result of type(value) to be `object`, and we'll throw
|
||
|
// whatever other junk is in the original error into the new `value`.
|
||
|
_type = 'object';
|
||
|
}
|
||
|
switch (_type) {
|
||
|
case 'object':
|
||
|
if (type(value.serialize) === 'function') {
|
||
|
parent[key] = value.serialize();
|
||
|
} else {
|
||
|
// by adding props to the `pairs` array, we will process it further
|
||
|
pairs.push(
|
||
|
...Object.keys(value)
|
||
|
.filter(key => SERIALIZABLE_TYPES.has(type(value[key])))
|
||
|
.map(key => [value, key])
|
||
|
);
|
||
|
}
|
||
|
break;
|
||
|
case 'function':
|
||
|
// we _may_ want to dig in to functions for some assertion libraries
|
||
|
// that might put a usable property on a function.
|
||
|
// for now, just zap it.
|
||
|
delete parent[key];
|
||
|
break;
|
||
|
case 'array':
|
||
|
pairs.push(
|
||
|
...value
|
||
|
.filter(value => SERIALIZABLE_TYPES.has(type(value)))
|
||
|
.map((value, index) => [value, index])
|
||
|
);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Modifies this object *in place* (for theoretical memory consumption &
|
||
|
* performance reasons); serializes `SerializableEvent#originalValue` (placing
|
||
|
* the result in `SerializableEvent#data`) and `SerializableEvent#error`.
|
||
|
* Freezes this object. The result is an object that can be transmitted over
|
||
|
* IPC.
|
||
|
* If this quickly becomes unmaintainable, we will want to move towards immutable
|
||
|
* objects post-haste.
|
||
|
*/
|
||
|
serialize() {
|
||
|
// given a parent object and a key, inspect the value and decide whether
|
||
|
// to replace it, remove it, or add it to our `pairs` array to further process.
|
||
|
// this is recursion in loop form.
|
||
|
const originalValue = this.originalValue;
|
||
|
const result = Object.assign(Object.create(null), {
|
||
|
data:
|
||
|
type(originalValue) === 'object' &&
|
||
|
type(originalValue.serialize) === 'function'
|
||
|
? originalValue.serialize()
|
||
|
: originalValue,
|
||
|
error: this.originalError
|
||
|
});
|
||
|
|
||
|
const pairs = Object.keys(result).map(key => [result, key]);
|
||
|
const seenObjects = new WeakSet();
|
||
|
|
||
|
let pair;
|
||
|
while ((pair = pairs.shift())) {
|
||
|
SerializableEvent._serialize(pairs, ...pair, seenObjects);
|
||
|
seenObjects.add(pair[0]);
|
||
|
}
|
||
|
|
||
|
this.data = result.data;
|
||
|
this.error = result.error;
|
||
|
|
||
|
return Object.freeze(this);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Used internally by {@link SerializableEvent.deserialize}; creates an `Error`
|
||
|
* from an `Error`-like (serialized) object
|
||
|
* @ignore
|
||
|
* @param {Object} value - An Error-like value
|
||
|
* @returns {Error} Real error
|
||
|
*/
|
||
|
static _deserializeError(value) {
|
||
|
const error = new Error(value.message);
|
||
|
error.stack = value.stack;
|
||
|
Object.assign(error, value);
|
||
|
delete error.__type;
|
||
|
return error;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Used internally by {@link SerializableEvent.deserialize}; recursively
|
||
|
* deserializes an object in-place.
|
||
|
* @param {object|Array} parent - Some object or array
|
||
|
* @param {string|number} key - Some prop name or array index within `parent`
|
||
|
*/
|
||
|
static _deserializeObject(parent, key) {
|
||
|
if (key === '__proto__') {
|
||
|
delete parent[key];
|
||
|
return;
|
||
|
}
|
||
|
const value = parent[key];
|
||
|
// keys beginning with `$$` are converted into functions returning the value
|
||
|
// and renamed, stripping the `$$` prefix.
|
||
|
// functions defined this way cannot be array members!
|
||
|
if (type(key) === 'string' && key.startsWith('$$')) {
|
||
|
const newKey = key.slice(2);
|
||
|
parent[newKey] = () => value;
|
||
|
delete parent[key];
|
||
|
key = newKey;
|
||
|
}
|
||
|
if (type(value) === 'array') {
|
||
|
value.forEach((_, idx) => {
|
||
|
SerializableEvent._deserializeObject(value, idx);
|
||
|
});
|
||
|
} else if (type(value) === 'object') {
|
||
|
if (value.__type === 'Error') {
|
||
|
parent[key] = SerializableEvent._deserializeError(value);
|
||
|
} else {
|
||
|
Object.keys(value).forEach(key => {
|
||
|
SerializableEvent._deserializeObject(value, key);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Deserialize value returned from a worker into something more useful.
|
||
|
* Does not return the same object.
|
||
|
* @todo do this in a loop instead of with recursion (if necessary)
|
||
|
* @param {SerializedEvent} obj - Object returned from worker
|
||
|
* @returns {SerializedEvent} Deserialized result
|
||
|
*/
|
||
|
static deserialize(obj) {
|
||
|
if (!obj) {
|
||
|
throw createInvalidArgumentTypeError('Expected value', obj);
|
||
|
}
|
||
|
|
||
|
obj = Object.assign(Object.create(null), obj);
|
||
|
|
||
|
if (obj.data) {
|
||
|
Object.keys(obj.data).forEach(key => {
|
||
|
SerializableEvent._deserializeObject(obj.data, key);
|
||
|
});
|
||
|
}
|
||
|
|
||
|
if (obj.error) {
|
||
|
obj.error = SerializableEvent._deserializeError(obj.error);
|
||
|
}
|
||
|
|
||
|
return obj;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* "Serializes" a value for transmission over IPC as a message.
|
||
|
*
|
||
|
* If value is an object and has a `serialize()` method, call that method; otherwise return the object and hope for the best.
|
||
|
*
|
||
|
* @param {*} [value] - A value to serialize
|
||
|
*/
|
||
|
exports.serialize = function serialize(value) {
|
||
|
const result =
|
||
|
type(value) === 'object' && type(value.serialize) === 'function'
|
||
|
? value.serialize()
|
||
|
: value;
|
||
|
debug('serialized: %O', result);
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* "Deserializes" a "message" received over IPC.
|
||
|
*
|
||
|
* This could be expanded with other objects that need deserialization,
|
||
|
* but at present time we only care about {@link SerializableWorkerResult} objects.
|
||
|
*
|
||
|
* @param {*} [value] - A "message" to deserialize
|
||
|
*/
|
||
|
exports.deserialize = function deserialize(value) {
|
||
|
const result = SerializableWorkerResult.isSerializedWorkerResult(value)
|
||
|
? SerializableWorkerResult.deserialize(value)
|
||
|
: value;
|
||
|
debug('deserialized: %O', result);
|
||
|
return result;
|
||
|
};
|
||
|
|
||
|
exports.SerializableEvent = SerializableEvent;
|
||
|
exports.SerializableWorkerResult = SerializableWorkerResult;
|
||
|
|
||
|
/**
|
||
|
* The result of calling `SerializableEvent.serialize`, as received
|
||
|
* by the deserializer.
|
||
|
* @private
|
||
|
* @typedef {Object} SerializedEvent
|
||
|
* @property {object?} data - Optional serialized data
|
||
|
* @property {object?} error - Optional serialized `Error`
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* The result of calling `SerializableWorkerResult.serialize` as received
|
||
|
* by the deserializer.
|
||
|
* @private
|
||
|
* @typedef {Object} SerializedWorkerResult
|
||
|
* @property {number} failureCount - Number of failures
|
||
|
* @property {SerializedEvent[]} events - Serialized events
|
||
|
* @property {"SerializedWorkerResult"} __type - Symbol-like to denote the type of object this is
|
||
|
*/
|