Skip to content

Commit

Permalink
fix: failed expect with circular references hangs jest worker
Browse files Browse the repository at this point in the history
relates jestjs#10577
  • Loading branch information
Pavel Chertorogov committed Nov 2, 2023
1 parent 544a4b8 commit 703cfad
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 3 deletions.
Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions packages/jest-worker/src/workers/__tests__/processChild.test.ts
Expand Up @@ -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);
Expand Down Expand Up @@ -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',
Expand Down
@@ -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},
});
});
14 changes: 13 additions & 1 deletion packages/jest-worker/src/workers/messageParent.ts
Expand Up @@ -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,
Expand All @@ -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');
}
Expand Down
11 changes: 10 additions & 1 deletion packages/jest-worker/src/workers/processChild.ts
Expand Up @@ -21,6 +21,7 @@ import {
PARENT_MESSAGE_SETUP_ERROR,
type ParentMessageMemUsage,
} from '../types';
import {withoutCircularRefs} from './withoutCircularRefs';

type UnknownFunction = (...args: Array<unknown>) => unknown | Promise<unknown>;

Expand Down Expand Up @@ -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) {
Expand Down
20 changes: 20 additions & 0 deletions 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);
}

0 comments on commit 703cfad

Please sign in to comment.