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

fix(jest-worker): hangs caused by failed assertions with circular values #15191

Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
- [**BREAKING**] `--testPathPattern` is now `--testPathPatterns`
- [**BREAKING**] Specifying `testPathPatterns` when programmatically calling `watch` must be specified as `new TestPathPatterns(patterns)`, where `TestPathPatterns` can be imported from `@jest/pattern`
- `[jest-reporters, jest-runner]` Unhandled errors without stack get correctly logged to console ([#14619](https://github.com/jestjs/jest/pull/14619))
- `[jest-worker]` Properly handle a circular reference error when worker tries to send an assertion fails where either the expected or actual value is circular ([#15191](https://github.com/jestjs/jest/pull/15191))
- `[jest-worker]` Properly handle a BigInt when worker tries to send an assertion fails where either the expected or actual value is BigInt ([#15191](https://github.com/jestjs/jest/pull/15191))

### Performance

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`handles \`BigInt\` 1`] = `
"FAIL __tests__/test-1.js
● test

expect(received).toBe(expected) // Object.is equality

Expected: 73n
Received: 42n

1 | it('test', () => {
> 2 | expect(BigInt(42)).toBe(BigInt(73));
| ^
3 | });

at Object.toBe (__tests__/test-1.js:2:22)

FAIL __tests__/test-2.js
● test

expect(received).toBe(expected) // Object.is equality

Expected: 73n
Received: 42n

1 | it('test', () => {
> 2 | expect(BigInt(42)).toBe(BigInt(73));
| ^
3 | });

at Object.toBe (__tests__/test-2.js:2:22)"
`;

exports[`handles \`BigInt\` 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;

exports[`handles \`Map\` 1`] = `
"FAIL __tests__/test-1.js
● test

expect(received).toEqual(expected) // deep equality

- Expected - 1
+ Received + 1

Map {
- 1 => "3",
+ 1 => "2",
}

1 | it('test', () => {
> 2 | expect(new Map([[1, "2"]])).toEqual(new Map([[1, "3"]]));
| ^
3 | });

at Object.toEqual (__tests__/test-1.js:2:31)

FAIL __tests__/test-2.js
● test

expect(received).toEqual(expected) // deep equality

- Expected - 1
+ Received + 1

Map {
- 1 => "3",
+ 1 => "2",
}

1 | it('test', () => {
> 2 | expect(new Map([[1, "2"]])).toEqual(new Map([[1, "3"]]));
| ^
3 | });

at Object.toEqual (__tests__/test-2.js:2:31)"
`;

exports[`handles \`Map\` 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;

exports[`handles \`Symbol\` 1`] = `
"FAIL __tests__/test-1.js
● test

expect(received).toEqual(expected) // deep equality

Expected: Symbol(b)
Received: Symbol(a)

1 | it('test', () => {
> 2 | expect(Symbol('a')).toEqual(Symbol('b'));
| ^
3 | });

at Object.toEqual (__tests__/test-1.js:2:23)

FAIL __tests__/test-2.js
● test

expect(received).toEqual(expected) // deep equality

Expected: Symbol(b)
Received: Symbol(a)

1 | it('test', () => {
> 2 | expect(Symbol('a')).toEqual(Symbol('b'));
| ^
3 | });

at Object.toEqual (__tests__/test-2.js:2:23)"
`;

exports[`handles \`Symbol\` 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;

exports[`handles functions 1`] = `
"FAIL __tests__/test-1.js
● test

expect(received).toEqual(expected) // deep equality

Expected: [Function fn2]
Received: [Function fn1]

2 | const fn1 = () => {};
3 | const fn2 = () => {};
> 4 | expect(fn1).toEqual(fn2);
| ^
5 | });

at Object.toEqual (__tests__/test-1.js:4:15)

FAIL __tests__/test-2.js
● test

expect(received).toEqual(expected) // deep equality

Expected: [Function fn2]
Received: [Function fn1]

2 | const fn1 = () => {};
3 | const fn2 = () => {};
> 4 | expect(fn1).toEqual(fn2);
| ^
5 | });

at Object.toEqual (__tests__/test-2.js:4:15)"
`;

exports[`handles functions 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;

exports[`handles mixed structure 1`] = `
"FAIL __tests__/test-1.js
● test

expect(received).toEqual(expected) // deep equality

Expected: false
Received: {"bigInt": 42n, "fn": [Function anonymous], "map": Map {1 => "2"}, "ref": [Circular], "symbol": Symbol(asd), Symbol(qwe): Symbol(zxc)}

11 | method() {}
12 | }
> 13 | expect(new Class()).toEqual(false);
| ^
14 | });

at Object.toEqual (__tests__/test-1.js:13:23)

FAIL __tests__/test-2.js
● test

expect(received).toEqual(expected) // deep equality

Expected: false
Received: {"bigInt": 42n, "fn": [Function anonymous], "map": Map {1 => "2"}, "ref": [Circular], "symbol": Symbol(asd), Symbol(qwe): Symbol(zxc)}

11 | method() {}
12 | }
> 13 | expect(new Class()).toEqual(false);
| ^
14 | });

at Object.toEqual (__tests__/test-2.js:13:23)"
`;

exports[`handles mixed structure 2`] = `
"Test Suites: 2 failed, 2 total
Tests: 2 failed, 2 total
Snapshots: 0 total
Time: <<REPLACED>>
Ran all test suites."
`;
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 @@
cleanup(tempDir);
});

test.skip('handles circular inequality properly', async () => {
test('handles circular inequality properly', async () => {
const testFileContent = `
it('test', () => {
const foo = {};
Expand All @@ -47,7 +47,7 @@
{stripAnsi: true, timeout: 5000},
);

await waitUntil(({stderr}) => stderr.includes('Ran all test suites.'));

Check failure on line 50 in e2e/__tests__/circularInequality.test.ts

View workflow job for this annotation

GitHub Actions / Windows with shard 3/4 / Node v16.x

handles circular inequality properly

Process exited at Object.waitUntil (e2e/__tests__/circularInequality.test.ts:50:9)

Check failure on line 50 in e2e/__tests__/circularInequality.test.ts

View workflow job for this annotation

GitHub Actions / Windows with shard 3/4 / Node v18.x

handles circular inequality properly

Process exited at Object.waitUntil (e2e/__tests__/circularInequality.test.ts:50:9)

Check failure on line 50 in e2e/__tests__/circularInequality.test.ts

View workflow job for this annotation

GitHub Actions / Windows with shard 3/4 / Node v20.x

handles circular inequality properly

Process exited at Object.waitUntil (e2e/__tests__/circularInequality.test.ts:50:9)

const {stderr} = await end();

Expand Down
109 changes: 109 additions & 0 deletions e2e/__tests__/nonSerializableStructuresInequality.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* 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 {tmpdir} from 'os';
import * as path from 'path';
import {
cleanup,
createEmptyPackage,
extractSortedSummary,
writeFiles,
} from '../Utils';
import {runContinuous} from '../runJest';

const tempDir = path.resolve(tmpdir(), 'bigint-inequality-test');

const testIn2Workers = async (testFileContent: string) => {
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}) => stderr.includes('Ran all test suites.'));

const {stderr} = await end();

return extractSortedSummary(stderr);
};

beforeEach(() => {
createEmptyPackage(tempDir);
});

afterEach(() => {
cleanup(tempDir);
});

test('handles `Map`', async () => {
const {summary, rest} = await testIn2Workers(`
it('test', () => {
expect(new Map([[1, "2"]])).toEqual(new Map([[1, "3"]]));
});
`);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});

test('handles `BigInt`', async () => {
const {summary, rest} = await testIn2Workers(`
it('test', () => {
expect(BigInt(42)).toBe(BigInt(73));
});
`);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});

test('handles `Symbol`', async () => {
const {summary, rest} = await testIn2Workers(`
it('test', () => {
expect(Symbol('a')).toEqual(Symbol('b'));
});
`);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});

test('handles functions', async () => {
const {summary, rest} = await testIn2Workers(`
it('test', () => {
const fn1 = () => {};
const fn2 = () => {};
expect(fn1).toEqual(fn2);
});
`);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});

test('handles mixed structure', async () => {
const {summary, rest} = await testIn2Workers(`
it('test', () => {
class Class {
constructor() {
this.ref = this;
this.bigInt = BigInt(42);
this.map = new Map([[1, "2"]]);
this.symbol = Symbol('asd');
this[Symbol('qwe')] = Symbol('zxc');
this.fn = () => {};
}
method() {}
}
expect(new Class()).toEqual(false);
});
`);
expect(rest).toMatchSnapshot();
expect(summary).toMatchSnapshot();
});
5 changes: 5 additions & 0 deletions packages/jest-types/src/TestResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export type AssertionResult = {
* was used.
*/
failing?: boolean;
/**
* The raw values of the `function` or `symbol` types will be lost in some cases
* because it's not possible to serialize them correctly between workers.
* However, information about them will be available in the `failureMessages`.
*/
failureDetails: Array<unknown>;
failureMessages: Array<string>;
fullName: string;
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-worker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
},
"dependencies": {
"@types/node": "*",
"@ungap/structured-clone": "^1.2.0",
"jest-util": "workspace:*",
"merge-stream": "^2.0.0",
"supports-color": "^8.0.0"
Expand All @@ -28,6 +29,7 @@
"@babel/core": "^7.11.6",
"@types/merge-stream": "^1.1.2",
"@types/supports-color": "^8.1.0",
"@types/ungap__structured-clone": "^1.2.0",
"get-stream": "^6.0.0",
"jest-leak-detector": "workspace:*",
"worker-farm": "^1.6.0"
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 @@ -27,6 +27,7 @@ import {
WorkerStates,
} from '../types';
import WorkerAbstract from './WorkerAbstract';
import {unpackMessage} from './safeMessageTransferring';

const SIGNAL_BASE_EXIT_CODE = 128;
const SIGKILL_EXIT_CODE = SIGNAL_BASE_EXIT_CODE + 9;
Expand Down Expand Up @@ -263,7 +264,7 @@ export default class ChildProcessWorker

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

case PARENT_MESSAGE_CLIENT_ERROR:
Expand Down Expand Up @@ -297,7 +298,7 @@ export default class ChildProcessWorker
break;

case PARENT_MESSAGE_CUSTOM:
this._onCustomMessage(response[1]);
this._onCustomMessage(unpackMessage(response[1]));
break;

case PARENT_MESSAGE_MEM_USAGE:
Expand Down
Loading
Loading