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