From 01ab943bfd725b0aa5f9dc4e3dd51bf71f425fdd Mon Sep 17 00:00:00 2001 From: Simen Bekkhus Date: Mon, 28 Dec 2020 14:19:29 +0100 Subject: [PATCH] fix(worker): handle circular messages --- CHANGELOG.md | 1 + .../circularInequality.test.ts.snap | 97 +++++++++++++++++++ e2e/__tests__/circularInequality.test.ts | 67 +++++++++++++ packages/jest-worker/package.json | 3 +- .../src/workers/ChildProcessWorker.ts | 5 +- .../src/workers/NodeThreadsWorker.ts | 5 +- .../jest-worker/src/workers/messageParent.ts | 5 +- .../jest-worker/src/workers/processChild.ts | 5 +- .../jest-worker/src/workers/threadChild.ts | 3 +- packages/jest-worker/src/workers/utils.ts | 34 +++++++ yarn.lock | 91 ++++++++++++++++- 11 files changed, 304 insertions(+), 12 deletions(-) create mode 100644 e2e/__tests__/__snapshots__/circularInequality.test.ts.snap create mode 100644 e2e/__tests__/circularInequality.test.ts create mode 100644 packages/jest-worker/src/workers/utils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index cbe103308c3d..bd87f3ba8f93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ - `[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)) - `[jest-worker]` [**BREAKING**] Use named exports ([#10623] (https://github.com/facebook/jest/pull/10623)) - `[jest-worker]` Do not swallow errors during serialization ([#10984] (https://github.com/facebook/jest/pull/10984)) +- `[jest-worker]` Handle passing messages with circular data ([#10981] (https://github.com/facebook/jest/pull/10981)) - `[pretty-format]` [**BREAKING**] Convert to ES Modules ([#10515](https://github.com/facebook/jest/pull/10515)) ### Chore & Maintenance diff --git a/e2e/__tests__/__snapshots__/circularInequality.test.ts.snap b/e2e/__tests__/__snapshots__/circularInequality.test.ts.snap new file mode 100644 index 000000000000..c3f3647b1996 --- /dev/null +++ b/e2e/__tests__/__snapshots__/circularInequality.test.ts.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`handles circular inequality properly 1`] = ` +FAIL __tests__/test-1.js + ● test + + expect(received).toEqual(expected) // deep equality + + - Expected - 1 + + Received + 3 + + - Object {} + + Object { + + "ref": [Circular], + + } + + 3 | foo.ref = foo; + 4 | + > 5 | expect(foo).toEqual({}); + | ^ + 6 | }); + 7 | + 8 | it('test 2', () => { + + at Object.toEqual (__tests__/test-1.js:5:15) + + ● test 2 + + expect(received).toEqual(expected) // deep equality + + - Expected - 1 + + Received + 3 + + - Object {} + + Object { + + "ref": [Circular], + + } + + 10 | foo.ref = foo; + 11 | + > 12 | expect(foo).toEqual({}); + | ^ + 13 | }); + + at Object.toEqual (__tests__/test-1.js:12:15) + +FAIL __tests__/test-2.js + ● test + + expect(received).toEqual(expected) // deep equality + + - Expected - 1 + + Received + 3 + + - Object {} + + Object { + + "ref": [Circular], + + } + + 3 | foo.ref = foo; + 4 | + > 5 | expect(foo).toEqual({}); + | ^ + 6 | }); + 7 | + 8 | it('test 2', () => { + + at Object.toEqual (__tests__/test-2.js:5:15) + + ● test 2 + + expect(received).toEqual(expected) // deep equality + + - Expected - 1 + + Received + 3 + + - Object {} + + Object { + + "ref": [Circular], + + } + + 10 | foo.ref = foo; + 11 | + > 12 | expect(foo).toEqual({}); + | ^ + 13 | }); + + at Object.toEqual (__tests__/test-2.js:12:15) +`; + +exports[`handles circular inequality properly 2`] = ` +Test Suites: 2 failed, 2 total +Tests: 4 failed, 4 total +Snapshots: 0 total +Time: <> +Ran all test suites. +`; diff --git a/e2e/__tests__/circularInequality.test.ts b/e2e/__tests__/circularInequality.test.ts new file mode 100644 index 000000000000..97bddc73edac --- /dev/null +++ b/e2e/__tests__/circularInequality.test.ts @@ -0,0 +1,67 @@ +/** + * 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 {tmpdir} from 'os'; +import * as path from 'path'; +import {wrap} from 'jest-snapshot-serializer-raw'; +import { + cleanup, + createEmptyPackage, + extractSortedSummary, + writeFiles, +} from '../Utils'; +import {runContinuous} from '../runJest'; + +const tempDir = path.resolve(tmpdir(), 'circular-inequality-test'); + +beforeEach(() => { + createEmptyPackage(tempDir); +}); + +afterEach(() => { + cleanup(tempDir); +}); + +test('handles circular inequality properly', async () => { + const testFileContent = ` + it('test', () => { + const foo = {}; + foo.ref = foo; + + expect(foo).toEqual({}); + }); + + it('test 2', () => { + const foo = {}; + foo.ref = foo; + + expect(foo).toEqual({}); + }); + `; + + writeFiles(tempDir, { + '__tests__/test-1.js': testFileContent, + '__tests__/test-2.js': testFileContent, + }); + + const {end, waitUntil} = runContinuous( + tempDir, + ['--no-watchman', '--watch-all'], + // timeout in case the `waitUntil` below doesn't fire + {stripAnsi: true, timeout: 5000}, + ); + + await waitUntil(({stderr}) => { + return stderr.includes('Ran all test suites.'); + }); + + const {stderr} = await end(); + + const {summary, rest} = extractSortedSummary(stderr); + expect(wrap(rest)).toMatchSnapshot(); + expect(wrap(summary)).toMatchSnapshot(); +}); diff --git a/packages/jest-worker/package.json b/packages/jest-worker/package.json index 35ccb2aab02d..9eb2112ad523 100644 --- a/packages/jest-worker/package.json +++ b/packages/jest-worker/package.json @@ -16,7 +16,8 @@ "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" + "supports-color": "^8.0.0", + "telejson": "^5.1.0" }, "devDependencies": { "@types/merge-stream": "^1.1.2", diff --git a/packages/jest-worker/src/workers/ChildProcessWorker.ts b/packages/jest-worker/src/workers/ChildProcessWorker.ts index 6b7daa4758f2..bf752c23dc4a 100644 --- a/packages/jest-worker/src/workers/ChildProcessWorker.ts +++ b/packages/jest-worker/src/workers/ChildProcessWorker.ts @@ -23,6 +23,7 @@ import { WorkerInterface, WorkerOptions, } from '../types'; +import {parse} from './utils'; const SIGNAL_BASE_EXIT_CODE = 128; const SIGKILL_EXIT_CODE = SIGNAL_BASE_EXIT_CODE + 9; @@ -162,7 +163,7 @@ export default class ChildProcessWorker implements WorkerInterface { switch (response[0]) { case PARENT_MESSAGE_OK: - this._onProcessEnd(null, response[1]); + this._onProcessEnd(null, parse(response[1])); break; case PARENT_MESSAGE_CLIENT_ERROR: @@ -195,7 +196,7 @@ export default class ChildProcessWorker implements WorkerInterface { this._onProcessEnd(error, null); break; case PARENT_MESSAGE_CUSTOM: - this._onCustomMessage(response[1]); + this._onCustomMessage(parse(response[1])); break; default: throw new TypeError('Unexpected response from worker: ' + response[0]); diff --git a/packages/jest-worker/src/workers/NodeThreadsWorker.ts b/packages/jest-worker/src/workers/NodeThreadsWorker.ts index ab7178b89a8e..f58089c5649a 100644 --- a/packages/jest-worker/src/workers/NodeThreadsWorker.ts +++ b/packages/jest-worker/src/workers/NodeThreadsWorker.ts @@ -23,6 +23,7 @@ import { WorkerInterface, WorkerOptions, } from '../types'; +import {parse} from './utils'; export default class ExperimentalWorker implements WorkerInterface { private _worker!: Worker; @@ -141,7 +142,7 @@ export default class ExperimentalWorker implements WorkerInterface { switch (response[0]) { case PARENT_MESSAGE_OK: - this._onProcessEnd(null, response[1]); + this._onProcessEnd(null, parse(response[1])); break; case PARENT_MESSAGE_CLIENT_ERROR: @@ -175,7 +176,7 @@ export default class ExperimentalWorker implements WorkerInterface { this._onProcessEnd(error, null); break; case PARENT_MESSAGE_CUSTOM: - this._onCustomMessage(response[1]); + this._onCustomMessage(parse(response[1])); break; default: throw new TypeError('Unexpected response from worker: ' + response[0]); diff --git a/packages/jest-worker/src/workers/messageParent.ts b/packages/jest-worker/src/workers/messageParent.ts index 0e65c835a6ab..86df6b248e68 100644 --- a/packages/jest-worker/src/workers/messageParent.ts +++ b/packages/jest-worker/src/workers/messageParent.ts @@ -6,6 +6,7 @@ */ import {PARENT_MESSAGE_CUSTOM} from '../types'; +import {serialize} from './utils'; const isWorkerThread: boolean = (() => { try { @@ -30,9 +31,9 @@ export default function messageParent( parentPort, } = require('worker_threads') as typeof import('worker_threads'); // ! is safe due to `null` check in `isWorkerThread` - parentPort!.postMessage([PARENT_MESSAGE_CUSTOM, message]); + parentPort!.postMessage([PARENT_MESSAGE_CUSTOM, serialize(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'); } diff --git a/packages/jest-worker/src/workers/processChild.ts b/packages/jest-worker/src/workers/processChild.ts index 64d29e19e132..d9290329a6f8 100644 --- a/packages/jest-worker/src/workers/processChild.ts +++ b/packages/jest-worker/src/workers/processChild.ts @@ -16,6 +16,7 @@ import { PARENT_MESSAGE_OK, PARENT_MESSAGE_SETUP_ERROR, } from '../types'; +import {serialize} from './utils'; let file: string | null = null; let setupArgs: Array = []; @@ -64,7 +65,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) { @@ -86,7 +87,7 @@ function reportError(error: Error, type: PARENT_MESSAGE_ERROR) { process.send([ type, - error.constructor && error.constructor.name, + error.constructor?.name, error.message, error.stack, typeof error === 'object' ? {...error} : error, diff --git a/packages/jest-worker/src/workers/threadChild.ts b/packages/jest-worker/src/workers/threadChild.ts index 6783ec843510..d0bd386a0414 100644 --- a/packages/jest-worker/src/workers/threadChild.ts +++ b/packages/jest-worker/src/workers/threadChild.ts @@ -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 = []; @@ -65,7 +66,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) { diff --git a/packages/jest-worker/src/workers/utils.ts b/packages/jest-worker/src/workers/utils.ts new file mode 100644 index 000000000000..5f8a5a20bba3 --- /dev/null +++ b/packages/jest-worker/src/workers/utils.ts @@ -0,0 +1,34 @@ +/** + * 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 {parse as parseMessage, stringify} from 'telejson'; + +const customMessageStarter = 'jest string - '; + +export function serialize(data: unknown): unknown { + if (data != null && typeof data === 'object') { + // we only use stringify for the circular objects, not other features + return `${customMessageStarter}${stringify(data, { + allowClass: false, + allowDate: false, + allowFunction: false, + allowRegExp: false, + allowSymbol: false, + allowUndefined: false, + })}`; + } + + return data; +} + +export function parse(data: unknown): unknown { + if (typeof data === 'string' && data.startsWith(customMessageStarter)) { + return parseMessage(data.slice(customMessageStarter.length)); + } + + return data; +} diff --git a/yarn.lock b/yarn.lock index 7d6be98b256b..89fefbf25dc9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3605,6 +3605,13 @@ __metadata: languageName: node linkType: hard +"@types/is-function@npm:^1.0.0": + version: 1.0.0 + resolution: "@types/is-function@npm:1.0.0" + checksum: 6cfa84eac88803fc3fcff37f452fa9fd029f434d6fee75db265b0e67603c781c8fa646b8a1f5056c307ac16313ead1c7f06c884ddee27bba7ec6c0362ce4f516 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.3 resolution: "@types/istanbul-lib-coverage@npm:2.0.3" @@ -7512,6 +7519,13 @@ __metadata: languageName: node linkType: hard +"dom-walk@npm:^0.1.0": + version: 0.1.2 + resolution: "dom-walk@npm:0.1.2" + checksum: 948c7527f3798cae9d7039cc0e5dc9f013ebd701d7d99478bac79d7d9eb8b81e7b6e836526e21ed9b156466b268e30ea0f2d5b72df955fabec3ce2aa7dc0086e + languageName: node + linkType: hard + "domelementtype@npm:1, domelementtype@npm:^1.3.0, domelementtype@npm:^1.3.1": version: 1.3.1 resolution: "domelementtype@npm:1.3.1" @@ -9802,6 +9816,16 @@ fsevents@^1.2.7: languageName: node linkType: hard +"global@npm:^4.4.0": + version: 4.4.0 + resolution: "global@npm:4.4.0" + dependencies: + min-document: ^2.19.0 + process: ^0.11.10 + checksum: da0cf92ef034b63cf4d0fe5e14cb71bc4c748b8c1bbeabe4061443562ba8e9027774f8074e66543fa98f0d965da6d11e0861e3bf8c628b7ab19220e8ee18cc71 + languageName: node + linkType: hard + "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -11045,6 +11069,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"is-function@npm:^1.0.2": + version: 1.0.2 + resolution: "is-function@npm:1.0.2" + checksum: 894562b5e4dcf3544eb0b5c26ba94e08c99007728059782f5e863296e865af9b7d2bcad06057d20bb862943dcfc9bb1387fedb4cdc953af93bd0a70ad61a3ba1 + languageName: node + linkType: hard + "is-generator-fn@npm:^2.0.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" @@ -11320,7 +11351,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"is-symbol@npm:^1.0.2": +"is-symbol@npm:^1.0.2, is-symbol@npm:^1.0.3": version: 1.0.3 resolution: "is-symbol@npm:1.0.3" dependencies: @@ -11451,6 +11482,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"isobject@npm:^4.0.0": + version: 4.0.0 + resolution: "isobject@npm:4.0.0" + checksum: bfc8e8f6e2bebf7d85e4bec91497e24f87e6b33576d03e223ff6ce1679f5a7dc6f357fdb3e1c6c6d85fc6f0feac26acca9e9e7b0ab473ea60fa3f838c203ee01 + languageName: node + linkType: hard + "isomorphic-fetch@npm:^2.1.1": version: 2.2.1 resolution: "isomorphic-fetch@npm:2.2.1" @@ -12334,6 +12372,7 @@ fsevents@^1.2.7: get-stream: ^6.0.0 merge-stream: ^2.0.0 supports-color: ^8.0.0 + telejson: ^5.1.0 worker-farm: ^1.6.0 languageName: unknown linkType: soft @@ -13131,7 +13170,7 @@ fsevents@^1.2.7: languageName: node linkType: hard -"lodash@npm:^4.15.0, lodash@npm:^4.17.12, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.4, lodash@npm:^4.2.1, lodash@npm:^4.3.0, lodash@npm:~4.17.10": +"lodash@npm:^4.15.0, lodash@npm:^4.17.12, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.4, lodash@npm:^4.2.1, lodash@npm:^4.3.0, lodash@npm:~4.17.10": version: 4.17.20 resolution: "lodash@npm:4.17.20" checksum: c62101d2500c383b5f174a7e9e6fe8098149ddd6e9ccfa85f36d4789446195f5c4afd3cfba433026bcaf3da271256566b04a2bf2618e5a39f6e67f8c12030cb6 @@ -13377,6 +13416,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"map-or-similar@npm:^1.5.0": + version: 1.5.0 + resolution: "map-or-similar@npm:1.5.0" + checksum: 3d759eff8025ad5d1e96acc618379f4664be56abdcca1d4f08a8e8df7c3ca6359acbc117611279d972405c0b4347ab28a6724c666dd25006f745bf487dc261d7 + languageName: node + linkType: hard + "map-visit@npm:^1.0.0": version: 1.0.0 resolution: "map-visit@npm:1.0.0" @@ -13459,6 +13505,15 @@ fsevents@^1.2.7: languageName: node linkType: hard +"memoizerific@npm:^1.11.3": + version: 1.11.3 + resolution: "memoizerific@npm:1.11.3" + dependencies: + map-or-similar: ^1.5.0 + checksum: 6601aab4719d269884882b24fc94d33da054817b6472b586dc9117773661abf838f838f5b80d202b8d84f942bcac63421a3044ef31f1af9790eed3f32e33eac6 + languageName: node + linkType: hard + "memory-pager@npm:^1.0.2": version: 1.5.0 resolution: "memory-pager@npm:1.5.0" @@ -14068,6 +14123,15 @@ fsevents@^1.2.7: languageName: node linkType: hard +"min-document@npm:^2.19.0": + version: 2.19.0 + resolution: "min-document@npm:2.19.0" + dependencies: + dom-walk: ^0.1.0 + checksum: 8da883996e00a53729e867dad45a358c6d8b3b55f2473a20768c1a2b4642d0983bc61827cf29eb98c53d7290c2a1a74a5cba60873857da416bdfae09bf73bb21 + languageName: node + linkType: hard + "min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -16235,6 +16299,13 @@ fsevents@^1.2.7: languageName: node linkType: hard +"process@npm:^0.11.10": + version: 0.11.10 + resolution: "process@npm:0.11.10" + checksum: ed93a85e9185b40fb01788c588a87c1a9da0eb925ef7cebebbe1b8bbf0eba1802130366603a29e3b689c116969d4fe018de6aed3474bbeb5aefb3716b85d6449 + languageName: node + linkType: hard + "progress@npm:^2.0.0": version: 2.0.3 resolution: "progress@npm:2.0.3" @@ -18817,6 +18888,22 @@ react-native@0.63.2: languageName: node linkType: hard +"telejson@npm:^5.1.0": + version: 5.1.0 + resolution: "telejson@npm:5.1.0" + dependencies: + "@types/is-function": ^1.0.0 + global: ^4.4.0 + is-function: ^1.0.2 + is-regex: ^1.1.1 + is-symbol: ^1.0.3 + isobject: ^4.0.0 + lodash: ^4.17.20 + memoizerific: ^1.11.3 + checksum: 6e74253262887cfc85098ded1f6cda053a8183fb3885ceda9b56e18a35736aa81ce73204d783a474a0457b1f1b5b1ebd0dd54abb8731de49f4df6515bba4638c + languageName: node + linkType: hard + "temp-dir@npm:^1.0.0": version: 1.0.0 resolution: "temp-dir@npm:1.0.0"