Skip to content

Commit 6d321d2

Browse files
committed
add some tests
- 100% coverage for `lib/serializer.js` - add a trivial integration test for `--parallel` - fix typo in integration test helper; output spawned command in a copy/pastable format - remove some unused code - rename `deserializeMessage` => `deserialize` - rename `serializeObject` => `serialize` - docstrings for `lib/serializer.js` - rewrite `SerializableEvent.serialize` as a loop instead of recursive function due to possibility of exceeding max stack trace; other refactors - do not freeze objects returned from various `Runnable`'s `serialize()` method, because `SerializableEvent#serialize` needs to mutate them.
1 parent 372edfa commit 6d321d2

File tree

13 files changed

+557
-74
lines changed

13 files changed

+557
-74
lines changed

lib/buffered-runner.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ const Runner = require('./runner');
66
const {EVENT_RUN_BEGIN, EVENT_RUN_END} = Runner.constants;
77
const debug = require('debug')('mocha:buffered-runner');
88
const workerpool = require('workerpool');
9-
const {deserializeMessage} = require('./serializer');
9+
const {deserialize} = require('./serializer');
1010

1111
/**
1212
* This `Runner` delegates tests runs to worker threads. Does not execute any
@@ -48,12 +48,11 @@ class BufferedRunner extends Runner {
4848
this.emit(EVENT_RUN_BEGIN);
4949

5050
const poolProxy = await pool.proxy();
51-
// const tasks = new Set(
5251
const results = await allSettled(
5352
files.map(async file => {
5453
debug('enqueueing test file %s', file);
5554
try {
56-
const {failures, events} = deserializeMessage(
55+
const {failures, events} = deserialize(
5756
await poolProxy.run(file, opts)
5857
);
5958
debug(
@@ -91,6 +90,7 @@ class BufferedRunner extends Runner {
9190

9291
await pool.terminate();
9392

93+
// XXX I'm not sure this is ever non-empty
9494
const uncaughtExceptions = results.filter(
9595
({status}) => status === 'rejected'
9696
);

lib/hook.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ Hook.prototype.error = function(err) {
4646
};
4747

4848
Hook.prototype.serialize = function serialize() {
49-
return Object.freeze({
49+
return {
5050
$$titlePath: this.titlePath(),
5151
ctx: {
5252
currentTest: {
@@ -59,5 +59,5 @@ Hook.prototype.serialize = function serialize() {
5959
},
6060
title: this.title,
6161
type: this.type
62-
});
62+
};
6363
};

lib/serializer.js

Lines changed: 202 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
'use strict';
22

3+
const {type} = require('./utils');
4+
const {createInvalidArgumentTypeError} = require('./errors');
35
// const debug = require('debug')('mocha:serializer');
46

7+
const SERIALIZABLE_RESULT_NAME = 'SerializableWorkerResult';
8+
const SERIALIZABLE_TYPES = new Set(['object', 'array', 'function', 'error']);
9+
510
class SerializableWorkerResult {
611
constructor(failures, events) {
712
this.failures = failures;
813
this.events = events;
9-
this.__type = 'SerializableWorkerResult';
14+
this.__type = SERIALIZABLE_RESULT_NAME;
1015
}
1116

1217
static create(...args) {
@@ -24,90 +29,198 @@ class SerializableWorkerResult {
2429
obj.events.forEach(SerializableEvent.deserialize);
2530
return obj;
2631
}
32+
33+
/**
34+
* Returns `true` if this is a {@link SerializableWorkerResult}, even if serialized
35+
* (in other words, not an instance).
36+
*
37+
* @param {*} value - A value to check
38+
*/
39+
static isSerializableWorkerResult(value) {
40+
return (
41+
type(value) === 'object' && value.__type === SERIALIZABLE_RESULT_NAME
42+
);
43+
}
2744
}
2845

46+
/**
47+
* Represents an event, emitted by a {@link Runner}, which is to be transmitted
48+
* over IPC.
49+
*
50+
* Due to the contents of the event data, it's not possible to send them verbatim.
51+
* When received by the main process--and handled by reporters--these objects are
52+
* expected to contain {@link Runnable} instances. This class provides facilities
53+
* to perform the translation via serialization and deserialization.
54+
*/
2955
class SerializableEvent {
30-
constructor(eventName, rawObject, error) {
56+
/**
57+
* Constructs a `SerializableEvent`, throwing if we receive unexpected data.
58+
*
59+
* Practically, events emitted from `Runner` have a minumum of zero (0) arguments--
60+
* (for example, {@link Runnable.constants.EVENT_RUN_BEGIN}) and a maximum of two (2)
61+
* (for example, {@link Runnable.constants.EVENT_TEST_FAIL}, where the second argument
62+
* is an `Error`). The first argument, if present, is a {@link Runnable}.
63+
* This constructor's arguments adhere to this convention.
64+
* @param {string} eventName - A non-empty event name.
65+
* @param {any} [originalValue] - Some data. Corresponds to extra arguments passed to `EventEmitter#emit`.
66+
* @param {Error} [originalError] - An error, if there's an error.
67+
* @throws If `eventName` is empty, or `originalValue` is a non-object.
68+
*/
69+
constructor(eventName, originalValue, originalError) {
70+
if (!eventName) {
71+
throw new Error('expected a non-empty `eventName` argument');
72+
}
73+
/**
74+
* The event name.
75+
* @memberof SerializableEvent
76+
*/
3177
this.eventName = eventName;
32-
if (rawObject && typeof rawObject !== 'object') {
78+
const originalValueType = type(originalValue);
79+
if (originalValueType !== 'object' && originalValueType !== 'undefined') {
3380
throw new Error(
34-
`expected object, received [${typeof rawObject}]: ${rawObject}`
81+
`expected object, received [${originalValueType}]: ${originalValue}`
3582
);
3683
}
37-
this.error = error;
38-
// we don't want this value sent via IPC.
39-
Object.defineProperty(this, 'rawObject', {
40-
value: rawObject,
84+
/**
85+
* An error, if present.
86+
* @memberof SerializableEvent
87+
*/
88+
Object.defineProperty(this, 'originalError', {
89+
value: originalError,
90+
enumerable: false
91+
});
92+
93+
/**
94+
* The raw value.
95+
*
96+
* We don't want this value sent via IPC; making it non-enumerable will do that.
97+
*
98+
* @memberof SerializableEvent
99+
*/
100+
Object.defineProperty(this, 'originalValue', {
101+
value: originalValue,
41102
enumerable: false
42103
});
43104
}
44105

106+
/**
107+
* In case you hated using `new` (I do).
108+
*
109+
* @param {...any} args - Args for {@link SerializableEvent#constructor}.
110+
* @returns {SerializableEvent} A new `SerializableEvent`
111+
*/
45112
static create(...args) {
46113
return new SerializableEvent(...args);
47114
}
48115

116+
/**
117+
* Modifies this object *in place* (for theoretical memory consumption & performance
118+
* reasons); serializes `SerializableEvent#originalValue` (placing the result in
119+
* `SerializableEvent#data`) and `SerializableEvent#error`. Freezes this object.
120+
* The result is an object that can be transmitted over IPC.
121+
*/
49122
serialize() {
50-
const createError = err => {
51-
const _serializeError = ([value, key]) => {
52-
if (value) {
53-
if (typeof value[key] === 'object') {
54-
const obj = value[key];
55-
Object.keys(obj)
56-
.map(key => [obj[key], key])
57-
.forEach(_serializeError);
58-
} else if (typeof value[key] === 'function') {
59-
delete value[key];
60-
}
61-
}
62-
};
63-
const error = {
64-
message: err.message,
65-
stack: err.stack,
66-
__type: 'Error'
67-
};
68-
69-
Object.keys(err)
70-
.map(key => [err[key], key])
71-
.forEach(_serializeError);
72-
return error;
73-
};
74-
const obj = this.rawObject;
75-
this.data = Object.create(null);
76-
Object.assign(
77-
this.data,
78-
typeof obj.serialize === 'function' ? obj.serialize() : obj
79-
);
80-
Object.keys(this.data).forEach(key => {
81-
if (this.data[key] instanceof Error) {
82-
this.data[key] = createError(this.data[key]);
123+
// list of types within values that we will attempt to serialize
124+
125+
// given a parent object and a key, inspect the value and decide whether
126+
// to replace it, remove it, or add it to our `pairs` array to further process.
127+
// this is recursion in loop form.
128+
const _serialize = (parent, key) => {
129+
let value = parent[key];
130+
switch (type(value)) {
131+
case 'error':
132+
// we need to reference the stack prop b/c it's lazily-loaded.
133+
// `__type` is necessary for deserialization to create an `Error` later.
134+
// fall through to the 'object' branch below to further process & remove
135+
// any junk that an assertion lib may throw in there.
136+
// `message` is apparently not enumerable, so we must handle it specifically.
137+
value = Object.assign(Object.create(null), value, {
138+
stack: value.stack,
139+
message: value.message,
140+
__type: 'Error'
141+
});
142+
parent[key] = value;
143+
// falls through
144+
case 'object':
145+
// by adding props to the `pairs` array, we will process it further
146+
pairs.push(
147+
...Object.keys(value)
148+
.filter(key => SERIALIZABLE_TYPES.has(type(value[key])))
149+
.map(key => [value, key])
150+
);
151+
break;
152+
case 'function':
153+
// we _may_ want to dig in to functions for some assertion libraries
154+
// that might put a usable property on a function.
155+
// for now, just zap it.
156+
delete parent[key];
157+
break;
158+
case 'array':
159+
pairs.push(
160+
...value
161+
.filter(value => SERIALIZABLE_TYPES.has(type(value)))
162+
.map((value, index) => [value, index])
163+
);
164+
break;
83165
}
166+
};
167+
168+
const result = Object.assign(Object.create(null), {
169+
data:
170+
type(this.originalValue) === 'object' &&
171+
type(this.originalValue.serialize) === 'function'
172+
? this.originalValue.serialize()
173+
: this.originalValue,
174+
error: this.originalError
84175
});
85-
if (this.error) {
86-
this.error = createError(this.error);
176+
177+
const pairs = Object.keys(result).map(key => [result, key]);
178+
179+
let pair;
180+
while ((pair = pairs.shift())) {
181+
_serialize(...pair);
87182
}
183+
184+
this.data = result.data;
185+
this.error = result.error;
186+
88187
return Object.freeze(this);
89188
}
90189

190+
/**
191+
* Deserialize value returned from a worker into something more useful.
192+
* Does not return the same object.
193+
* @todo - do this in a loop instead of with recursion (if necessary)
194+
* @param {SerializedEvent} obj - Object returned from worker
195+
* @returns {SerializedEvent} Deserialized result
196+
*/
91197
static deserialize(obj) {
92198
const createError = value => {
93199
const error = new Error(value.message);
94200
error.stack = value.stack;
95201
Object.assign(error, value);
202+
delete error.__type;
96203
return error;
97204
};
98205
const _deserialize = ([object, key]) => {
99-
const value = typeof key !== 'undefined' ? object[key] : object;
100-
if (typeof key === 'string' && key.startsWith('$$')) {
206+
if (key === '__proto__') {
207+
delete object[key];
208+
return;
209+
}
210+
const value = type(key) !== 'undefined' ? object[key] : object;
211+
// keys beginning with `$$` are converted into functions returning the value
212+
// and renamed, stripping the `$$` prefix
213+
if (type(key) === 'string' && key.startsWith('$$')) {
101214
const newKey = key.slice(2);
102215
object[newKey] = () => value;
103216
delete object[key];
104217
key = newKey;
105218
}
106-
if (Array.isArray(value)) {
219+
if (type(value) === 'array') {
107220
value.forEach((_, idx) => {
108221
_deserialize([value, idx]);
109222
});
110-
} else if (value && typeof value === 'object') {
223+
} else if (type(value) === 'object') {
111224
if (value.__type === 'Error') {
112225
object[key] = createError(value);
113226
} else {
@@ -118,27 +231,60 @@ class SerializableEvent {
118231
}
119232
};
120233

121-
Object.keys(obj.data)
122-
.map(key => [obj.data, key])
123-
.forEach(_deserialize);
234+
if (!obj) {
235+
throw createInvalidArgumentTypeError('Expected value', obj);
236+
}
237+
238+
obj = Object.assign(Object.create(null), obj);
239+
240+
if (obj.data) {
241+
Object.keys(obj.data)
242+
.map(key => [obj.data, key])
243+
.forEach(_deserialize);
244+
}
245+
124246
if (obj.error) {
125247
obj.error = createError(obj.error);
126248
}
249+
127250
return obj;
128251
}
129252
}
130253

131-
exports.serializeObject = function serializeObject(obj) {
132-
return obj instanceof SerializableWorkerResult ? obj.serialize() : obj;
254+
/**
255+
* "Serializes" a value for transmission over IPC as a message.
256+
*
257+
* If value is an object and has a `serialize()` method, call that method; otherwise return the object and hope for the best.
258+
*
259+
* @param {*} obj - A value to serialize
260+
*/
261+
exports.serialize = function serialize(value) {
262+
return type(value) === 'object' && type(value.serialize) === 'function'
263+
? value.serialize()
264+
: value;
133265
};
134266

135-
exports.deserializeMessage = function deserializeMessage(message) {
136-
return message &&
137-
typeof message === 'object' &&
138-
message.__type === 'SerializableWorkerResult'
267+
/**
268+
* "Deserializes" a "message" received over IPC.
269+
*
270+
* This could be expanded with other objects that need deserialization,
271+
* but at present time we only care about {@link SerializableWorkerResult} objects.
272+
*
273+
* @param {*} message - A "message" to deserialize
274+
*/
275+
exports.deserialize = function deserialize(message) {
276+
return SerializableWorkerResult.isSerializableWorkerResult(message)
139277
? SerializableWorkerResult.deserialize(message)
140278
: message;
141279
};
142280

143281
exports.SerializableEvent = SerializableEvent;
144282
exports.SerializableWorkerResult = SerializableWorkerResult;
283+
284+
/**
285+
* The result of calling `SerializableEvent.serialize`, as received
286+
* by the deserializer.
287+
* @typedef {Object} SerializedEvent
288+
* @property {object?} data - Optional serialized data
289+
* @property {object?} error - Optional serialized `Error`
290+
*/

lib/suite.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -555,12 +555,12 @@ Suite.prototype.cleanReferences = function cleanReferences() {
555555
* @returns {Object}
556556
*/
557557
Suite.prototype.serialize = function serialize() {
558-
return Object.freeze({
558+
return {
559559
_bail: this._bail,
560560
$$fullTitle: this.fullTitle(),
561561
root: this.root,
562562
title: this.title
563-
});
563+
};
564564
};
565565

566566
var constants = utils.defineConstants(

0 commit comments

Comments
 (0)