| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400 |
- /**
- * Serialization/deserialization classes and functions for communication between a main Mocha process and worker processes.
- * @module serializer
- * @private
- */
- 'use strict';
- /**
- * @typedef {import('../types.d.ts').SerializedEvent} SerializedEvent
- * @typedef {import('../types.d.ts').SerializedWorkerResult} SerializedWorkerResult
- */
- const {type, breakCircularDeps} = 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 minimum 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
- */
- static _serialize(pairs, parent, key) {
- let value = parent[key];
- 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
- });
- // mutates the object
- breakCircularDeps(result.error);
- const pairs = Object.keys(result).map(key => [result, key]);
- const seenPairs = new Set();
- let pair;
- while ((pair = pairs.shift())) {
- if (seenPairs.has(pair[1])) {
- continue;
- }
- seenPairs.add(pair[1]);
- SerializableEvent._serialize(pairs, ...pair);
- }
- 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;
|