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 argument to filter tests by projects #8612

Merged
merged 21 commits into from May 10, 2020
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Expand Up @@ -2,6 +2,8 @@

### Features

- `[jest-cli, jest-core]` Add `--selectProjects` CLI argument to filter test suites by project name ([#8612](https://github.com/facebook/jest/pull/8612))

### Fixes

- `[jest-jasmine2]` Stop adding `:` after an error that has no message ([#9990](https://github.com/facebook/jest/pull/9990))
Expand Down
4 changes: 4 additions & 0 deletions docs/CLI.md
Expand Up @@ -254,6 +254,10 @@ Run tests with specified reporters. [Reporter options](configuration#reporters-a

Alias: `-i`. Run all tests serially in the current process, rather than creating a worker pool of child processes that run tests. This can be useful for debugging.

### `--selectProjects <project1> ... <projectN>`

Run only the tests of the specified projects. Jest uses the attribute `displayName` in the configuration to identify each project.

### `--runTestsByPath`

Run only the tests that were specified with their exact paths.
Expand Down
82 changes: 82 additions & 0 deletions e2e/__tests__/selectProjects.test.ts
@@ -0,0 +1,82 @@
/**
SimenB marked this conversation as resolved.
Show resolved Hide resolved
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import {resolve} from 'path';

import {json as runWithJson} from '../runJest';

const dir = resolve(__dirname, '..', 'select-projects');

describe('when Jest is started with `--selectProjects first-project`', () => {
const result = runWithJson('select-projects', [
'--selectProjects',
'first-project',
]);
it('runs the tests in the first project only', () => {
expect(result.json.success).toBe(true);
expect(result.json.numTotalTests).toBe(1);
expect(result.json.testResults.map(({name}) => name)).toEqual([
resolve(dir, '__tests__/first-project.test.js'),
]);
});
it('prints that only first-project will run', () => {
expect(result.stderr).toMatch(/^Running one project: first-project/);
});
});

describe('when Jest is started with `--selectProjects second-project`', () => {
const result = runWithJson('select-projects', [
`--selectProjects`,
'second-project',
]);
it('runs the tests in the second project only', () => {
expect(result.json.success).toBe(true);
expect(result.json.numTotalTests).toBe(1);
expect(result.json.testResults.map(({name}) => name)).toEqual([
resolve(dir, '__tests__/second-project.test.js'),
]);
});
it('prints that only second-project will run', () => {
expect(result.stderr).toMatch(/^Running one project: second-project/);
});
});

describe('when Jest is started with `--selectProjects first-project second-project`', () => {
const result = runWithJson('select-projects', [
`--selectProjects`,
'first-project',
'second-project',
]);
it('runs the tests in the first and second projects', () => {
expect(result.json.success).toBe(true);
expect(result.json.numTotalTests).toBe(2);
expect(result.json.testResults.map(({name}) => name).sort()).toEqual([
resolve(dir, '__tests__/first-project.test.js'),
resolve(dir, '__tests__/second-project.test.js'),
]);
});
it('prints that both first-project and second-project will run', () => {
expect(result.stderr).toMatch(
/^Running 2 projects:\n- first-project\n- second-project/,
);
});
});

describe('when Jest is started without providing `--selectProjects`', () => {
const result = runWithJson('select-projects', []);
it('runs the tests in the first and second projects', () => {
expect(result.json.success).toBe(true);
expect(result.json.numTotalTests).toBe(2);
expect(result.json.testResults.map(({name}) => name).sort()).toEqual([
resolve(dir, '__tests__/first-project.test.js'),
resolve(dir, '__tests__/second-project.test.js'),
]);
});
it('does not print which projects are run', () => {
expect(result.stderr).not.toMatch(/^Running/);
});
});
10 changes: 10 additions & 0 deletions e2e/select-projects/__tests__/first-project.test.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

it('should run when first-project appears in selectProjects', () => {
expect(true).toBe(true);
});
10 changes: 10 additions & 0 deletions e2e/select-projects/__tests__/second-project.test.js
@@ -0,0 +1,10 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

it('should run when second-project appears in selectProjects', () => {
expect(true).toBe(true);
});
24 changes: 24 additions & 0 deletions e2e/select-projects/package.json
@@ -0,0 +1,24 @@
{
"description": "Testing the behaviour of --selectProjects",
"jest": {
"projects": [
{
"displayName": "first-project",
"testMatch": [
"<rootDir>/__tests__/first-project.test.js"
],
"testEnvironment": "node"
},
{
"displayName": {
"name": "second-project",
"color": "blue"
},
"testMatch": [
"<rootDir>/__tests__/second-project.test.js"
],
"testEnvironment": "node"
}
]
}
}
7 changes: 7 additions & 0 deletions packages/jest-cli/src/__tests__/cli/args.test.ts
Expand Up @@ -72,6 +72,13 @@ describe('check', () => {
},
);

it('raises an exception if selectProjects is not provided any project names', () => {
const argv: Config.Argv = {selectProjects: []} as Config.Argv;
expect(() => check(argv)).toThrow(
'The --selectProjects option requires the name of at least one project to be specified.\n',
);
});

it('raises an exception if config is not a valid JSON string', () => {
const argv = {config: 'x:1'} as Config.Argv;
expect(() => check(argv)).toThrow(
Expand Down
14 changes: 14 additions & 0 deletions packages/jest-cli/src/cli/args.ts
Expand Up @@ -49,6 +49,13 @@ export function check(argv: Config.Argv): true {
);
}

if (argv.selectProjects && argv.selectProjects.length === 0) {
throw new Error(
'The --selectProjects option requires the name of at least one project to be specified.\n' +
'Example usage: jest --selectProjects my-first-project my-second-project',
);
}

if (
argv.config &&
!isJSONString(argv.config) &&
Expand Down Expand Up @@ -528,6 +535,13 @@ export const options = {
"Allows to use a custom runner instead of Jest's default test runner.",
type: 'string',
},
selectProjects: {
description:
'Run only the tests of the specified projects.' +
'Jest uses the attribute `displayName` in the configuration to identify each project.',
string: true,
type: 'array',
},
setupFiles: {
description:
'A list of paths to modules that run some code to configure or ' +
Expand Down
9 changes: 8 additions & 1 deletion packages/jest-core/src/cli/index.ts
Expand Up @@ -26,6 +26,8 @@ import TestWatcher from '../TestWatcher';
import watch from '../watch';
import pluralize from '../pluralize';
import logDebugMessages from '../lib/log_debug_messages';
import getConfigsOfProjectsToRun from '../getConfigsOfProjectsToRun';
import getSelectProjectsMessage from '../getSelectProjectsMessage';

const {print: preRunMessagePrint} = preRunMessage;

Expand Down Expand Up @@ -68,9 +70,14 @@ export async function runCLI(
exit(0);
}

const configsOfProjectsToRun = getConfigsOfProjectsToRun(argv, configs);
if (argv.selectProjects) {
outputStream.write(getSelectProjectsMessage(configsOfProjectsToRun));
}

await _run(
globalConfig,
configs,
configsOfProjectsToRun,
hasDeprecationWarnings,
outputStream,
r => (results = r),
Expand Down
23 changes: 23 additions & 0 deletions packages/jest-core/src/getConfigsOfProjectsToRun.ts
@@ -0,0 +1,23 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import type {Config} from '@jest/types';
import getProjectDisplayName from './getProjectDisplayName';

export default function getConfigsOfProjectsToRun(
argv: Config.Argv,
projectConfigs: Array<Config.ProjectConfig>,
): Array<Config.ProjectConfig> {
if (!argv.selectProjects) {
return projectConfigs;
}
const namesOfProjectsToRun = new Set<string>(argv.selectProjects);
return projectConfigs.filter(config => {
const name = getProjectDisplayName(config);
return name && namesOfProjectsToRun.has(name);
Copy link
Member

@SimenB SimenB Dec 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we print a warning if name is not defined? If you're using this option you should (probably) also set a name for all your projects

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is not. Doing it right now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

});
}
24 changes: 24 additions & 0 deletions packages/jest-core/src/getProjectDisplayName.ts
@@ -0,0 +1,24 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import type {Config} from '@jest/types';

export default function getProjectDisplayName(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like https://github.com/facebook/jest/blob/d7a7b4294a4507030f86fe4f78e1790f53d0bda9/packages/jest-reporters/src/utils.ts#L18-L34 without the color. Can we share the code somehow?

Thinking about it, normalize should probably spit out the object form with the default white color, so we'd only have to deal with the one form inside jest

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's what I was telling myself when I wrote this. However this bit of code is in jest-reporters and I didn't know if or where I should move the code.

Leveraging normalize makes more sense to me. I'll try that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried and I failed. I can't give a detailed account of the blockers because it was too long ago.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened up #10010 for this btw, so we don't forget 🙂

projectConfig: Config.ProjectConfig,
): string | undefined {
const {displayName} = projectConfig;
if (!displayName) {
return undefined;
}
if (typeof displayName === 'string') {
return displayName;
}
if (typeof displayName === 'object') {
return displayName.name;
}
return undefined;
}
67 changes: 67 additions & 0 deletions packages/jest-core/src/getSelectProjectsMessage.ts
@@ -0,0 +1,67 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import chalk = require('chalk');
import type {Config} from '@jest/types';
import getProjectDisplayName from './getProjectDisplayName';

export default function getSelectProjectsMessage(
projectConfigs: Array<Config.ProjectConfig>,
): string {
if (projectConfigs.length === 0) {
return getNoSelectionWarning();
}
const numberOfProjectsWithoutAName = projectConfigs.filter(
config => !getProjectDisplayName(config),
).length;
return (
getNamesMissingWarning(numberOfProjectsWithoutAName) +
getProjectsRunningMessage(projectConfigs)
);
}

function getNoSelectionWarning(): string {
return chalk.yellow(
'You provided values for --selectProjects but no projects were found matching the selection.\n',
);
}

function getNamesMissingWarning(numberOfProjectsWithoutAName: number): string {
if (numberOfProjectsWithoutAName === 0) {
return '';
}
return chalk.yellow(
`You provided values for --selectProjects but ${
numberOfProjectsWithoutAName === 1
? 'a project has'
: `${numberOfProjectsWithoutAName} projects have`
} no name.\n` +
'Set displayName in the config of all projects in order to disable this warning.\n',
);
}

function getProjectsRunningMessage(
projectConfigs: Array<Config.ProjectConfig>,
): string {
if (projectConfigs.length === 1) {
const name = getProjectDisplayName(projectConfigs[0]);
return `Running one project: ${chalk.bold(name)}\n`;
}
const projectsList = projectConfigs
.map(getProjectNameListElement)
.sort()
.join('\n');
return `Running ${projectConfigs.length} projects:\n${projectsList}\n`;
}

function getProjectNameListElement(
projectConfig: Config.ProjectConfig,
): string {
const name = getProjectDisplayName(projectConfig);
const elementContent = name ? chalk.bold(name) : '<unnamed project>';
return `- ${elementContent}`;
}
1 change: 1 addition & 0 deletions packages/jest-types/src/Config.ts
Expand Up @@ -408,6 +408,7 @@ export type Argv = Arguments<
rootDir: string;
roots: Array<string>;
runInBand: boolean;
selectProjects: Array<string>;
setupFiles: Array<string>;
setupFilesAfterEnv: Array<string>;
showConfig: boolean;
Expand Down