mirror of https://github.com/facebook/jest.git
Globals cleanup: avoid setting protection symbol when feature is off (#15684)
This commit is contained in:
parent
b00bd3c8ea
commit
33820adab2
|
@ -33,7 +33,7 @@ jobs:
|
||||||
id: cpu-cores
|
id: cpu-cores
|
||||||
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0
|
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0
|
||||||
- name: run node-env tests
|
- name: run node-env tests
|
||||||
run: yarn test-node-env
|
run: yarn workspace jest-environment-node test
|
||||||
- name: run tests
|
- name: run tests
|
||||||
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
|
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
id: cpu-cores
|
id: cpu-cores
|
||||||
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0
|
uses: SimenB/github-actions-cpu-cores@97ba232459a8e02ff6121db9362b09661c875ab8 # v2.0.0
|
||||||
- name: run node-env tests
|
- name: run node-env tests
|
||||||
run: yarn test-node-env
|
run: yarn workspace jest-environment-node test
|
||||||
- name: run tests
|
- name: run tests
|
||||||
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
|
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
|
||||||
with:
|
with:
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
- `[jest-resolver]` Resolve builtin modules correctly ([#15683](https://github.com/jestjs/jest/pull/15683))
|
- `[jest-resolver]` Resolve builtin modules correctly ([#15683](https://github.com/jestjs/jest/pull/15683))
|
||||||
|
- `[jest-environment-node, jest-util]` Avoid setting globals cleanup protection symbol when feature is off ([#15684](https://github.com/jestjs/jest/pull/15684))
|
||||||
|
|
||||||
### Chore & Maintenance
|
### Chore & Maintenance
|
||||||
|
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default {
|
||||||
},
|
},
|
||||||
snapshotSerializers: [require.resolve('jest-serializer-ansi-escapes')],
|
snapshotSerializers: [require.resolve('jest-serializer-ansi-escapes')],
|
||||||
testEnvironmentOptions: {
|
testEnvironmentOptions: {
|
||||||
globalsCleanup: 'on',
|
globalsCleanup: process.env.GLOBALS_CLEANUP ?? 'on',
|
||||||
},
|
},
|
||||||
testPathIgnorePatterns: [
|
testPathIgnorePatterns: [
|
||||||
'/__arbitraries__/',
|
'/__arbitraries__/',
|
||||||
|
|
|
@ -113,7 +113,6 @@
|
||||||
"test-ts": "yarn jest --config jest.config.ts.mjs",
|
"test-ts": "yarn jest --config jest.config.ts.mjs",
|
||||||
"test-types": "yarn tstyche",
|
"test-types": "yarn tstyche",
|
||||||
"test-with-type-info": "yarn jest e2e/__tests__/jest.config.ts.test.ts",
|
"test-with-type-info": "yarn jest e2e/__tests__/jest.config.ts.test.ts",
|
||||||
"test-node-env": "yarn jest packages/jest-environment-node/src/__tests__",
|
|
||||||
"test": "yarn lint && yarn jest",
|
"test": "yarn lint && yarn jest",
|
||||||
"typecheck": "yarn typecheck:examples && yarn typecheck:tests",
|
"typecheck": "yarn typecheck:examples && yarn typecheck:tests",
|
||||||
"typecheck:examples": "tsc -p examples/expect-extend && tsc -p examples/typescript",
|
"typecheck:examples": "tsc -p examples/expect-extend && tsc -p examples/typescript",
|
||||||
|
|
|
@ -31,6 +31,13 @@
|
||||||
"@jest/test-utils": "workspace:*",
|
"@jest/test-utils": "workspace:*",
|
||||||
"clsx": "^2.1.1"
|
"clsx": "^2.1.1"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test:base": "echo GLOBALS_CLEANUP=$GLOBALS_CLEANUP && yarn --cwd='../.' jest --runInBand packages/jest-environment-node/src/__tests__",
|
||||||
|
"test:globals-cleanup-off": "GLOBALS_CLEANUP=off yarn test:base",
|
||||||
|
"test:globals-cleanup-soft": "GLOBALS_CLEANUP=soft yarn test:base",
|
||||||
|
"test:globals-cleanup-on": "GLOBALS_CLEANUP=on yarn test:base",
|
||||||
|
"test": "yarn test:globals-cleanup-off && yarn test:globals-cleanup-soft && yarn test:globals-cleanup-on"
|
||||||
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,7 +9,7 @@ import {AsyncLocalStorage, createHook} from 'async_hooks';
|
||||||
import {clsx} from 'clsx';
|
import {clsx} from 'clsx';
|
||||||
import {onNodeVersions} from '@jest/test-utils';
|
import {onNodeVersions} from '@jest/test-utils';
|
||||||
|
|
||||||
describe('NodeEnvironment 2', () => {
|
describe('Globals Cleanup 1', () => {
|
||||||
test('dispatch event', () => {
|
test('dispatch event', () => {
|
||||||
new EventTarget().dispatchEvent(new Event('foo'));
|
new EventTarget().dispatchEvent(new Event('foo'));
|
||||||
});
|
});
|
|
@ -0,0 +1,36 @@
|
||||||
|
/**
|
||||||
|
* 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 {AsyncLocalStorage, createHook} from 'async_hooks';
|
||||||
|
import {clsx} from 'clsx';
|
||||||
|
import {onNodeVersions} from '@jest/test-utils';
|
||||||
|
|
||||||
|
describe('Globals Cleanup 2', () => {
|
||||||
|
test('dispatch event', () => {
|
||||||
|
new EventTarget().dispatchEvent(new Event('foo'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('set modules on global', () => {
|
||||||
|
(globalThis as any).async_hooks = require('async_hooks');
|
||||||
|
(globalThis as any).AsyncLocalStorage =
|
||||||
|
require('async_hooks').AsyncLocalStorage;
|
||||||
|
(globalThis as any).createHook = require('async_hooks').createHook;
|
||||||
|
(globalThis as any).clsx = require('clsx');
|
||||||
|
expect(AsyncLocalStorage).toBeDefined();
|
||||||
|
expect(clsx).toBeDefined();
|
||||||
|
expect(createHook).toBeDefined();
|
||||||
|
expect(createHook({})).toBeDefined();
|
||||||
|
expect(clsx()).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
onNodeVersions('>=19.8.0', () => {
|
||||||
|
test('use static function from core module set on global', () => {
|
||||||
|
expect(AsyncLocalStorage.snapshot).toBeDefined();
|
||||||
|
expect(AsyncLocalStorage.snapshot()).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,26 @@
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function onlyIfGlobalsCleanup(
|
||||||
|
globalsCleanup: string,
|
||||||
|
testBody: () => void,
|
||||||
|
): void {
|
||||||
|
const describeFunc =
|
||||||
|
process.env.GLOBALS_CLEANUP === globalsCleanup ? describe : describe.skip;
|
||||||
|
describeFunc(`GLOBALS_CLEANUP=${globalsCleanup}`, testBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Globals Cleanup 3', () => {
|
||||||
|
onlyIfGlobalsCleanup('off', () => {
|
||||||
|
test('assign Object prototype descriptors to a new empty object', () => {
|
||||||
|
const descriptors = Object.getOwnPropertyDescriptors(
|
||||||
|
Object.getPrototypeOf({}),
|
||||||
|
);
|
||||||
|
Object.assign({}, descriptors);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -6,14 +6,8 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {EnvironmentContext} from '@jest/environment';
|
import type {EnvironmentContext} from '@jest/environment';
|
||||||
import {
|
import {makeGlobalConfig, makeProjectConfig} from '@jest/test-utils';
|
||||||
makeGlobalConfig,
|
|
||||||
makeProjectConfig,
|
|
||||||
onNodeVersions,
|
|
||||||
} from '@jest/test-utils';
|
|
||||||
import NodeEnvironment from '../';
|
import NodeEnvironment from '../';
|
||||||
import {AsyncLocalStorage, createHook} from 'async_hooks';
|
|
||||||
import {clsx} from 'clsx';
|
|
||||||
|
|
||||||
const context: EnvironmentContext = {
|
const context: EnvironmentContext = {
|
||||||
console,
|
console,
|
||||||
|
@ -93,28 +87,4 @@ describe('NodeEnvironment', () => {
|
||||||
test('TextEncoder references the same global Uint8Array constructor', () => {
|
test('TextEncoder references the same global Uint8Array constructor', () => {
|
||||||
expect(new TextEncoder().encode('abc')).toBeInstanceOf(Uint8Array);
|
expect(new TextEncoder().encode('abc')).toBeInstanceOf(Uint8Array);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('dispatch event', () => {
|
|
||||||
new EventTarget().dispatchEvent(new Event('foo'));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('set modules on global', () => {
|
|
||||||
(globalThis as any).async_hooks = require('async_hooks');
|
|
||||||
(globalThis as any).AsyncLocalStorage =
|
|
||||||
require('async_hooks').AsyncLocalStorage;
|
|
||||||
(globalThis as any).createHook = require('async_hooks').createHook;
|
|
||||||
(globalThis as any).clsx = require('clsx');
|
|
||||||
expect(AsyncLocalStorage).toBeDefined();
|
|
||||||
expect(clsx).toBeDefined();
|
|
||||||
expect(createHook).toBeDefined();
|
|
||||||
expect(createHook({})).toBeDefined();
|
|
||||||
expect(clsx()).toBeDefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
onNodeVersions('>=19.8.0', () => {
|
|
||||||
test('use static function from core module set on global', () => {
|
|
||||||
expect(AsyncLocalStorage.snapshot).toBeDefined();
|
|
||||||
expect(AsyncLocalStorage.snapshot()).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
@ -12,12 +12,13 @@ import type {
|
||||||
JestEnvironmentConfig,
|
JestEnvironmentConfig,
|
||||||
} from '@jest/environment';
|
} from '@jest/environment';
|
||||||
import {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers';
|
import {LegacyFakeTimers, ModernFakeTimers} from '@jest/fake-timers';
|
||||||
import type {Global} from '@jest/types';
|
import type {Config, Global} from '@jest/types';
|
||||||
import {ModuleMocker} from 'jest-mock';
|
import {ModuleMocker} from 'jest-mock';
|
||||||
import {
|
import {
|
||||||
type DeletionMode,
|
type DeletionMode,
|
||||||
canDeleteProperties,
|
canDeleteProperties,
|
||||||
deleteProperties,
|
deleteProperties,
|
||||||
|
initializeGarbageCollectionUtils,
|
||||||
installCommonGlobals,
|
installCommonGlobals,
|
||||||
protectProperties,
|
protectProperties,
|
||||||
} from 'jest-util';
|
} from 'jest-util';
|
||||||
|
@ -88,11 +89,14 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
|
||||||
customExportConditions = ['node', 'node-addons'];
|
customExportConditions = ['node', 'node-addons'];
|
||||||
private readonly _configuredExportConditions?: Array<string>;
|
private readonly _configuredExportConditions?: Array<string>;
|
||||||
private _globalProxy: GlobalProxy;
|
private _globalProxy: GlobalProxy;
|
||||||
private _globalsCleanup: DeletionMode;
|
|
||||||
|
|
||||||
// while `context` is unused, it should always be passed
|
// while `context` is unused, it should always be passed
|
||||||
constructor(config: JestEnvironmentConfig, _context: EnvironmentContext) {
|
constructor(config: JestEnvironmentConfig, _context: EnvironmentContext) {
|
||||||
const {projectConfig} = config;
|
const {projectConfig} = config;
|
||||||
|
|
||||||
|
const globalsCleanupMode = readGlobalsCleanupConfig(projectConfig);
|
||||||
|
initializeGarbageCollectionUtils(globalThis, globalsCleanupMode);
|
||||||
|
|
||||||
this._globalProxy = new GlobalProxy();
|
this._globalProxy = new GlobalProxy();
|
||||||
this.context = createContext(this._globalProxy.proxy());
|
this.context = createContext(this._globalProxy.proxy());
|
||||||
const global = runInContext(
|
const global = runInContext(
|
||||||
|
@ -160,7 +164,7 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
|
||||||
// same constructor is referenced by both.
|
// same constructor is referenced by both.
|
||||||
global.Uint8Array = Uint8Array;
|
global.Uint8Array = Uint8Array;
|
||||||
|
|
||||||
installCommonGlobals(global, projectConfig.globals);
|
installCommonGlobals(global, projectConfig.globals, globalsCleanupMode);
|
||||||
|
|
||||||
if ('asyncDispose' in Symbol && !('asyncDispose' in global.Symbol)) {
|
if ('asyncDispose' in Symbol && !('asyncDispose' in global.Symbol)) {
|
||||||
const globalSymbol = global.Symbol as unknown as SymbolConstructor;
|
const globalSymbol = global.Symbol as unknown as SymbolConstructor;
|
||||||
|
@ -206,26 +210,6 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
|
||||||
});
|
});
|
||||||
|
|
||||||
this._globalProxy.envSetupCompleted();
|
this._globalProxy.envSetupCompleted();
|
||||||
this._globalsCleanup = (() => {
|
|
||||||
const rawConfig = projectConfig.testEnvironmentOptions.globalsCleanup;
|
|
||||||
const config = rawConfig?.toString()?.toLowerCase();
|
|
||||||
switch (config) {
|
|
||||||
case 'off':
|
|
||||||
case 'on':
|
|
||||||
case 'soft':
|
|
||||||
return config;
|
|
||||||
default: {
|
|
||||||
if (config !== undefined) {
|
|
||||||
logValidationWarning(
|
|
||||||
'testEnvironmentOptions.globalsCleanup',
|
|
||||||
`Unknown value given: ${rawConfig}`,
|
|
||||||
'Available options are: [on, soft, off]',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return 'soft';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
@ -241,9 +225,7 @@ export default class NodeEnvironment implements JestEnvironment<Timer> {
|
||||||
this.context = null;
|
this.context = null;
|
||||||
this.fakeTimers = null;
|
this.fakeTimers = null;
|
||||||
this.fakeTimersModern = null;
|
this.fakeTimersModern = null;
|
||||||
if (this._globalsCleanup !== 'off') {
|
this._globalProxy.clear();
|
||||||
this._globalProxy.clear(this._globalsCleanup);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
exportConditions(): Array<string> {
|
exportConditions(): Array<string> {
|
||||||
|
@ -293,10 +275,8 @@ class GlobalProxy implements ProxyHandler<typeof globalThis> {
|
||||||
* Deletes any property that was set on the global object, except for:
|
* Deletes any property that was set on the global object, except for:
|
||||||
* 1. Properties that were set before {@link #envSetupCompleted} was invoked.
|
* 1. Properties that were set before {@link #envSetupCompleted} was invoked.
|
||||||
* 2. Properties protected by {@link #protectProperties}.
|
* 2. Properties protected by {@link #protectProperties}.
|
||||||
*
|
|
||||||
* @param mode determines whether to soft or hard delete the properties.
|
|
||||||
*/
|
*/
|
||||||
clear(mode: DeletionMode): void {
|
clear(): void {
|
||||||
for (const {value} of [
|
for (const {value} of [
|
||||||
...[...this.propertyToValue.entries()].map(([property, value]) => ({
|
...[...this.propertyToValue.entries()].map(([property, value]) => ({
|
||||||
property,
|
property,
|
||||||
|
@ -304,7 +284,7 @@ class GlobalProxy implements ProxyHandler<typeof globalThis> {
|
||||||
})),
|
})),
|
||||||
...this.leftovers,
|
...this.leftovers,
|
||||||
]) {
|
]) {
|
||||||
deleteProperties(value, mode);
|
deleteProperties(value);
|
||||||
}
|
}
|
||||||
this.propertyToValue.clear();
|
this.propertyToValue.clear();
|
||||||
this.leftovers = [];
|
this.leftovers = [];
|
||||||
|
@ -365,3 +345,26 @@ class GlobalProxy implements ProxyHandler<typeof globalThis> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readGlobalsCleanupConfig(
|
||||||
|
projectConfig: Config.ProjectConfig,
|
||||||
|
): DeletionMode {
|
||||||
|
const rawConfig = projectConfig.testEnvironmentOptions.globalsCleanup;
|
||||||
|
const config = rawConfig?.toString()?.toLowerCase();
|
||||||
|
switch (config) {
|
||||||
|
case 'off':
|
||||||
|
case 'on':
|
||||||
|
case 'soft':
|
||||||
|
return config;
|
||||||
|
default: {
|
||||||
|
if (config !== undefined) {
|
||||||
|
logValidationWarning(
|
||||||
|
'testEnvironmentOptions.globalsCleanup',
|
||||||
|
`Unknown value given: ${rawConfig}`,
|
||||||
|
'Available options are: [on, soft, off]',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return 'soft';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -4,27 +4,69 @@
|
||||||
* This source code is licensed under the MIT license found in the
|
* This source code is licensed under the MIT license found in the
|
||||||
* LICENSE file in the root directory of this source tree.
|
* LICENSE file in the root directory of this source tree.
|
||||||
*/
|
*/
|
||||||
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The symbol that is set on the global object to store the deletion mode.
|
||||||
|
*/
|
||||||
|
const DELETION_MODE_SYMBOL = Symbol.for('$$jest-deletion-mode');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The symbol that is set on objects to protect them from deletion.
|
||||||
|
*
|
||||||
|
* If the value is an empty array, then all properties will be protected.
|
||||||
|
* If the value is an array of strings or symbols, then only those properties will be protected.
|
||||||
|
*/
|
||||||
const PROTECT_SYMBOL = Symbol.for('$$jest-protect-from-deletion');
|
const PROTECT_SYMBOL = Symbol.for('$$jest-protect-from-deletion');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - <b>off</b>: deletion is completely turned off.
|
||||||
|
* - <b>soft</b>: doesn't delete objects, but instead wraps their getter/setter with a deprecation warning.
|
||||||
|
* - <b>on</b>: actually delete objects (using `delete`).
|
||||||
|
*/
|
||||||
export type DeletionMode = 'soft' | 'off' | 'on';
|
export type DeletionMode = 'soft' | 'off' | 'on';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes the garbage collection utils with the given deletion mode.
|
||||||
|
*
|
||||||
|
* @param globalObject the global object on which to store the deletion mode.
|
||||||
|
* @param deletionMode the deletion mode to use.
|
||||||
|
*/
|
||||||
|
export function initializeGarbageCollectionUtils(
|
||||||
|
globalObject: typeof globalThis,
|
||||||
|
deletionMode: DeletionMode,
|
||||||
|
): void {
|
||||||
|
const currentMode = Reflect.get(globalObject, DELETION_MODE_SYMBOL);
|
||||||
|
if (currentMode && currentMode !== deletionMode) {
|
||||||
|
console.warn(
|
||||||
|
chalk.yellow(
|
||||||
|
[
|
||||||
|
'[jest-util] garbage collection deletion mode already initialized, ignoring new mode',
|
||||||
|
` Current: '${currentMode}'`,
|
||||||
|
` Given: '${deletionMode}'`,
|
||||||
|
].join('\n'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Reflect.set(globalObject, DELETION_MODE_SYMBOL, deletionMode);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes all the properties from the given value (if it's an object),
|
* Deletes all the properties from the given value (if it's an object),
|
||||||
* unless the value was protected via {@link #protectProperties}.
|
* unless the value was protected via {@link #protectProperties}.
|
||||||
*
|
*
|
||||||
* @param value the given value.
|
* @param value the given value.
|
||||||
* @param mode the deletion mode (see {@link #deleteProperty}).
|
|
||||||
*/
|
*/
|
||||||
export function deleteProperties(value: unknown, mode: DeletionMode): void {
|
export function deleteProperties(value: unknown): void {
|
||||||
if (canDeleteProperties(value)) {
|
if (getDeletionMode() !== 'off' && canDeleteProperties(value)) {
|
||||||
const protectedKeys = getProtectedKeys(
|
const protectedKeys = getProtectedKeys(
|
||||||
value,
|
value,
|
||||||
Reflect.get(value, PROTECT_SYMBOL),
|
Reflect.get(value, PROTECT_SYMBOL),
|
||||||
);
|
);
|
||||||
for (const key of Reflect.ownKeys(value)) {
|
for (const key of Reflect.ownKeys(value)) {
|
||||||
if (!protectedKeys.includes(key) && key !== PROTECT_SYMBOL) {
|
if (!protectedKeys.includes(key) && key !== PROTECT_SYMBOL) {
|
||||||
deleteProperty(value, key, mode);
|
deleteProperty(value, key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,8 +88,11 @@ export function protectProperties<T>(
|
||||||
properties: Array<keyof T> = [],
|
properties: Array<keyof T> = [],
|
||||||
depth = 2,
|
depth = 2,
|
||||||
): boolean {
|
): boolean {
|
||||||
// Reflect.get may cause deprecation warnings, so we disable them temporarily
|
if (getDeletionMode() === 'off') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reflect.get may cause deprecation warnings, so we disable them temporarily
|
||||||
const originalEmitWarning = process.emitWarning;
|
const originalEmitWarning = process.emitWarning;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -105,17 +150,13 @@ export function canDeleteProperties(value: unknown): value is object {
|
||||||
*
|
*
|
||||||
* @returns whether the deletion was successful or not.
|
* @returns whether the deletion was successful or not.
|
||||||
*/
|
*/
|
||||||
function deleteProperty(
|
function deleteProperty(obj: object, key: string | symbol): boolean {
|
||||||
obj: object,
|
|
||||||
key: string | symbol,
|
|
||||||
mode: DeletionMode,
|
|
||||||
): boolean {
|
|
||||||
const descriptor = Reflect.getOwnPropertyDescriptor(obj, key);
|
const descriptor = Reflect.getOwnPropertyDescriptor(obj, key);
|
||||||
if (!descriptor?.configurable) {
|
if (!descriptor?.configurable) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === 'on') {
|
if (getDeletionMode() === 'on') {
|
||||||
return Reflect.deleteProperty(obj, key);
|
return Reflect.deleteProperty(obj, key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -137,6 +178,10 @@ function deleteProperty(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getDeletionMode(): DeletionMode {
|
||||||
|
return Reflect.get(globalThis, DELETION_MODE_SYMBOL) ?? 'off';
|
||||||
|
}
|
||||||
|
|
||||||
const warningCache = new WeakSet<object>();
|
const warningCache = new WeakSet<object>();
|
||||||
|
|
||||||
function emitAccessWarning(obj: object, key: string | symbol): void {
|
function emitAccessWarning(obj: object, key: string | symbol): void {
|
||||||
|
@ -153,7 +198,7 @@ function emitAccessWarning(obj: object, key: string | symbol): void {
|
||||||
detail: [
|
detail: [
|
||||||
'Jest deletes objects that were set on the global scope between test files to reduce memory leaks.',
|
'Jest deletes objects that were set on the global scope between test files to reduce memory leaks.',
|
||||||
'Currently it only "soft" deletes them and emits this warning if those objects were accessed after their deletion.',
|
'Currently it only "soft" deletes them and emits this warning if those objects were accessed after their deletion.',
|
||||||
'In future versions of Jest, this behavior will change to "hard", which will likely fail tests.',
|
'In future versions of Jest, this behavior will change to "on", which will likely fail tests.',
|
||||||
'You can change the behavior in your test configuration now to reduce memory usage.',
|
'You can change the behavior in your test configuration now to reduce memory usage.',
|
||||||
]
|
]
|
||||||
.map(s => ` ${s}`)
|
.map(s => ` ${s}`)
|
||||||
|
|
|
@ -32,6 +32,7 @@ export {default as isNonNullable} from './isNonNullable';
|
||||||
export {
|
export {
|
||||||
type DeletionMode,
|
type DeletionMode,
|
||||||
canDeleteProperties,
|
canDeleteProperties,
|
||||||
|
initializeGarbageCollectionUtils,
|
||||||
protectProperties,
|
protectProperties,
|
||||||
deleteProperties,
|
deleteProperties,
|
||||||
} from './garbage-collection-utils';
|
} from './garbage-collection-utils';
|
||||||
|
|
|
@ -9,12 +9,17 @@ import * as fs from 'graceful-fs';
|
||||||
import type {Config} from '@jest/types';
|
import type {Config} from '@jest/types';
|
||||||
import createProcessObject from './createProcessObject';
|
import createProcessObject from './createProcessObject';
|
||||||
import deepCyclicCopy from './deepCyclicCopy';
|
import deepCyclicCopy from './deepCyclicCopy';
|
||||||
|
import {
|
||||||
|
type DeletionMode,
|
||||||
|
initializeGarbageCollectionUtils,
|
||||||
|
} from './garbage-collection-utils';
|
||||||
|
|
||||||
const DTRACE = Object.keys(globalThis).filter(key => key.startsWith('DTRACE'));
|
const DTRACE = Object.keys(globalThis).filter(key => key.startsWith('DTRACE'));
|
||||||
|
|
||||||
export default function installCommonGlobals(
|
export default function installCommonGlobals(
|
||||||
globalObject: typeof globalThis,
|
globalObject: typeof globalThis,
|
||||||
globals: Config.ConfigGlobals,
|
globals: Config.ConfigGlobals,
|
||||||
|
garbageCollectionDeletionMode?: DeletionMode,
|
||||||
): typeof globalThis & Config.ConfigGlobals {
|
): typeof globalThis & Config.ConfigGlobals {
|
||||||
globalObject.process = createProcessObject();
|
globalObject.process = createProcessObject();
|
||||||
|
|
||||||
|
@ -62,5 +67,12 @@ export default function installCommonGlobals(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (garbageCollectionDeletionMode) {
|
||||||
|
initializeGarbageCollectionUtils(
|
||||||
|
globalObject,
|
||||||
|
garbageCollectionDeletionMode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Object.assign(globalObject, deepCyclicCopy(globals));
|
return Object.assign(globalObject, deepCyclicCopy(globals));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue