Skip to content

Commit 5e8ae67

Browse files
committed
fix(worker): handle circular messages
1 parent c2f152d commit 5e8ae67

11 files changed

Lines changed: 304 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
- `[jest-transform]` [**BREAKING**] Refactor API of transformers to pass an options bag rather than separate `config` and other options ([#10834](https://github.com/facebook/jest/pull/10834))
5151
- `[jest-worker]` [**BREAKING**] Use named exports ([#10623] (https://github.com/facebook/jest/pull/10623))
5252
- `[jest-worker]` Do not swallow errors during serialization ([#10984] (https://github.com/facebook/jest/pull/10984))
53+
- `[jest-worker]` Handle passing messages with circular data ([#10981] (https://github.com/facebook/jest/pull/10981))
5354
- `[pretty-format]` [**BREAKING**] Convert to ES Modules ([#10515](https://github.com/facebook/jest/pull/10515))
5455

5556
### Chore & Maintenance
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`handles circular inequality properly 1`] = `
4+
FAIL __tests__/test-1.js
5+
● test
6+
7+
expect(received).toEqual(expected) // deep equality
8+
9+
- Expected - 1
10+
+ Received + 3
11+
12+
- Object {}
13+
+ Object {
14+
+ "ref": [Circular],
15+
+ }
16+
17+
3 | foo.ref = foo;
18+
4 |
19+
> 5 | expect(foo).toEqual({});
20+
| ^
21+
6 | });
22+
7 |
23+
8 | it('test 2', () => {
24+
25+
at Object.toEqual (__tests__/test-1.js:5:15)
26+
27+
test 2
28+
29+
expect(received).toEqual(expected) // deep equality
30+
31+
- Expected - 1
32+
+ Received + 3
33+
34+
- Object {}
35+
+ Object {
36+
+ "ref": [Circular],
37+
+ }
38+
39+
10 | foo.ref = foo;
40+
11 |
41+
> 12 | expect(foo).toEqual({});
42+
| ^
43+
13 | });
44+
45+
at Object.toEqual (__tests__/test-1.js:12:15)
46+
47+
FAIL __tests__/test-2.js
48+
● test
49+
50+
expect(received).toEqual(expected) // deep equality
51+
52+
- Expected - 1
53+
+ Received + 3
54+
55+
- Object {}
56+
+ Object {
57+
+ "ref": [Circular],
58+
+ }
59+
60+
3 | foo.ref = foo;
61+
4 |
62+
> 5 | expect(foo).toEqual({});
63+
| ^
64+
6 | });
65+
7 |
66+
8 | it('test 2', () => {
67+
68+
at Object.toEqual (__tests__/test-2.js:5:15)
69+
70+
test 2
71+
72+
expect(received).toEqual(expected) // deep equality
73+
74+
- Expected - 1
75+
+ Received + 3
76+
77+
- Object {}
78+
+ Object {
79+
+ "ref": [Circular],
80+
+ }
81+
82+
10 | foo.ref = foo;
83+
11 |
84+
> 12 | expect(foo).toEqual({});
85+
| ^
86+
13 | });
87+
88+
at Object.toEqual (__tests__/test-2.js:12:15)
89+
`;
90+
91+
exports[`handles circular inequality properly 2`] = `
92+
Test Suites: 2 failed, 2 total
93+
Tests: 4 failed, 4 total
94+
Snapshots: 0 total
95+
Time: <<REPLACED>>
96+
Ran all test suites.
97+
`;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {tmpdir} from 'os';
9+
import * as path from 'path';
10+
import {wrap} from 'jest-snapshot-serializer-raw';
11+
import {
12+
cleanup,
13+
createEmptyPackage,
14+
extractSortedSummary,
15+
writeFiles,
16+
} from '../Utils';
17+
import {runContinuous} from '../runJest';
18+
19+
const tempDir = path.resolve(tmpdir(), 'circular-inequality-test');
20+
21+
beforeEach(() => {
22+
createEmptyPackage(tempDir);
23+
});
24+
25+
afterEach(() => {
26+
cleanup(tempDir);
27+
});
28+
29+
test('handles circular inequality properly', async () => {
30+
const testFileContent = `
31+
it('test', () => {
32+
const foo = {};
33+
foo.ref = foo;
34+
35+
expect(foo).toEqual({});
36+
});
37+
38+
it('test 2', () => {
39+
const foo = {};
40+
foo.ref = foo;
41+
42+
expect(foo).toEqual({});
43+
});
44+
`;
45+
46+
writeFiles(tempDir, {
47+
'__tests__/test-1.js': testFileContent,
48+
'__tests__/test-2.js': testFileContent,
49+
});
50+
51+
const {end, waitUntil} = runContinuous(
52+
tempDir,
53+
['--no-watchman', '--watch-all'],
54+
// timeout in case the `waitUntil` below doesn't fire
55+
{stripAnsi: true, timeout: 5000},
56+
);
57+
58+
await waitUntil(({stderr}) => {
59+
return stderr.includes('Ran all test suites.');
60+
});
61+
62+
const {stderr} = await end();
63+
64+
const {summary, rest} = extractSortedSummary(stderr);
65+
expect(wrap(rest)).toMatchSnapshot();
66+
expect(wrap(summary)).toMatchSnapshot();
67+
});

packages/jest-worker/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"dependencies": {
1717
"@types/node": "*",
1818
"merge-stream": "^2.0.0",
19-
"supports-color": "^8.0.0"
19+
"supports-color": "^8.0.0",
20+
"telejson": "^5.1.0"
2021
},
2122
"devDependencies": {
2223
"@types/merge-stream": "^1.1.2",

packages/jest-worker/src/workers/ChildProcessWorker.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
WorkerInterface,
2424
WorkerOptions,
2525
} from '../types';
26+
import {parse} from './utils';
2627

2728
const SIGNAL_BASE_EXIT_CODE = 128;
2829
const SIGKILL_EXIT_CODE = SIGNAL_BASE_EXIT_CODE + 9;
@@ -162,7 +163,7 @@ export default class ChildProcessWorker implements WorkerInterface {
162163

163164
switch (response[0]) {
164165
case PARENT_MESSAGE_OK:
165-
this._onProcessEnd(null, response[1]);
166+
this._onProcessEnd(null, parse(response[1]));
166167
break;
167168

168169
case PARENT_MESSAGE_CLIENT_ERROR:
@@ -195,7 +196,7 @@ export default class ChildProcessWorker implements WorkerInterface {
195196
this._onProcessEnd(error, null);
196197
break;
197198
case PARENT_MESSAGE_CUSTOM:
198-
this._onCustomMessage(response[1]);
199+
this._onCustomMessage(parse(response[1]));
199200
break;
200201
default:
201202
throw new TypeError('Unexpected response from worker: ' + response[0]);

packages/jest-worker/src/workers/NodeThreadsWorker.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
WorkerInterface,
2424
WorkerOptions,
2525
} from '../types';
26+
import {parse} from './utils';
2627

2728
export default class ExperimentalWorker implements WorkerInterface {
2829
private _worker!: Worker;
@@ -141,7 +142,7 @@ export default class ExperimentalWorker implements WorkerInterface {
141142

142143
switch (response[0]) {
143144
case PARENT_MESSAGE_OK:
144-
this._onProcessEnd(null, response[1]);
145+
this._onProcessEnd(null, parse(response[1]));
145146
break;
146147

147148
case PARENT_MESSAGE_CLIENT_ERROR:
@@ -175,7 +176,7 @@ export default class ExperimentalWorker implements WorkerInterface {
175176
this._onProcessEnd(error, null);
176177
break;
177178
case PARENT_MESSAGE_CUSTOM:
178-
this._onCustomMessage(response[1]);
179+
this._onCustomMessage(parse(response[1]));
179180
break;
180181
default:
181182
throw new TypeError('Unexpected response from worker: ' + response[0]);

packages/jest-worker/src/workers/messageParent.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import {PARENT_MESSAGE_CUSTOM} from '../types';
9+
import {serialize} from './utils';
910

1011
const isWorkerThread: boolean = (() => {
1112
try {
@@ -30,9 +31,9 @@ export default function messageParent(
3031
parentPort,
3132
} = require('worker_threads') as typeof import('worker_threads');
3233
// ! is safe due to `null` check in `isWorkerThread`
33-
parentPort!.postMessage([PARENT_MESSAGE_CUSTOM, message]);
34+
parentPort!.postMessage([PARENT_MESSAGE_CUSTOM, serialize(message)]);
3435
} else if (typeof parentProcess.send === 'function') {
35-
parentProcess.send([PARENT_MESSAGE_CUSTOM, message]);
36+
parentProcess.send([PARENT_MESSAGE_CUSTOM, serialize(message)]);
3637
} else {
3738
throw new Error('"messageParent" can only be used inside a worker');
3839
}

packages/jest-worker/src/workers/processChild.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
PARENT_MESSAGE_OK,
1717
PARENT_MESSAGE_SETUP_ERROR,
1818
} from '../types';
19+
import {serialize} from './utils';
1920

2021
let file: string | null = null;
2122
let setupArgs: Array<unknown> = [];
@@ -64,7 +65,7 @@ function reportSuccess(result: unknown) {
6465
throw new Error('Child can only be used on a forked process');
6566
}
6667

67-
process.send([PARENT_MESSAGE_OK, result]);
68+
process.send([PARENT_MESSAGE_OK, serialize(result)]);
6869
}
6970

7071
function reportClientError(error: Error) {
@@ -86,7 +87,7 @@ function reportError(error: Error, type: PARENT_MESSAGE_ERROR) {
8687

8788
process.send([
8889
type,
89-
error.constructor && error.constructor.name,
90+
error.constructor?.name,
9091
error.message,
9192
error.stack,
9293
typeof error === 'object' ? {...error} : error,

packages/jest-worker/src/workers/threadChild.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
PARENT_MESSAGE_OK,
1818
PARENT_MESSAGE_SETUP_ERROR,
1919
} from '../types';
20+
import {serialize} from './utils';
2021

2122
let file: string | null = null;
2223
let setupArgs: Array<unknown> = [];
@@ -65,7 +66,7 @@ function reportSuccess(result: unknown) {
6566
throw new Error('Child can only be used on a forked process');
6667
}
6768

68-
parentPort!.postMessage([PARENT_MESSAGE_OK, result]);
69+
parentPort!.postMessage([PARENT_MESSAGE_OK, serialize(result)]);
6970
}
7071

7172
function reportClientError(error: Error) {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {parse as parseMessage, stringify} from 'telejson';
9+
10+
const customMessageStarter = 'jest string - ';
11+
12+
export function serialize(data: unknown): unknown {
13+
if (data != null && typeof data === 'object') {
14+
// we only use stringify for the circular objects, not other features
15+
return `${customMessageStarter}${stringify(data, {
16+
allowClass: false,
17+
allowDate: false,
18+
allowFunction: false,
19+
allowRegExp: false,
20+
allowSymbol: false,
21+
allowUndefined: false,
22+
})}`;
23+
}
24+
25+
return data;
26+
}
27+
28+
export function parse(data: unknown): unknown {
29+
if (typeof data === 'string' && data.startsWith(customMessageStarter)) {
30+
return parseMessage(data.slice(customMessageStarter.length));
31+
}
32+
33+
return data;
34+
}

0 commit comments

Comments
 (0)