From 703cfad7ec1471504efa89d7a316d45cf0a5a444 Mon Sep 17 00:00:00 2001 From: Pavel Chertorogov Date: Thu, 2 Nov 2023 21:16:17 +0100 Subject: [PATCH] fix: failed expect with circular references hangs jest worker relates #10577 --- .../workers/__tests__/WorkerEdgeCases.test.ts | 7 +++- .../workers/__tests__/processChild.test.ts | 33 +++++++++++++++++++ .../__tests__/withoutCircularRefs.test.ts | 26 +++++++++++++++ .../jest-worker/src/workers/messageParent.ts | 14 +++++++- .../jest-worker/src/workers/processChild.ts | 11 ++++++- .../src/workers/withoutCircularRefs.ts | 20 +++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 packages/jest-worker/src/workers/__tests__/withoutCircularRefs.test.ts create mode 100644 packages/jest-worker/src/workers/withoutCircularRefs.ts diff --git a/packages/jest-worker/src/workers/__tests__/WorkerEdgeCases.test.ts b/packages/jest-worker/src/workers/__tests__/WorkerEdgeCases.test.ts index 32dd72fbcdeb..15b3292f9c6a 100644 --- a/packages/jest-worker/src/workers/__tests__/WorkerEdgeCases.test.ts +++ b/packages/jest-worker/src/workers/__tests__/WorkerEdgeCases.test.ts @@ -20,7 +20,12 @@ import ThreadsWorker from '../NodeThreadsWorker'; jest.setTimeout(10_000); const root = join('../../'); -const filesToBuild = ['workers/processChild', 'workers/threadChild', 'types']; +const filesToBuild = [ + 'workers/processChild', + 'workers/threadChild', + 'workers/withoutCircularRefs', + 'types', +]; const writeDestination = join(__dirname, '__temp__'); const processChildWorkerPath = join( writeDestination, diff --git a/packages/jest-worker/src/workers/__tests__/processChild.test.ts b/packages/jest-worker/src/workers/__tests__/processChild.test.ts index 09973f5758a6..c4e3992f322a 100644 --- a/packages/jest-worker/src/workers/__tests__/processChild.test.ts +++ b/packages/jest-worker/src/workers/__tests__/processChild.test.ts @@ -41,6 +41,12 @@ beforeEach(() => { mockCount++; return { + fooCircularResult() { + const circular = {self: undefined as unknown}; + circular.self = circular; + return {error: circular}; + }, + fooPromiseThrows() { return new Promise((_resolve, reject) => { setTimeout(() => reject(mockError), 5); @@ -338,6 +344,33 @@ it('returns results when it gets resolved if function is asynchronous', async () expect(spyProcessSend).toHaveBeenCalledTimes(2); }); +it('returns results with circular references', () => { + process.emit( + 'message', + [ + CHILD_MESSAGE_INITIALIZE, + true, // Not really used here, but for type purity. + './my-fancy-worker', + ], + null, + ); + + process.emit( + 'message', + [ + CHILD_MESSAGE_CALL, + true, // Not really used here, but for type purity. + 'fooCircularResult', + [], + ], + null, + ); + + expect(spyProcessSend.mock.calls[0][0][1]).toEqual({ + error: {self: expect.anything()}, + }); +}); + it('calls the main module if the method call is "default"', () => { process.emit( 'message', diff --git a/packages/jest-worker/src/workers/__tests__/withoutCircularRefs.test.ts b/packages/jest-worker/src/workers/__tests__/withoutCircularRefs.test.ts new file mode 100644 index 000000000000..13fc371a4866 --- /dev/null +++ b/packages/jest-worker/src/workers/__tests__/withoutCircularRefs.test.ts @@ -0,0 +1,26 @@ +import {withoutCircularRefs} from '../withoutCircularRefs'; + +it('test simple values', () => { + expect(withoutCircularRefs(undefined)).toBeUndefined(); + expect(withoutCircularRefs(null)).toBeNull(); + expect(withoutCircularRefs(0)).toBe(0); + expect(withoutCircularRefs('12')).toBe('12'); + expect(withoutCircularRefs(true)).toBe(true); + expect(withoutCircularRefs([1])).toEqual([1]); + expect(withoutCircularRefs({a: 1, b: {c: 2}})).toEqual({a: 1, b: {c: 2}}); +}); + +it('test circular values', () => { + const circular = {self: undefined as any}; + circular.self = circular; + + expect(withoutCircularRefs(circular)).toEqual({self: '[Circular]'}); + + expect(withoutCircularRefs([{a: circular, b: null}])).toEqual([ + {a: {self: '[Circular]'}, b: null}, + ]); + + expect(withoutCircularRefs({a: {b: circular}, c: undefined})).toEqual({ + a: {b: {self: '[Circular]'}, c: undefined}, + }); +}); diff --git a/packages/jest-worker/src/workers/messageParent.ts b/packages/jest-worker/src/workers/messageParent.ts index 333dc57ae865..f7c254b7d5bd 100644 --- a/packages/jest-worker/src/workers/messageParent.ts +++ b/packages/jest-worker/src/workers/messageParent.ts @@ -7,6 +7,7 @@ import {isMainThread, parentPort} from 'worker_threads'; import {PARENT_MESSAGE_CUSTOM} from '../types'; +import {withoutCircularRefs} from './withoutCircularRefs'; export default function messageParent( message: unknown, @@ -15,7 +16,18 @@ 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]); + try { + parentProcess.send([PARENT_MESSAGE_CUSTOM, message]); + } catch (e: any) { + if (/circular structure/.test(e?.message)) { + parentProcess.send([ + PARENT_MESSAGE_CUSTOM, + withoutCircularRefs(message), + ]); + } else { + throw e; + } + } } 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 f81c56983692..ad4e92d21306 100644 --- a/packages/jest-worker/src/workers/processChild.ts +++ b/packages/jest-worker/src/workers/processChild.ts @@ -21,6 +21,7 @@ import { PARENT_MESSAGE_SETUP_ERROR, type ParentMessageMemUsage, } from '../types'; +import {withoutCircularRefs} from './withoutCircularRefs'; type UnknownFunction = (...args: Array) => unknown | Promise; @@ -97,7 +98,15 @@ function reportSuccess(result: unknown) { throw new Error('Child can only be used on a forked process'); } - process.send([PARENT_MESSAGE_OK, result]); + try { + process.send([PARENT_MESSAGE_OK, result]); + } catch (e: any) { + if (e && /circular structure/.test(e?.message)) { + process.send([PARENT_MESSAGE_OK, withoutCircularRefs(result)]); + } else { + throw e; + } + } } function reportClientError(error: Error) { diff --git a/packages/jest-worker/src/workers/withoutCircularRefs.ts b/packages/jest-worker/src/workers/withoutCircularRefs.ts new file mode 100644 index 000000000000..3833fc533e7a --- /dev/null +++ b/packages/jest-worker/src/workers/withoutCircularRefs.ts @@ -0,0 +1,20 @@ +export function withoutCircularRefs(obj: unknown): unknown { + const cache = new WeakSet(); + function copy(obj: unknown) { + if (typeof obj !== 'object' || obj === null) { + return obj; + } + if (cache.has(obj)) { + return '[Circular]'; + } + cache.add(obj); + const copyObj: any = Array.isArray(obj) ? [] : {}; + for (const key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) { + copyObj[key] = copy((obj as any)[key]); + } + } + return copyObj; + } + return copy(obj); +}