Spaces:
Paused
Paused
| /** | |
| * Serialization/deserialization classes and functions for communication between a main Mocha process and worker processes. | |
| * @module serializer | |
| * @private | |
| */ | |
| ; | |
| 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 | |
| */ | |