This commit is contained in:
lewibs 2025-04-21 10:41:52 +02:00 committed by GitHub
commit a460b08be3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 122 additions and 5 deletions

View File

@ -2,6 +2,7 @@
### Features
- `[@jest/core, @jest/cli, @jest/resolve-dependencies]` Add `--maxRelatedTestsDepth` flag to control the depth of related test selection ([#15495](https://github.com/jestjs/jest/pull/15495))
- `[babel-jest]` Add option `excludeJestPreset` to allow opting out of `babel-preset-jest` ([#15164](https://github.com/jestjs/jest/pull/15164))
- `[jest-circus, jest-cli, jest-config]` Add `waitNextEventLoopTurnForUnhandledRejectionEvents` flag to minimise performance impact of correct detection of unhandled promise rejections introduced in [#14315](https://github.com/jestjs/jest/pull/14315) ([#14681](https://github.com/jestjs/jest/pull/14681))
- `[jest-circus]` Add a `waitBeforeRetry` option to `jest.retryTimes` ([#14738](https://github.com/jestjs/jest/pull/14738))

View File

@ -34,6 +34,12 @@ Run tests related to `path/to/fileA.js` and `path/to/fileB.js`:
jest --findRelatedTests path/to/fileA.js path/to/fileB.js
```
Used with `--findRelatedTests`, sets the maximum import depth that it will accept tests from.
```bash
jest --maxRelatedTestsDepth=5
```
Run tests that match this spec name (match against the name in `describe` or `test`, basically).
```bash
@ -213,6 +219,10 @@ module.exports = testPaths => {
Find and run the tests that cover a space separated list of source files that were passed in as arguments. Useful for pre-commit hook integration to run the minimal amount of tests necessary. Can be used together with `--coverage` to include a test coverage for the source files, no duplicate `--collectCoverageFrom` arguments needed.
### `--maxRelatedTestsDepth=<number>`
Used with `--findRelatedTests`, limits how deep Jest will traverse the dependency graph when finding related tests. A test at depth 1 directly imports the changed file, a test at depth 2 imports a file that imports the changed file, and so on. This is useful for large projects where you want to limit the scope of related test execution.
### `--forceExit`
Force Jest to exit after all tests have completed running. This is useful when resources set up by test code cannot be adequately cleaned up.

View File

@ -126,6 +126,7 @@ exports[`--showConfig outputs config info and exits 1`] = `
"listTests": false,
"logHeapUsage": false,
"maxConcurrency": 5,
"maxRelatedTestsDepth": Infinity,
"maxWorkers": "[maxWorkers]",
"noStackTrace": false,
"nonFlagArgs": [],

View File

@ -262,4 +262,50 @@ describe('--findRelatedTests flag', () => {
expect(stdout).toMatch('No tests found');
expect(stderr).toBe('');
});
test.each([
[4, ['a', 'b', 'b2', 'c', 'd']],
[3, ['a', 'b', 'b2', 'c']],
[2, ['a', 'b', 'b2']],
[1, ['a']],
])(
'runs tests with dependency chain and --maxRelatedTestsDepth=%d',
(depth, expectedTests) => {
writeFiles(DIR, {
'.watchmanconfig': '{}',
'__tests__/a.test.js':
"const a = require('../a'); test('a', () => {expect(a).toBe(\"value\")});",
'__tests__/b.test.js':
"const b = require('../b'); test('b', () => {expect(b).toBe(\"value\")});",
'__tests__/b2.test.js':
"const b = require('../b2'); test('b', () => {expect(b).toBe(\"value\")});",
'__tests__/c.test.js':
"const c = require('../c'); test('c', () => {expect(c).toBe(\"value\")});",
'__tests__/d.test.js':
"const d = require('../d'); test('d', () => {expect(d).toBe(\"value\")});",
'a.js': 'module.exports = "value";',
'b.js': 'module.exports = require("./a");',
'b2.js': 'module.exports = require("./a");',
'c.js': 'module.exports = require("./b");',
'd.js': 'module.exports = require("./c");',
'package.json': JSON.stringify({jest: {testEnvironment: 'node'}}),
});
const {stderr} = runJest(DIR, [
`--maxRelatedTestsDepth=${depth}`,
'--findRelatedTests',
'a.js',
]);
for (const testFile of expectedTests) {
expect(stderr).toMatch(`PASS __tests__/${testFile}.test.js`);
}
for (const testFile of ['a', 'b', 'b2', 'c', 'd']) {
if (!expectedTests.includes(testFile)) {
expect(stderr).not.toMatch(`PASS __tests__/${testFile}.test.js`);
}
}
},
);
});

View File

@ -48,6 +48,22 @@ export function check(argv: Config.Argv): true {
);
}
if (argv.maxRelatedTestsDepth && !argv.findRelatedTests) {
throw new Error(
'The --maxRelatedTestsDepth option requires --findRelatedTests is being used.',
);
}
if (
Object.prototype.hasOwnProperty.call(argv, 'maxRelatedTestsDepth') &&
typeof argv.maxRelatedTestsDepth !== 'number'
) {
throw new Error(
'The --maxRelatedTestsDepth option must be a number.\n' +
'Example usage: jest --findRelatedTests --maxRelatedTestsDepth 2',
);
}
if (
Object.prototype.hasOwnProperty.call(argv, 'maxWorkers') &&
argv.maxWorkers === undefined
@ -349,6 +365,12 @@ export const options: {[key: string]: Options} = {
requiresArg: true,
type: 'number',
},
maxRelatedTestsDepth: {
description:
'Specifies the maximum depth for finding related tests. Requires ' +
'--findRelatedTests to be used. Helps limit the scope of related test detection.',
type: 'number',
},
maxWorkers: {
alias: 'w',
description:

View File

@ -94,6 +94,7 @@ export const initialOptions: Config.InitialOptions = {
listTests: false,
logHeapUsage: true,
maxConcurrency: 5,
maxRelatedTestsDepth: Infinity,
maxWorkers: '50%',
moduleDirectories: ['node_modules'],
moduleFileExtensions: [

View File

@ -105,6 +105,7 @@ const groupOptions = (
listTests: options.listTests,
logHeapUsage: options.logHeapUsage,
maxConcurrency: options.maxConcurrency,
maxRelatedTestsDepth: options.maxRelatedTestsDepth,
maxWorkers: options.maxWorkers,
noSCM: undefined,
noStackTrace: options.noStackTrace,

View File

@ -900,6 +900,7 @@ export default async function normalize(
case 'globals':
case 'fakeTimers':
case 'findRelatedTests':
case 'maxRelatedTestsDepth':
case 'forceCoverageMatch':
case 'forceExit':
case 'injectGlobals':

View File

@ -178,6 +178,7 @@ export default class SearchSource {
async findRelatedTests(
allPaths: Set<string>,
collectCoverage: boolean,
maxDepth: number,
): Promise<SearchResult> {
const dependencyResolver = await this._getOrBuildDependencyResolver();
@ -188,7 +189,10 @@ export default class SearchSource {
dependencyResolver.resolveInverse(
allPaths,
this.isTestFilePath.bind(this),
{skipNodeResolution: this._context.config.skipNodeResolution},
{
maxDepth,
skipNodeResolution: this._context.config.skipNodeResolution,
},
),
),
};
@ -197,7 +201,10 @@ export default class SearchSource {
const testModulesMap = dependencyResolver.resolveInverseModuleMap(
allPaths,
this.isTestFilePath.bind(this),
{skipNodeResolution: this._context.config.skipNodeResolution},
{
maxDepth,
skipNodeResolution: this._context.config.skipNodeResolution,
},
);
const allPathsAbsolute = new Set([...allPaths].map(p => path.resolve(p)));
@ -246,12 +253,17 @@ export default class SearchSource {
async findRelatedTestsFromPattern(
paths: Array<string>,
collectCoverage: boolean,
maxDepth: number,
): Promise<SearchResult> {
if (Array.isArray(paths) && paths.length > 0) {
const resolvedPaths = paths.map(p =>
path.resolve(this._context.config.cwd, p),
);
return this.findRelatedTests(new Set(resolvedPaths), collectCoverage);
return this.findRelatedTests(
new Set(resolvedPaths),
collectCoverage,
maxDepth,
);
}
return {tests: []};
}
@ -259,12 +271,13 @@ export default class SearchSource {
async findTestRelatedToChangedFiles(
changedFilesInfo: ChangedFiles,
collectCoverage: boolean,
maxDepth: number,
): Promise<SearchResult> {
if (!hasSCM(changedFilesInfo)) {
return {noSCM: true, tests: []};
}
const {changedFiles} = changedFilesInfo;
return this.findRelatedTests(changedFiles, collectCoverage);
return this.findRelatedTests(changedFiles, collectCoverage, maxDepth);
}
private async _getTestPaths(
@ -280,6 +293,7 @@ export default class SearchSource {
return this.findTestRelatedToChangedFiles(
changedFiles,
globalConfig.collectCoverage,
globalConfig.maxRelatedTestsDepth,
);
}
@ -295,6 +309,7 @@ export default class SearchSource {
return this.findRelatedTestsFromPattern(
paths,
globalConfig.collectCoverage,
globalConfig.maxRelatedTestsDepth,
);
} else {
return this.findMatchingTests(

View File

@ -102,6 +102,7 @@ exports[`prints the config object 1`] = `
"listTests": false,
"logHeapUsage": false,
"maxConcurrency": 5,
"maxRelatedTestsDepth": Infinity,
"maxWorkers": 2,
"noStackTrace": false,
"nonFlagArgs": [],

View File

@ -70,6 +70,10 @@ export default function updateGlobalConfig(
newConfig.findRelatedTests = options.findRelatedTests;
}
if (options.maxRelatedTestsDepth !== undefined) {
newConfig.maxRelatedTestsDepth = options.maxRelatedTestsDepth;
}
if (options.nonFlagArgs !== undefined) {
newConfig.nonFlagArgs = options.nonFlagArgs;
}

View File

@ -115,6 +115,7 @@ export default async function watch(
coverageDirectory,
coverageReporters,
findRelatedTests,
maxRelatedTestsDepth,
mode,
nonFlagArgs,
notify,
@ -135,6 +136,7 @@ export default async function watch(
coverageDirectory,
coverageReporters,
findRelatedTests,
maxRelatedTestsDepth,
mode,
nonFlagArgs,
notify,

View File

@ -118,7 +118,10 @@ export class DependencyResolver {
) => {
const visitedModules = new Set();
const result: Array<ResolvedModule> = [];
while (changed.size > 0) {
let depth = 0;
const maxDepth = options?.maxDepth ?? Infinity;
while (changed.size > 0 && depth < maxDepth) {
changed = new Set(
moduleMap.reduce<Array<string>>((acc, module) => {
if (
@ -138,6 +141,7 @@ export class DependencyResolver {
return acc;
}, []),
);
depth++;
}
return [
...result,
@ -165,6 +169,7 @@ export class DependencyResolver {
file,
});
}
return collectModules(relatedPaths, modules, changed);
}

View File

@ -37,6 +37,7 @@ export type ResolveModuleConfig = {
conditions?: Array<string>;
skipNodeResolution?: boolean;
paths?: Array<string>;
maxDepth?: number;
};
const NATIVE_PLATFORM = 'native';

View File

@ -251,6 +251,7 @@ export const InitialOptions = Type.Partial(
fakeTimers: FakeTimers,
filter: Type.String(),
findRelatedTests: Type.Boolean(),
maxRelatedTestsDepth: Type.Number(),
forceCoverageMatch: Type.Array(Type.String()),
forceExit: Type.Boolean(),
json: Type.Boolean(),

View File

@ -173,6 +173,7 @@ export type DefaultOptions = {
injectGlobals: boolean;
listTests: boolean;
maxConcurrency: number;
maxRelatedTestsDepth: number;
maxWorkers: number | string;
moduleDirectories: Array<string>;
moduleFileExtensions: Array<string>;
@ -271,6 +272,7 @@ export type GlobalConfig = {
expand: boolean;
filter?: string;
findRelatedTests: boolean;
maxRelatedTestsDepth: number;
forceExit: boolean;
json: boolean;
globalSetup?: string;
@ -424,6 +426,7 @@ export type Argv = Arguments<
env: string;
expand: boolean;
findRelatedTests: boolean;
maxRelatedTestsDepth: number;
forceExit: boolean;
globals: string;
globalSetup: string | null | undefined;

View File

@ -57,6 +57,7 @@ export type AllowedConfigOptions = Partial<
| 'coverageDirectory'
| 'coverageReporters'
| 'findRelatedTests'
| 'maxRelatedTestsDepth'
| 'nonFlagArgs'
| 'notify'
| 'notifyMode'

View File

@ -33,6 +33,7 @@ const DEFAULT_GLOBAL_CONFIG: Config.GlobalConfig = {
listTests: false,
logHeapUsage: false,
maxConcurrency: 5,
maxRelatedTestsDepth: Infinity,
maxWorkers: 2,
noSCM: undefined,
noStackTrace: false,