Skip to content

Commit 02c04c4

Browse files
authored
fix: better tracking of seen objects in error serialization (#5032)
* fix: closes #4552 Breaks circular references in before objects are serialized to prevent cryptic error in parallel mode. * chore: trigger test re-run
1 parent 103c56b commit 02c04c4

File tree

4 files changed

+62
-10
lines changed

4 files changed

+62
-10
lines changed

lib/nodejs/serializer.js

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
'use strict';
88

9-
const {type} = require('../utils');
9+
const {type, breakCircularDeps} = require('../utils');
1010
const {createInvalidArgumentTypeError} = require('../errors');
1111
// this is not named `mocha:parallel:serializer` because it's noisy and it's
1212
// helpful to be able to write `DEBUG=mocha:parallel*` and get everything else.
@@ -188,14 +188,9 @@ class SerializableEvent {
188188
* @param {Array<object|string>} pairs - List of parent/key tuples to process; modified in-place. This JSDoc type is an approximation
189189
* @param {object} parent - Some parent object
190190
* @param {string} key - Key to inspect
191-
* @param {WeakSet<Object>} seenObjects - For avoiding circular references
192191
*/
193-
static _serialize(pairs, parent, key, seenObjects) {
192+
static _serialize(pairs, parent, key) {
194193
let value = parent[key];
195-
if (seenObjects.has(value)) {
196-
parent[key] = Object.create(null);
197-
return;
198-
}
199194
let _type = type(value);
200195
if (_type === 'error') {
201196
// we need to reference the stack prop b/c it's lazily-loaded.
@@ -263,13 +258,14 @@ class SerializableEvent {
263258
error: this.originalError
264259
});
265260

261+
// mutates the object
262+
breakCircularDeps(result);
263+
266264
const pairs = Object.keys(result).map(key => [result, key]);
267-
const seenObjects = new WeakSet();
268265

269266
let pair;
270267
while ((pair = pairs.shift())) {
271-
SerializableEvent._serialize(pairs, ...pair, seenObjects);
272-
seenObjects.add(pair[0]);
268+
SerializableEvent._serialize(pairs, ...pair);
273269
}
274270

275271
this.data = result.data;

lib/utils.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -647,3 +647,36 @@ exports.assignNewMochaID = obj => {
647647
*/
648648
exports.getMochaID = obj =>
649649
obj && typeof obj === 'object' ? obj[MOCHA_ID_PROP_NAME] : undefined;
650+
651+
/**
652+
* Replaces any detected circular dependency with the string '[Circular]'
653+
* Mutates original object
654+
* @param inputObj {*}
655+
* @returns {*}
656+
*/
657+
exports.breakCircularDeps = inputObj => {
658+
const seen = new Set();
659+
660+
function _breakCircularDeps(obj) {
661+
if (obj && typeof obj !== 'object') {
662+
return obj;
663+
}
664+
665+
if (seen.has(obj)) {
666+
return '[Circular]';
667+
}
668+
669+
seen.add(obj);
670+
for (const k in obj) {
671+
if (Object.prototype.hasOwnProperty.call(obj, k)) {
672+
obj[k] = _breakCircularDeps(obj[k], k);
673+
}
674+
}
675+
676+
// deleting means only a seen object that is its own child will be detected
677+
seen.delete(obj);
678+
return obj;
679+
}
680+
681+
return _breakCircularDeps(inputObj);
682+
};
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import {describe,it} from "../../../../index.js";
2+
3+
describe('test1', () => {
4+
it('test', () => {
5+
const error = new Error('Foo');
6+
error.foo = { props: [] };
7+
error.foo.props.push(error.foo);
8+
throw error;
9+
});
10+
});

test/integration/parallel.spec.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,17 @@ describe('parallel run', () => {
3030
assert.strictEqual(result.stats.failures, 0);
3131
assert.strictEqual(result.stats.passes, 3);
3232
});
33+
34+
it('should correctly handle circular references in an exception', async () => {
35+
const result = await runMochaJSONAsync('parallel/circular-error.mjs', [
36+
'--parallel',
37+
'--jobs',
38+
'2',
39+
require.resolve('./fixtures/parallel/testworkerid1.mjs')
40+
]);
41+
assert.strictEqual(result.stats.failures, 1);
42+
assert.strictEqual(result.stats.passes, 1);
43+
assert.strictEqual(result.failures[0].err.message, 'Foo');
44+
assert.strictEqual(result.failures[0].err.foo.props[0], '[Circular]');
45+
});
3346
});

0 commit comments

Comments
 (0)