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

feat(CLI): --maxRelatedTestsDepth flag to control the depth of related test in --findRelatedTests #15495

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
10 changes: 10 additions & 0 deletions docs/CLI.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions e2e/__tests__/__snapshots__/showConfig.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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": [],
Expand Down
46 changes: 46 additions & 0 deletions e2e/__tests__/findRelatedFiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`);
}
}
},
);
});
22 changes: 22 additions & 0 deletions packages/jest-cli/src/args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@
);
}

if (argv.maxRelatedTestsDepth && !argv.findRelatedTests) {
throw new Error(

Check warning on line 52 in packages/jest-cli/src/args.ts

View check run for this annotation

Codecov / codecov/patch

packages/jest-cli/src/args.ts#L52

Added line #L52 was not covered by tests
'The --maxRelatedTestsDepth option requires --findRelatedTests is being used.',
);
}

if (
Object.prototype.hasOwnProperty.call(argv, 'maxRelatedTestsDepth') &&
typeof argv.maxRelatedTestsDepth !== 'number'

Check warning on line 59 in packages/jest-cli/src/args.ts

View check run for this annotation

Codecov / codecov/patch

packages/jest-cli/src/args.ts#L59

Added line #L59 was not covered by tests
) {
throw new Error(

Check warning on line 61 in packages/jest-cli/src/args.ts

View check run for this annotation

Codecov / codecov/patch

packages/jest-cli/src/args.ts#L61

Added line #L61 was not covered by tests
'The --maxRelatedTestsDepth option must be a number.\n' +
'Example usage: jest --findRelatedTests --maxRelatedTestsDepth 2',
);
}

if (
Object.prototype.hasOwnProperty.call(argv, 'maxWorkers') &&
argv.maxWorkers === undefined
Expand Down Expand Up @@ -349,6 +365,12 @@
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:
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/ValidConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export const initialOptions: Config.InitialOptions = {
listTests: false,
logHeapUsage: true,
maxConcurrency: 5,
maxRelatedTestsDepth: Infinity,
maxWorkers: '50%',
moduleDirectories: ['node_modules'],
moduleFileExtensions: [
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-config/src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,7 @@ export default async function normalize(
case 'globals':
case 'fakeTimers':
case 'findRelatedTests':
case 'maxRelatedTestsDepth':
case 'forceCoverageMatch':
case 'forceExit':
case 'injectGlobals':
Expand Down
23 changes: 19 additions & 4 deletions packages/jest-core/src/SearchSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@
async findRelatedTests(
allPaths: Set<string>,
collectCoverage: boolean,
maxDepth: number,
): Promise<SearchResult> {
const dependencyResolver = await this._getOrBuildDependencyResolver();

Expand All @@ -188,7 +189,10 @@
dependencyResolver.resolveInverse(
allPaths,
this.isTestFilePath.bind(this),
{skipNodeResolution: this._context.config.skipNodeResolution},
{
maxDepth,
skipNodeResolution: this._context.config.skipNodeResolution,
},
),
),
};
Expand All @@ -197,7 +201,10 @@
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)));
Expand Down Expand Up @@ -246,25 +253,31 @@
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: []};
}

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);

Check warning on line 280 in packages/jest-core/src/SearchSource.ts

View check run for this annotation

Codecov / codecov/patch

packages/jest-core/src/SearchSource.ts#L280

Added line #L280 was not covered by tests
}

private async _getTestPaths(
Expand All @@ -280,6 +293,7 @@
return this.findTestRelatedToChangedFiles(
changedFiles,
globalConfig.collectCoverage,
globalConfig.maxRelatedTestsDepth,
);
}

Expand All @@ -295,6 +309,7 @@
return this.findRelatedTestsFromPattern(
paths,
globalConfig.collectCoverage,
globalConfig.maxRelatedTestsDepth,
);
} else {
return this.findMatchingTests(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ exports[`prints the config object 1`] = `
"listTests": false,
"logHeapUsage": false,
"maxConcurrency": 5,
"maxRelatedTestsDepth": Infinity,
"maxWorkers": 2,
"noStackTrace": false,
"nonFlagArgs": [],
Expand Down
4 changes: 4 additions & 0 deletions packages/jest-core/src/lib/updateGlobalConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@
newConfig.findRelatedTests = options.findRelatedTests;
}

if (options.maxRelatedTestsDepth !== undefined) {
newConfig.maxRelatedTestsDepth = options.maxRelatedTestsDepth;

Check warning on line 74 in packages/jest-core/src/lib/updateGlobalConfig.ts

View check run for this annotation

Codecov / codecov/patch

packages/jest-core/src/lib/updateGlobalConfig.ts#L74

Added line #L74 was not covered by tests
}

if (options.nonFlagArgs !== undefined) {
newConfig.nonFlagArgs = options.nonFlagArgs;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-core/src/watch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export default async function watch(
coverageDirectory,
coverageReporters,
findRelatedTests,
maxRelatedTestsDepth,
mode,
nonFlagArgs,
notify,
Expand All @@ -135,6 +136,7 @@ export default async function watch(
coverageDirectory,
coverageReporters,
findRelatedTests,
maxRelatedTestsDepth,
mode,
nonFlagArgs,
notify,
Expand Down
7 changes: 6 additions & 1 deletion packages/jest-resolve-dependencies/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,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 (
Expand All @@ -132,6 +135,7 @@ export class DependencyResolver {
return acc;
}, []),
);
depth++;
}
return [
...result,
Expand Down Expand Up @@ -159,6 +163,7 @@ export class DependencyResolver {
file,
});
}

return collectModules(relatedPaths, modules, changed);
}

Expand Down
1 change: 1 addition & 0 deletions packages/jest-resolve/src/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export type ResolveModuleConfig = {
conditions?: Array<string>;
skipNodeResolution?: boolean;
paths?: Array<string>;
maxDepth?: number;
};

const NATIVE_PLATFORM = 'native';
Expand Down
1 change: 1 addition & 0 deletions packages/jest-schemas/src/raw-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
3 changes: 3 additions & 0 deletions packages/jest-types/src/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export type DefaultOptions = {
injectGlobals: boolean;
listTests: boolean;
maxConcurrency: number;
maxRelatedTestsDepth: number;
maxWorkers: number | string;
moduleDirectories: Array<string>;
moduleFileExtensions: Array<string>;
Expand Down Expand Up @@ -271,6 +272,7 @@ export type GlobalConfig = {
expand: boolean;
filter?: string;
findRelatedTests: boolean;
maxRelatedTestsDepth: number;
forceExit: boolean;
json: boolean;
globalSetup?: string;
Expand Down Expand Up @@ -424,6 +426,7 @@ export type Argv = Arguments<
env: string;
expand: boolean;
findRelatedTests: boolean;
maxRelatedTestsDepth: number;
forceExit: boolean;
globals: string;
globalSetup: string | null | undefined;
Expand Down
1 change: 1 addition & 0 deletions packages/jest-watcher/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type AllowedConfigOptions = Partial<
| 'coverageDirectory'
| 'coverageReporters'
| 'findRelatedTests'
| 'maxRelatedTestsDepth'
| 'nonFlagArgs'
| 'notify'
| 'notifyMode'
Expand Down
1 change: 1 addition & 0 deletions packages/test-utils/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const DEFAULT_GLOBAL_CONFIG: Config.GlobalConfig = {
listTests: false,
logHeapUsage: false,
maxConcurrency: 5,
maxRelatedTestsDepth: Infinity,
maxWorkers: 2,
noSCM: undefined,
noStackTrace: false,
Expand Down
Loading