From a2a54a02285b625954e4d8ec1a231712cfaa47bc Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Mon, 21 Aug 2023 23:30:30 +0400 Subject: [PATCH 1/4] fix: Additional error wrapper for `postMessage` to avoid unhandled `DataCloneError` --- .../src/workers/__tests__/threadChild.test.ts | 21 +++++++++++++++++++ .../jest-worker/src/workers/threadChild.ts | 9 +++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/jest-worker/src/workers/__tests__/threadChild.test.ts b/packages/jest-worker/src/workers/__tests__/threadChild.test.ts index 649317919f6f..b7637963969f 100644 --- a/packages/jest-worker/src/workers/__tests__/threadChild.test.ts +++ b/packages/jest-worker/src/workers/__tests__/threadChild.test.ts @@ -365,3 +365,24 @@ it('throws if child is not forked', () => { messagePort.emit('message', [CHILD_MESSAGE_CALL, true, 'fooThrows', []]); }).toThrow('_worker_threads.parentPort.postMessage is not a function'); }); + +it('handle error if `postMessage` throws an error', () => { + messagePort.emit('message', [ + CHILD_MESSAGE_INITIALIZE, + true, + './my-fancy-worker', + ]); + + jest.mocked(messagePort.postMessage).mockImplementationOnce(() => { + throw mockError; + }); + + messagePort.emit('message', [CHILD_MESSAGE_CALL, true, 'fooWorks', []]); + expect(jest.mocked(messagePort.postMessage).mock.calls[1][0]).toEqual([ + PARENT_MESSAGE_CLIENT_ERROR, + 'TypeError', + 'Boo', + mockError.stack, + {}, + ]); +}); diff --git a/packages/jest-worker/src/workers/threadChild.ts b/packages/jest-worker/src/workers/threadChild.ts index 2380e1afd23c..efd31d04ed37 100644 --- a/packages/jest-worker/src/workers/threadChild.ts +++ b/packages/jest-worker/src/workers/threadChild.ts @@ -112,7 +112,14 @@ function reportSuccess(result: unknown) { throw new Error('Child can only be used on a forked process'); } - parentPort!.postMessage([PARENT_MESSAGE_OK, result]); + try { + parentPort!.postMessage([PARENT_MESSAGE_OK, result]); + } catch (err: any) { + // Handling it here to avoid unhandled `DataCloneError` rejection + // which is hard to distinguish on the parent side + // (such error doesn't have any message or stack trace) + reportClientError(err); + } } function reportClientError(error: Error) { From 627031cefe29777db5f4d73660e7791b8db9a2b1 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Tue, 22 Aug 2023 01:37:55 +0400 Subject: [PATCH 2/4] fix: Handle unsafe `structureClone` with `functions` in matchers while using workers --- .../jest-runner/src/__tests__/helpers.test.ts | 34 +++++++++++++++++++ packages/jest-runner/src/helpers.ts | 29 ++++++++++++++++ packages/jest-runner/src/testWorker.ts | 24 ++++++++++++- 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 packages/jest-runner/src/__tests__/helpers.test.ts create mode 100644 packages/jest-runner/src/helpers.ts diff --git a/packages/jest-runner/src/__tests__/helpers.test.ts b/packages/jest-runner/src/__tests__/helpers.test.ts new file mode 100644 index 000000000000..6b2441a2e634 --- /dev/null +++ b/packages/jest-runner/src/__tests__/helpers.test.ts @@ -0,0 +1,34 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + */ + +import {replaceFunctionsWithStringReferences} from '../helpers'; + +it('serialize functions inside the nested object', () => { + const obj = { + foo: () => {}, + nested: { + fn: function bar() { + return 0; + }, + }, + nestedArray: [{baz: function baz() {}}, () => {}], + }; + + expect(replaceFunctionsWithStringReferences(obj)).toEqual({ + foo: '[Function foo]', + nested: { + fn: '[Function bar]', + }, + nestedArray: [ + { + baz: '[Function baz]', + }, + '[Function anonymous]', + ], + }); +}); diff --git a/packages/jest-runner/src/helpers.ts b/packages/jest-runner/src/helpers.ts new file mode 100644 index 000000000000..ca77e354a34c --- /dev/null +++ b/packages/jest-runner/src/helpers.ts @@ -0,0 +1,29 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +export const replaceFunctionsWithStringReferences = (value: T): T => { + if (typeof value === 'function') { + return `[Function ${value.name || 'anonymous'}]` as unknown as T; + } + + if (Array.isArray(value)) { + return value.map(replaceFunctionsWithStringReferences) as T; + } + + const isObject = value !== null && typeof value === 'object'; + if (isObject) { + const oldObject = value as Record; + const newObject: Record = {}; + for (const key of Object.keys(value)) { + newObject[key] = replaceFunctionsWithStringReferences(oldObject[key]); + } + + return newObject as T; + } + + return value; +}; diff --git a/packages/jest-runner/src/testWorker.ts b/packages/jest-runner/src/testWorker.ts index 4d1ee811f8e5..3377b939134a 100644 --- a/packages/jest-runner/src/testWorker.ts +++ b/packages/jest-runner/src/testWorker.ts @@ -18,6 +18,7 @@ import {separateMessageFromStack} from 'jest-message-util'; import type Resolver from 'jest-resolve'; import Runtime from 'jest-runtime'; import {messageParent} from 'jest-worker'; +import {replaceFunctionsWithStringReferences} from './helpers'; import runTest from './runTest'; import type {ErrorWithCode, TestRunnerSerializedContext} from './types'; @@ -92,7 +93,7 @@ export async function worker({ context, }: WorkerData): Promise { try { - return await runTest( + const testResult = await runTest( path, globalConfig, config, @@ -106,7 +107,28 @@ export async function worker({ }, sendMessageToJest, ); + return makeSerializableTestResults(testResult); } catch (error: any) { throw formatError(error); } } + +function makeSerializableTestResults(result: TestResult): TestResult { + const {testResults} = result; + const serializableResults = testResults.map(resultItem => { + if (resultItem.failureDetails) { + return { + ...resultItem, + // Functions cause DataCloneError when passed between workers, + // therefore they are converted to string references + failureDetails: replaceFunctionsWithStringReferences( + resultItem.failureDetails, + ), + }; + } + + return resultItem; + }); + + return {...result, testResults: serializableResults}; +} From 3a3295f67562b871b1ee5e0c1bae098644a417b1 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Tue, 22 Aug 2023 02:05:05 +0400 Subject: [PATCH 3/4] chore: Changelog update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bbc4accd8ca..1b8650a10e4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ - `[expect]` Remove `@types/node` from dependencies ([#14385](https://github.com/jestjs/jest/pull/14385)) - `[jest-core]` Use workers in watch mode by default to avoid crashes ([#14059](https://github.com/facebook/jest/pull/14059) & [#14085](https://github.com/facebook/jest/pull/14085)). - `[jest-reporters]` Update `istanbul-lib-instrument` dependency to v6. ([#14401](https://github.com/jestjs/jest/pull/14401)) +- `[jest-runner]` +- `[jest-worker]` Additional error wrapper for `parentPort.postMessage` to fix unhandled `DataCloneError`. ([#14436](https://github.com/jestjs/jest/pull/14436)) ### Chore & Maintenance From 76791cc71b1971f14faaf04f01bc29b8c1b308c6 Mon Sep 17 00:00:00 2001 From: Ilya Kuznetsov Date: Tue, 22 Aug 2023 02:07:11 +0400 Subject: [PATCH 4/4] chore: Changelog bump --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b8650a10e4c..762bc5c314a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - `[expect]` Remove `@types/node` from dependencies ([#14385](https://github.com/jestjs/jest/pull/14385)) - `[jest-core]` Use workers in watch mode by default to avoid crashes ([#14059](https://github.com/facebook/jest/pull/14059) & [#14085](https://github.com/facebook/jest/pull/14085)). - `[jest-reporters]` Update `istanbul-lib-instrument` dependency to v6. ([#14401](https://github.com/jestjs/jest/pull/14401)) -- `[jest-runner]` +- `[jest-runner]` Handle unsafe worker_threads structureClone with function type in matchers. ([#14436](https://github.com/jestjs/jest/pull/14436)) - `[jest-worker]` Additional error wrapper for `parentPort.postMessage` to fix unhandled `DataCloneError`. ([#14436](https://github.com/jestjs/jest/pull/14436)) ### Chore & Maintenance