mirror of
https://github.com/hanzoai/jest.git
synced 2026-09-24 16:59:57 +00:00
fix(jest-worker): hangs caused by failed assertions with circular values (#15191)
This commit is contained in:
@@ -82,6 +82,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
|
||||
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
// 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 | });
|
||||
|
||||
at Object.toEqual (__tests__/test-1.js:5: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 | });
|
||||
|
||||
at Object.toEqual (__tests__/test-2.js:5:15)"
|
||||
`;
|
||||
|
||||
exports[`handles circular inequality properly 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
@@ -0,0 +1,533 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`processChild 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[`processChild handles \`BigInt\` 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`processChild 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[`processChild handles \`Map\` 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`processChild 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[`processChild handles \`Symbol\` 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`processChild 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 | });
|
||||
|
||||
at Object.toEqual (__tests__/test-1.js:5: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 | });
|
||||
|
||||
at Object.toEqual (__tests__/test-2.js:5:15)"
|
||||
`;
|
||||
|
||||
exports[`processChild handles circular inequality properly 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`processChild 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[`processChild handles functions 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`processChild 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[`processChild handles mixed structure 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`workerThreads 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[`workerThreads handles \`BigInt\` 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`workerThreads 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[`workerThreads handles \`Map\` 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`workerThreads 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[`workerThreads handles \`Symbol\` 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`workerThreads 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 | });
|
||||
|
||||
at Object.toEqual (__tests__/test-1.js:5: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 | });
|
||||
|
||||
at Object.toEqual (__tests__/test-2.js:5:15)"
|
||||
`;
|
||||
|
||||
exports[`workerThreads handles circular inequality properly 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`workerThreads 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[`workerThreads handles functions 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
|
||||
exports[`workerThreads 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[`workerThreads handles mixed structure 2`] = `
|
||||
"Test Suites: 2 failed, 2 total
|
||||
Tests: 2 failed, 2 total
|
||||
Snapshots: 0 total
|
||||
Time: <<REPLACED>>
|
||||
Ran all test suites."
|
||||
`;
|
||||
@@ -1,57 +0,0 @@
|
||||
/**
|
||||
* 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(), 'circular-inequality-test');
|
||||
|
||||
beforeEach(() => {
|
||||
createEmptyPackage(tempDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(tempDir);
|
||||
});
|
||||
|
||||
test.skip('handles circular inequality properly', async () => {
|
||||
const testFileContent = `
|
||||
it('test', () => {
|
||||
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}) => stderr.includes('Ran all test suites.'));
|
||||
|
||||
const {stderr} = await end();
|
||||
|
||||
const {summary, rest} = extractSortedSummary(stderr);
|
||||
expect(rest).toMatchSnapshot();
|
||||
expect(summary).toMatchSnapshot();
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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,
|
||||
extraOptions: Array<string> = [],
|
||||
) => {
|
||||
writeFiles(tempDir, {
|
||||
'__tests__/test-1.js': testFileContent,
|
||||
'__tests__/test-2.js': testFileContent,
|
||||
});
|
||||
|
||||
const {end, waitUntil} = runContinuous(
|
||||
tempDir,
|
||||
['--no-watchman', '--watch-all', ...extraOptions],
|
||||
// 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);
|
||||
});
|
||||
|
||||
describe.each([
|
||||
{name: 'processChild'},
|
||||
{extraOptions: ['--workerThreads'], name: 'workerThreads'},
|
||||
])('$name', ({extraOptions}) => {
|
||||
test('handles circular inequality properly', async () => {
|
||||
const {summary, rest} = await testIn2Workers(
|
||||
`
|
||||
it('test', () => {
|
||||
const foo = {};
|
||||
foo.ref = foo;
|
||||
|
||||
expect(foo).toEqual({});
|
||||
});
|
||||
`,
|
||||
extraOptions,
|
||||
);
|
||||
expect(rest).toMatchSnapshot();
|
||||
expect(summary).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handles `Map`', async () => {
|
||||
const {summary, rest} = await testIn2Workers(
|
||||
`
|
||||
it('test', () => {
|
||||
expect(new Map([[1, "2"]])).toEqual(new Map([[1, "3"]]));
|
||||
});
|
||||
`,
|
||||
extraOptions,
|
||||
);
|
||||
expect(rest).toMatchSnapshot();
|
||||
expect(summary).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handles `BigInt`', async () => {
|
||||
const {summary, rest} = await testIn2Workers(
|
||||
`
|
||||
it('test', () => {
|
||||
expect(BigInt(42)).toBe(BigInt(73));
|
||||
});
|
||||
`,
|
||||
extraOptions,
|
||||
);
|
||||
expect(rest).toMatchSnapshot();
|
||||
expect(summary).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('handles `Symbol`', async () => {
|
||||
const {summary, rest} = await testIn2Workers(
|
||||
`
|
||||
it('test', () => {
|
||||
expect(Symbol('a')).toEqual(Symbol('b'));
|
||||
});
|
||||
`,
|
||||
extraOptions,
|
||||
);
|
||||
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);
|
||||
});
|
||||
`,
|
||||
extraOptions,
|
||||
);
|
||||
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);
|
||||
});
|
||||
`,
|
||||
extraOptions,
|
||||
);
|
||||
expect(rest).toMatchSnapshot();
|
||||
expect(summary).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
@@ -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:
|
||||
@@ -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:
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
WorkerStates,
|
||||
} from '../types';
|
||||
import WorkerAbstract from './WorkerAbstract';
|
||||
import {unpackMessage} from './safeMessageTransferring';
|
||||
|
||||
export default class ExperimentalWorker
|
||||
extends WorkerAbstract
|
||||
@@ -177,7 +178,7 @@ export default class ExperimentalWorker
|
||||
|
||||
switch (response[0]) {
|
||||
case PARENT_MESSAGE_OK:
|
||||
this._onProcessEnd(null, response[1]);
|
||||
this._onProcessEnd(null, unpackMessage(response[1]));
|
||||
break;
|
||||
|
||||
case PARENT_MESSAGE_CLIENT_ERROR:
|
||||
@@ -213,7 +214,7 @@ export default class ExperimentalWorker
|
||||
break;
|
||||
|
||||
case PARENT_MESSAGE_CUSTOM:
|
||||
this._onCustomMessage(response[1]);
|
||||
this._onCustomMessage(unpackMessage(response[1]));
|
||||
break;
|
||||
|
||||
case PARENT_MESSAGE_MEM_USAGE:
|
||||
|
||||
@@ -20,7 +20,13 @@ 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/safeMessageTransferring',
|
||||
'workers/isDataCloneError',
|
||||
'types',
|
||||
];
|
||||
const writeDestination = join(__dirname, '__temp__');
|
||||
const processChildWorkerPath = join(
|
||||
writeDestination,
|
||||
|
||||
@@ -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,32 @@ 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,
|
||||
);
|
||||
|
||||
const processCallError = spyProcessSend.mock.calls[0][0][1].error;
|
||||
expect(processCallError.self).toBe(processCallError.self.self);
|
||||
});
|
||||
|
||||
it('calls the main module if the method call is "default"', () => {
|
||||
process.emit(
|
||||
'message',
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// https://webidl.spec.whatwg.org/#datacloneerror
|
||||
const DATA_CLONE_ERROR_CODE = 25;
|
||||
|
||||
/**
|
||||
* Unfortunately, [`util.types.isNativeError(value)`](https://nodejs.org/api/util.html#utiltypesisnativeerrorvalue)
|
||||
* return `false` for `DataCloneError` error.
|
||||
* For this reason, try to detect it in this way
|
||||
*/
|
||||
export function isDataCloneError(error: unknown): error is DOMException {
|
||||
return (
|
||||
error != null &&
|
||||
typeof error === 'object' &&
|
||||
'name' in error &&
|
||||
error.name === 'DataCloneError' &&
|
||||
'message' in error &&
|
||||
typeof error.message === 'string' &&
|
||||
'code' in error &&
|
||||
error.code === DATA_CLONE_ERROR_CODE
|
||||
);
|
||||
}
|
||||
@@ -5,17 +5,44 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {types} from 'node:util';
|
||||
import {isMainThread, parentPort} from 'worker_threads';
|
||||
import {PARENT_MESSAGE_CUSTOM} from '../types';
|
||||
import {isDataCloneError} from './isDataCloneError';
|
||||
import {packMessage} from './safeMessageTransferring';
|
||||
|
||||
export default function messageParent(
|
||||
message: unknown,
|
||||
parentProcess = process,
|
||||
): void {
|
||||
if (!isMainThread && parentPort != null) {
|
||||
parentPort.postMessage([PARENT_MESSAGE_CUSTOM, message]);
|
||||
try {
|
||||
parentPort.postMessage([PARENT_MESSAGE_CUSTOM, message]);
|
||||
} catch (error) {
|
||||
// Try to handle https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
|
||||
// for `symbols` and `functions`
|
||||
if (isDataCloneError(error)) {
|
||||
parentPort.postMessage([PARENT_MESSAGE_CUSTOM, packMessage(message)]);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else if (typeof parentProcess.send === 'function') {
|
||||
parentProcess.send([PARENT_MESSAGE_CUSTOM, message]);
|
||||
try {
|
||||
parentProcess.send([PARENT_MESSAGE_CUSTOM, message]);
|
||||
} catch (error) {
|
||||
if (
|
||||
types.isNativeError(error) &&
|
||||
// if .send is a function, it's a serialization issue
|
||||
!error.message.includes('.send is not a function')
|
||||
) {
|
||||
// Apply specific serialization only in error cases
|
||||
// to avoid affecting performance in regular cases.
|
||||
parentProcess.send([PARENT_MESSAGE_CUSTOM, packMessage(message)]);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new TypeError('"messageParent" can only be used inside a worker');
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
import {types} from 'node:util';
|
||||
import {isPromise} from 'jest-util';
|
||||
import {
|
||||
CHILD_MESSAGE_CALL,
|
||||
@@ -21,6 +22,7 @@ import {
|
||||
PARENT_MESSAGE_SETUP_ERROR,
|
||||
type ParentMessageMemUsage,
|
||||
} from '../types';
|
||||
import {packMessage} from './safeMessageTransferring';
|
||||
|
||||
type UnknownFunction = (...args: Array<unknown>) => unknown | Promise<unknown>;
|
||||
|
||||
@@ -97,7 +99,21 @@ 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 (error) {
|
||||
if (
|
||||
types.isNativeError(error) &&
|
||||
// if .send is a function, it's a serialization issue
|
||||
!error.message.includes('.send is not a function')
|
||||
) {
|
||||
// Apply specific serialization only in error cases
|
||||
// to avoid affecting performance in regular cases.
|
||||
process.send([PARENT_MESSAGE_OK, packMessage(result)]);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function reportClientError(error: Error) {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* 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 {
|
||||
type SerializedRecord,
|
||||
deserialize,
|
||||
serialize,
|
||||
} from '@ungap/structured-clone';
|
||||
|
||||
type TransferringContainer = {
|
||||
__STRUCTURED_CLONE_SERIALIZED__: true;
|
||||
data: SerializedRecord;
|
||||
};
|
||||
|
||||
export function packMessage(message: unknown): TransferringContainer {
|
||||
return {
|
||||
__STRUCTURED_CLONE_SERIALIZED__: true,
|
||||
/**
|
||||
* Use the `json: true` option to avoid errors
|
||||
* caused by `function` or `symbol` types.
|
||||
* It's not ideal to lose `function` and `symbol` types,
|
||||
* but reliability is more important.
|
||||
*/
|
||||
data: serialize(message, {json: true}),
|
||||
};
|
||||
}
|
||||
|
||||
function isTransferringContainer(
|
||||
message: unknown,
|
||||
): message is TransferringContainer {
|
||||
return (
|
||||
message != null &&
|
||||
typeof message === 'object' &&
|
||||
'__STRUCTURED_CLONE_SERIALIZED__' in message &&
|
||||
'data' in message
|
||||
);
|
||||
}
|
||||
|
||||
export function unpackMessage(message: unknown): unknown {
|
||||
if (isTransferringContainer(message)) {
|
||||
return deserialize(message.data);
|
||||
}
|
||||
return message;
|
||||
}
|
||||
@@ -22,6 +22,8 @@ import {
|
||||
PARENT_MESSAGE_SETUP_ERROR,
|
||||
type ParentMessageMemUsage,
|
||||
} from '../types';
|
||||
import {isDataCloneError} from './isDataCloneError';
|
||||
import {packMessage} from './safeMessageTransferring';
|
||||
|
||||
type UnknownFunction = (...args: Array<unknown>) => unknown | Promise<unknown>;
|
||||
|
||||
@@ -114,11 +116,21 @@ function reportSuccess(result: unknown) {
|
||||
|
||||
try {
|
||||
parentPort!.postMessage([PARENT_MESSAGE_OK, result]);
|
||||
} catch (error: any) {
|
||||
// Handling it here to avoid unhandled `DataCloneError` rejection
|
||||
} catch (error) {
|
||||
let resolvedError = error;
|
||||
// Try to handle https://html.spec.whatwg.org/multipage/structured-data.html#structuredserializeinternal
|
||||
// for `symbols` and `functions`
|
||||
if (isDataCloneError(error)) {
|
||||
try {
|
||||
parentPort!.postMessage([PARENT_MESSAGE_OK, packMessage(result)]);
|
||||
return;
|
||||
} catch (secondTryError) {
|
||||
resolvedError = secondTryError;
|
||||
}
|
||||
}
|
||||
// Handling it here to avoid unhandled rejection
|
||||
// which is hard to distinguish on the parent side
|
||||
// (such error doesn't have any message or stack trace)
|
||||
reportClientError(error);
|
||||
reportClientError(resolvedError as Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5902,6 +5902,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/ungap__structured-clone@npm:^1.2.0":
|
||||
version: 1.2.0
|
||||
resolution: "@types/ungap__structured-clone@npm:1.2.0"
|
||||
checksum: 5399cfc129ecae2986fbe223fdf8f443c27a628e0d24e1be0fd3d041d643e9a041be54d9b9511fdf36279e7a3cb4c9d72493f30d75fdcde52efa3ac9dc0ebb1e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/unist@npm:*, @types/unist@npm:^3.0.0":
|
||||
version: 3.0.2
|
||||
resolution: "@types/unist@npm:3.0.2"
|
||||
@@ -13711,6 +13718,8 @@ __metadata:
|
||||
"@types/merge-stream": ^1.1.2
|
||||
"@types/node": "*"
|
||||
"@types/supports-color": ^8.1.0
|
||||
"@types/ungap__structured-clone": ^1.2.0
|
||||
"@ungap/structured-clone": ^1.2.0
|
||||
get-stream: ^6.0.0
|
||||
jest-leak-detector: "workspace:*"
|
||||
jest-util: "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user