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+
510class 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+ */
2955class 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
143281exports . SerializableEvent = SerializableEvent ;
144282exports . 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+ */
0 commit comments