Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support serializing circular refs properly #11467

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
- `[jest-resolver]` [**BREAKING**] Add support for `package.json` `exports` ([11961](https://github.com/facebook/jest/pull/11961))
- `[@jes/schemas]` New module for JSON schemas for Jest's config ([#12384](https://github.com/facebook/jest/pull/12384))
- `[jest-worker]` [**BREAKING**] Allow only absolute `workerPath` ([#12343](https://github.com/facebook/jest/pull/12343))
- `[jest-worker]` Support serializing complex objects ([#11467](https://github.com/facebook/jest/pull/11467))

### Fixes

Expand Down
2 changes: 1 addition & 1 deletion e2e/__tests__/circularInequality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ afterEach(() => {
cleanup(tempDir);
});

test.skip('handles circular inequality properly', async () => {
test('handles circular inequality properly', async () => {
const testFileContent = `
it('test', () => {
const foo = {};
Expand Down
2 changes: 1 addition & 1 deletion e2e/__tests__/complexItemsInErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ test('handles functions that close over outside variables', async () => {
expect(rest).toMatchSnapshot();
});

test.skip('handles errors with BigInt', async () => {
test('handles errors with BigInt', async () => {
const testFileContent = `
test('dummy', () => {
expect(1n).toEqual(2n);
Expand Down
5 changes: 3 additions & 2 deletions packages/jest-worker/src/workers/ChildProcessWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
WorkerInterface,
WorkerOptions,
} from '../types';
import {deserialize} from './utils';

const SIGNAL_BASE_EXIT_CODE = 128;
const SIGKILL_EXIT_CODE = SIGNAL_BASE_EXIT_CODE + 9;
Expand Down Expand Up @@ -164,7 +165,7 @@ export default class ChildProcessWorker implements WorkerInterface {

switch (response[0]) {
case PARENT_MESSAGE_OK:
this._onProcessEnd(null, response[1]);
this._onProcessEnd(null, deserialize(response));
break;

case PARENT_MESSAGE_CLIENT_ERROR:
Expand Down Expand Up @@ -197,7 +198,7 @@ export default class ChildProcessWorker implements WorkerInterface {
this._onProcessEnd(error, null);
break;
case PARENT_MESSAGE_CUSTOM:
this._onCustomMessage(response[1]);
this._onCustomMessage(deserialize(response));
break;
default:
throw new TypeError('Unexpected response from worker: ' + response[0]);
Expand Down
5 changes: 3 additions & 2 deletions packages/jest-worker/src/workers/NodeThreadsWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
WorkerInterface,
WorkerOptions,
} from '../types';
import {deserialize} from './utils';

export default class ExperimentalWorker implements WorkerInterface {
private _worker!: Worker;
Expand Down Expand Up @@ -131,7 +132,7 @@ export default class ExperimentalWorker implements WorkerInterface {

switch (response[0]) {
case PARENT_MESSAGE_OK:
this._onProcessEnd(null, response[1]);
this._onProcessEnd(null, deserialize(response));
break;

case PARENT_MESSAGE_CLIENT_ERROR:
Expand Down Expand Up @@ -165,7 +166,7 @@ export default class ExperimentalWorker implements WorkerInterface {
this._onProcessEnd(error, null);
break;
case PARENT_MESSAGE_CUSTOM:
this._onCustomMessage(response[1]);
this._onCustomMessage(deserialize(response));
break;
default:
throw new TypeError('Unexpected response from worker: ' + response[0]);
Expand Down
21 changes: 17 additions & 4 deletions packages/jest-worker/src/workers/__tests__/processChild.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const processSend = process.send;
const uninitializedParam = {};
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

import {serialize} from 'v8';
import {
CHILD_MESSAGE_CALL,
CHILD_MESSAGE_END,
Expand Down Expand Up @@ -177,7 +178,10 @@ it('returns results immediately when function is synchronous', () => {
[],
]);

expect(process.send.mock.calls[0][0]).toEqual([PARENT_MESSAGE_OK, 1989]);
expect(process.send.mock.calls[0][0]).toEqual([
PARENT_MESSAGE_OK,
{serializedMessage: serialize(1989)},
]);

process.emit('message', [
CHILD_MESSAGE_CALL,
Expand Down Expand Up @@ -258,7 +262,10 @@ it('returns results when it gets resolved if function is asynchronous', async ()

await sleep(10);

expect(process.send.mock.calls[0][0]).toEqual([PARENT_MESSAGE_OK, 1989]);
expect(process.send.mock.calls[0][0]).toEqual([
PARENT_MESSAGE_OK,
{serializedMessage: serialize(1989)},
]);

process.emit('message', [
CHILD_MESSAGE_CALL,
Expand Down Expand Up @@ -294,7 +301,10 @@ it('calls the main module if the method call is "default"', () => {
[],
]);

expect(process.send.mock.calls[0][0]).toEqual([PARENT_MESSAGE_OK, 12345]);
expect(process.send.mock.calls[0][0]).toEqual([
PARENT_MESSAGE_OK,
{serializedMessage: serialize(12345)},
]);
});

it('calls the main export if the method call is "default" and it is a Babel transpiled one', () => {
Expand All @@ -311,7 +321,10 @@ it('calls the main export if the method call is "default" and it is a Babel tran
[],
]);

expect(process.send.mock.calls[0][0]).toEqual([PARENT_MESSAGE_OK, 67890]);
expect(process.send.mock.calls[0][0]).toEqual([
PARENT_MESSAGE_OK,
{serializedMessage: serialize(67890)},
]);
});

it('removes the message listener on END message', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ const mockExtendedError = new ReferenceError('Booo extended');
const uninitializedParam = {};
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

import {serialize} from 'v8';
import {
CHILD_MESSAGE_CALL,
CHILD_MESSAGE_END,
Expand Down Expand Up @@ -201,7 +202,7 @@ it('returns results immediately when function is synchronous', () => {

expect(thread.postMessage.mock.calls[0][0]).toEqual([
PARENT_MESSAGE_OK,
1989,
{serializedMessage: serialize(1989)},
]);

thread.emit('message', [
Expand Down Expand Up @@ -287,7 +288,7 @@ it('returns results when it gets resolved if function is asynchronous', async ()

expect(thread.postMessage.mock.calls[0][0]).toEqual([
PARENT_MESSAGE_OK,
1989,
{serializedMessage: serialize(1989)},
]);

thread.emit('message', [
Expand Down Expand Up @@ -326,7 +327,7 @@ it('calls the main module if the method call is "default"', () => {

expect(thread.postMessage.mock.calls[0][0]).toEqual([
PARENT_MESSAGE_OK,
12345,
{serializedMessage: serialize(12345)},
]);
});

Expand All @@ -346,7 +347,7 @@ it('calls the main export if the method call is "default" and it is a Babel tran

expect(thread.postMessage.mock.calls[0][0]).toEqual([
PARENT_MESSAGE_OK,
67890,
{serializedMessage: serialize(67890)},
]);
});

Expand Down
3 changes: 2 additions & 1 deletion packages/jest-worker/src/workers/messageParent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import {isMainThread, parentPort} from 'worker_threads';
import {PARENT_MESSAGE_CUSTOM} from '../types';
import {serialize} from './utils';

export default function messageParent(
message: unknown,
Expand All @@ -15,7 +16,7 @@ export default function messageParent(
if (!isMainThread && parentPort != null) {
parentPort.postMessage([PARENT_MESSAGE_CUSTOM, message]);
} else if (typeof parentProcess.send === 'function') {
parentProcess.send([PARENT_MESSAGE_CUSTOM, message]);
parentProcess.send([PARENT_MESSAGE_CUSTOM, serialize(message)]);
} else {
throw new Error('"messageParent" can only be used inside a worker');
}
Expand Down
3 changes: 2 additions & 1 deletion packages/jest-worker/src/workers/processChild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
PARENT_MESSAGE_OK,
PARENT_MESSAGE_SETUP_ERROR,
} from '../types';
import {serialize} from './utils';

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

process.send([PARENT_MESSAGE_OK, result]);
process.send([PARENT_MESSAGE_OK, serialize(result)]);
}

function reportClientError(error: Error) {
Expand Down
3 changes: 2 additions & 1 deletion packages/jest-worker/src/workers/threadChild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
PARENT_MESSAGE_OK,
PARENT_MESSAGE_SETUP_ERROR,
} from '../types';
import {serialize} from './utils';

let file: string | null = null;
let setupArgs: Array<unknown> = [];
Expand Down Expand Up @@ -67,7 +68,7 @@ function reportSuccess(result: unknown) {
throw new Error('Child can only be used on a forked process');
}

parentPort!.postMessage([PARENT_MESSAGE_OK, result]);
parentPort!.postMessage([PARENT_MESSAGE_OK, serialize(result)]);
}

function reportClientError(error: Error) {
Expand Down
27 changes: 27 additions & 0 deletions packages/jest-worker/src/workers/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import * as v8 from 'v8';

type SerializedMessage = {serializedMessage: Buffer};
type WorkerResponse = Array<unknown> | [unknown, SerializedMessage];

export const serialize = (message: unknown): SerializedMessage => {
return {serializedMessage: v8.serialize(message)};
};

function hasSerializedMessage(obj: unknown): obj is SerializedMessage {
return obj != null && typeof obj === 'object' && 'serializedMessage' in obj;
}

export const deserialize = ([, re]: WorkerResponse): unknown => {
if (hasSerializedMessage(re)) {
return v8.deserialize(Buffer.from(re.serializedMessage));
}

return re;
};