/** * 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} */ 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} 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} 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 */