serializer.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. /**
  2. * Serialization/deserialization classes and functions for communication between a main Mocha process and worker processes.
  3. * @module serializer
  4. * @private
  5. */
  6. 'use strict';
  7. /**
  8. * @typedef {import('../types.d.ts').SerializedEvent} SerializedEvent
  9. * @typedef {import('../types.d.ts').SerializedWorkerResult} SerializedWorkerResult
  10. */
  11. const {type, breakCircularDeps} = require('../utils');
  12. const {createInvalidArgumentTypeError} = require('../errors');
  13. // this is not named `mocha:parallel:serializer` because it's noisy and it's
  14. // helpful to be able to write `DEBUG=mocha:parallel*` and get everything else.
  15. const debug = require('debug')('mocha:serializer');
  16. const SERIALIZABLE_RESULT_NAME = 'SerializableWorkerResult';
  17. const SERIALIZABLE_TYPES = new Set(['object', 'array', 'function', 'error']);
  18. /**
  19. * The serializable result of a test file run from a worker.
  20. * @private
  21. */
  22. class SerializableWorkerResult {
  23. /**
  24. * Creates instance props; of note, the `__type` prop.
  25. *
  26. * Note that the failure count is _redundant_ and could be derived from the
  27. * list of events; but since we're already doing the work, might as well use
  28. * it.
  29. * @param {SerializableEvent[]} [events=[]] - Events to eventually serialize
  30. * @param {number} [failureCount=0] - Failure count
  31. */
  32. constructor(events = [], failureCount = 0) {
  33. /**
  34. * The number of failures in this run
  35. * @type {number}
  36. */
  37. this.failureCount = failureCount;
  38. /**
  39. * All relevant events emitted from the {@link Runner}.
  40. * @type {SerializableEvent[]}
  41. */
  42. this.events = events;
  43. /**
  44. * Symbol-like value needed to distinguish when attempting to deserialize
  45. * this object (once it's been received over IPC).
  46. * @type {Readonly<"SerializableWorkerResult">}
  47. */
  48. Object.defineProperty(this, '__type', {
  49. value: SERIALIZABLE_RESULT_NAME,
  50. enumerable: true,
  51. writable: false
  52. });
  53. }
  54. /**
  55. * Instantiates a new {@link SerializableWorkerResult}.
  56. * @param {...any} args - Args to constructor
  57. * @returns {SerializableWorkerResult}
  58. */
  59. static create(...args) {
  60. return new SerializableWorkerResult(...args);
  61. }
  62. /**
  63. * Serializes each {@link SerializableEvent} in our `events` prop;
  64. * makes this object read-only.
  65. * @returns {Readonly<SerializableWorkerResult>}
  66. */
  67. serialize() {
  68. this.events.forEach(event => {
  69. event.serialize();
  70. });
  71. return Object.freeze(this);
  72. }
  73. /**
  74. * Deserializes a {@link SerializedWorkerResult} into something reporters can
  75. * use; calls {@link SerializableEvent.deserialize} on each item in its
  76. * `events` prop.
  77. * @param {SerializedWorkerResult} obj
  78. * @returns {SerializedWorkerResult}
  79. */
  80. static deserialize(obj) {
  81. obj.events.forEach(event => {
  82. SerializableEvent.deserialize(event);
  83. });
  84. return obj;
  85. }
  86. /**
  87. * Returns `true` if this is a {@link SerializedWorkerResult} or a
  88. * {@link SerializableWorkerResult}.
  89. * @param {*} value - A value to check
  90. * @returns {boolean} If true, it's deserializable
  91. */
  92. static isSerializedWorkerResult(value) {
  93. return (
  94. value instanceof SerializableWorkerResult ||
  95. (type(value) === 'object' && value.__type === SERIALIZABLE_RESULT_NAME)
  96. );
  97. }
  98. }
  99. /**
  100. * Represents an event, emitted by a {@link Runner}, which is to be transmitted
  101. * over IPC.
  102. *
  103. * Due to the contents of the event data, it's not possible to send them
  104. * verbatim. When received by the main process--and handled by reporters--these
  105. * objects are expected to contain {@link Runnable} instances. This class
  106. * provides facilities to perform the translation via serialization and
  107. * deserialization.
  108. * @private
  109. */
  110. class SerializableEvent {
  111. /**
  112. * Constructs a `SerializableEvent`, throwing if we receive unexpected data.
  113. *
  114. * Practically, events emitted from `Runner` have a minimum of zero (0)
  115. * arguments-- (for example, {@link Runnable.constants.EVENT_RUN_BEGIN}) and a
  116. * maximum of two (2) (for example,
  117. * {@link Runnable.constants.EVENT_TEST_FAIL}, where the second argument is an
  118. * `Error`). The first argument, if present, is a {@link Runnable}. This
  119. * constructor's arguments adhere to this convention.
  120. * @param {string} eventName - A non-empty event name.
  121. * @param {any} [originalValue] - Some data. Corresponds to extra arguments
  122. * passed to `EventEmitter#emit`.
  123. * @param {Error} [originalError] - An error, if there's an error.
  124. * @throws If `eventName` is empty, or `originalValue` is a non-object.
  125. */
  126. constructor(eventName, originalValue, originalError) {
  127. if (!eventName) {
  128. throw createInvalidArgumentTypeError(
  129. 'Empty `eventName` string argument',
  130. 'eventName',
  131. 'string'
  132. );
  133. }
  134. /**
  135. * The event name.
  136. * @memberof SerializableEvent
  137. */
  138. this.eventName = eventName;
  139. const originalValueType = type(originalValue);
  140. if (originalValueType !== 'object' && originalValueType !== 'undefined') {
  141. throw createInvalidArgumentTypeError(
  142. `Expected object but received ${originalValueType}`,
  143. 'originalValue',
  144. 'object'
  145. );
  146. }
  147. /**
  148. * An error, if present.
  149. * @memberof SerializableEvent
  150. */
  151. Object.defineProperty(this, 'originalError', {
  152. value: originalError,
  153. enumerable: false
  154. });
  155. /**
  156. * The raw value.
  157. *
  158. * We don't want this value sent via IPC; making it non-enumerable will do that.
  159. *
  160. * @memberof SerializableEvent
  161. */
  162. Object.defineProperty(this, 'originalValue', {
  163. value: originalValue,
  164. enumerable: false
  165. });
  166. }
  167. /**
  168. * In case you hated using `new` (I do).
  169. *
  170. * @param {...any} args - Args for {@link SerializableEvent#constructor}.
  171. * @returns {SerializableEvent} A new `SerializableEvent`
  172. */
  173. static create(...args) {
  174. return new SerializableEvent(...args);
  175. }
  176. /**
  177. * Used internally by {@link SerializableEvent#serialize}.
  178. * @ignore
  179. * @param {Array<object|string>} pairs - List of parent/key tuples to process; modified in-place. This JSDoc type is an approximation
  180. * @param {object} parent - Some parent object
  181. * @param {string} key - Key to inspect
  182. */
  183. static _serialize(pairs, parent, key) {
  184. let value = parent[key];
  185. let _type = type(value);
  186. if (_type === 'error') {
  187. // we need to reference the stack prop b/c it's lazily-loaded.
  188. // `__type` is necessary for deserialization to create an `Error` later.
  189. // `message` is apparently not enumerable, so we must handle it specifically.
  190. value = Object.assign(Object.create(null), value, {
  191. stack: value.stack,
  192. message: value.message,
  193. __type: 'Error'
  194. });
  195. parent[key] = value;
  196. // after this, set the result of type(value) to be `object`, and we'll throw
  197. // whatever other junk is in the original error into the new `value`.
  198. _type = 'object';
  199. }
  200. switch (_type) {
  201. case 'object':
  202. if (type(value.serialize) === 'function') {
  203. parent[key] = value.serialize();
  204. } else {
  205. // by adding props to the `pairs` array, we will process it further
  206. pairs.push(
  207. ...Object.keys(value)
  208. .filter(key => SERIALIZABLE_TYPES.has(type(value[key])))
  209. .map(key => [value, key])
  210. );
  211. }
  212. break;
  213. case 'function':
  214. // we _may_ want to dig in to functions for some assertion libraries
  215. // that might put a usable property on a function.
  216. // for now, just zap it.
  217. delete parent[key];
  218. break;
  219. case 'array':
  220. pairs.push(
  221. ...value
  222. .filter(value => SERIALIZABLE_TYPES.has(type(value)))
  223. .map((value, index) => [value, index])
  224. );
  225. break;
  226. }
  227. }
  228. /**
  229. * Modifies this object *in place* (for theoretical memory consumption &
  230. * performance reasons); serializes `SerializableEvent#originalValue` (placing
  231. * the result in `SerializableEvent#data`) and `SerializableEvent#error`.
  232. * Freezes this object. The result is an object that can be transmitted over
  233. * IPC.
  234. * If this quickly becomes unmaintainable, we will want to move towards immutable
  235. * objects post-haste.
  236. */
  237. serialize() {
  238. // given a parent object and a key, inspect the value and decide whether
  239. // to replace it, remove it, or add it to our `pairs` array to further process.
  240. // this is recursion in loop form.
  241. const originalValue = this.originalValue;
  242. const result = Object.assign(Object.create(null), {
  243. data:
  244. type(originalValue) === 'object' &&
  245. type(originalValue.serialize) === 'function'
  246. ? originalValue.serialize()
  247. : originalValue,
  248. error: this.originalError
  249. });
  250. // mutates the object
  251. breakCircularDeps(result.error);
  252. const pairs = Object.keys(result).map(key => [result, key]);
  253. const seenPairs = new Set();
  254. let pair;
  255. while ((pair = pairs.shift())) {
  256. if (seenPairs.has(pair[1])) {
  257. continue;
  258. }
  259. seenPairs.add(pair[1]);
  260. SerializableEvent._serialize(pairs, ...pair);
  261. }
  262. this.data = result.data;
  263. this.error = result.error;
  264. return Object.freeze(this);
  265. }
  266. /**
  267. * Used internally by {@link SerializableEvent.deserialize}; creates an `Error`
  268. * from an `Error`-like (serialized) object
  269. * @ignore
  270. * @param {Object} value - An Error-like value
  271. * @returns {Error} Real error
  272. */
  273. static _deserializeError(value) {
  274. const error = new Error(value.message);
  275. error.stack = value.stack;
  276. Object.assign(error, value);
  277. delete error.__type;
  278. return error;
  279. }
  280. /**
  281. * Used internally by {@link SerializableEvent.deserialize}; recursively
  282. * deserializes an object in-place.
  283. * @param {object|Array} parent - Some object or array
  284. * @param {string|number} key - Some prop name or array index within `parent`
  285. */
  286. static _deserializeObject(parent, key) {
  287. if (key === '__proto__') {
  288. delete parent[key];
  289. return;
  290. }
  291. const value = parent[key];
  292. // keys beginning with `$$` are converted into functions returning the value
  293. // and renamed, stripping the `$$` prefix.
  294. // functions defined this way cannot be array members!
  295. if (type(key) === 'string' && key.startsWith('$$')) {
  296. const newKey = key.slice(2);
  297. parent[newKey] = () => value;
  298. delete parent[key];
  299. key = newKey;
  300. }
  301. if (type(value) === 'array') {
  302. value.forEach((_, idx) => {
  303. SerializableEvent._deserializeObject(value, idx);
  304. });
  305. } else if (type(value) === 'object') {
  306. if (value.__type === 'Error') {
  307. parent[key] = SerializableEvent._deserializeError(value);
  308. } else {
  309. Object.keys(value).forEach(key => {
  310. SerializableEvent._deserializeObject(value, key);
  311. });
  312. }
  313. }
  314. }
  315. /**
  316. * Deserialize value returned from a worker into something more useful.
  317. * Does not return the same object.
  318. * @todo do this in a loop instead of with recursion (if necessary)
  319. * @param {SerializedEvent} obj - Object returned from worker
  320. * @returns {SerializedEvent} Deserialized result
  321. */
  322. static deserialize(obj) {
  323. if (!obj) {
  324. throw createInvalidArgumentTypeError('Expected value', obj);
  325. }
  326. obj = Object.assign(Object.create(null), obj);
  327. if (obj.data) {
  328. Object.keys(obj.data).forEach(key => {
  329. SerializableEvent._deserializeObject(obj.data, key);
  330. });
  331. }
  332. if (obj.error) {
  333. obj.error = SerializableEvent._deserializeError(obj.error);
  334. }
  335. return obj;
  336. }
  337. }
  338. /**
  339. * "Serializes" a value for transmission over IPC as a message.
  340. *
  341. * If value is an object and has a `serialize()` method, call that method; otherwise return the object and hope for the best.
  342. *
  343. * @param {*} [value] - A value to serialize
  344. */
  345. exports.serialize = function serialize(value) {
  346. const result =
  347. type(value) === 'object' && type(value.serialize) === 'function'
  348. ? value.serialize()
  349. : value;
  350. debug('serialized: %O', result);
  351. return result;
  352. };
  353. /**
  354. * "Deserializes" a "message" received over IPC.
  355. *
  356. * This could be expanded with other objects that need deserialization,
  357. * but at present time we only care about {@link SerializableWorkerResult} objects.
  358. *
  359. * @param {*} [value] - A "message" to deserialize
  360. */
  361. exports.deserialize = function deserialize(value) {
  362. const result = SerializableWorkerResult.isSerializedWorkerResult(value)
  363. ? SerializableWorkerResult.deserialize(value)
  364. : value;
  365. debug('deserialized: %O', result);
  366. return result;
  367. };
  368. exports.SerializableEvent = SerializableEvent;
  369. exports.SerializableWorkerResult = SerializableWorkerResult;