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 all 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
### Features

- `[jest-config]` Support config files exporting (`async`) `function`s ([#10001](https://github.com/facebook/jest/pull/10001))
- `[jest-cli, jest-core]` Add `--selectProjects` CLI argument to filter test suites by project name ([#8612](https://github.com/facebook/jest/pull/8612))

### Fixes

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. If you use this option, you should provide a `displayName` to all your projects.

### `--runTestsByPath`

Run only the tests that were specified with their exact paths.
Expand Down
189 changes: 189 additions & 0 deletions e2e/__tests__/selectProjects.test.ts
@@ -0,0 +1,189 @@
/**
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 run, {
RunJestJsonResult,
RunJestResult,
json as runWithJson,
} from '../runJest';

describe('Given a config with two named projects, first-project and second-project', () => {
const dir = resolve(__dirname, '..', 'select-projects');

describe('when Jest is started with `--selectProjects first-project`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects', [
'--selectProjects',
'first-project',
]);
});
it('runs the tests in the first project only', () => {
expect(result.json).toHaveProperty('success', true);
expect(result.json).toHaveProperty('numTotalTests', 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`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects', [
'--selectProjects',
'second-project',
]);
});
it('runs the tests in the second project only', () => {
expect(result.json).toHaveProperty('success', true);
expect(result.json).toHaveProperty('numTotalTests', 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`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects', [
'--selectProjects',
'first-project',
'second-project',
]);
});
it('runs the tests in the first and second projects', () => {
expect(result.json).toHaveProperty('success', true);
expect(result.json).toHaveProperty('numTotalTests', 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`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects', []);
});
it('runs the tests in the first and second projects', () => {
expect(result.json).toHaveProperty('success', true);
expect(result.json).toHaveProperty('numTotalTests', 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/);
});
});

describe('when Jest is started with `--selectProjects third-project`', () => {
let result: RunJestResult;
beforeAll(() => {
result = run('select-projects', ['--selectProjects', 'third-project']);
});
it('fails', () => {
expect(result).toHaveProperty('failed', true);
});
it('prints that no project was found', () => {
expect(result.stdout).toMatch(
/^You provided values for --selectProjects but no projects were found matching the selection/,
);
});
});
});

describe('Given a config with two projects, first-project and an unnamed project', () => {
const dir = resolve(__dirname, '..', 'select-projects-missing-name');

describe('when Jest is started with `--selectProjects first-project`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects-missing-name', [
'--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 a project does not have a name', () => {
expect(result.stderr).toMatch(
/^You provided values for --selectProjects but a project does not have a name/,
);
});
it('prints that only first-project will run', () => {
const stderrThirdLine = result.stderr.split('\n')[2];
expect(stderrThirdLine).toMatch(/^Running one project: first-project/);
});
});

describe('when Jest is started without providing `--selectProjects`', () => {
let result: RunJestJsonResult;
beforeAll(() => {
result = runWithJson('select-projects-missing-name', []);
});
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 that a project has no name', () => {
expect(result.stderr).not.toMatch(
/^You provided values for --selectProjects but a project does not have a name/,
);
});
});

describe('when Jest is started with `--selectProjects third-project`', () => {
let result: RunJestResult;
beforeAll(() => {
result = run('select-projects-missing-name', [
'--selectProjects',
'third-project',
]);
});
it('fails', () => {
expect(result).toHaveProperty('failed', true);
});
it('prints that a project does not have a name', () => {
expect(result.stdout).toMatch(
/^You provided values for --selectProjects but a project does not have a name/,
);
});
it('prints that no project was found', () => {
const stdoutThirdLine = result.stdout.split('\n')[2];
expect(stdoutThirdLine).toMatch(
/^You provided values for --selectProjects but no projects were found matching the selection/,
);
});
});
});
2 changes: 1 addition & 1 deletion e2e/runJest.ts
Expand Up @@ -95,7 +95,7 @@ function spawnJest(

export type RunJestResult = execa.ExecaReturnValue;

interface RunJestJsonResult extends RunJestResult {
export interface RunJestJsonResult extends RunJestResult {
json: FormattedTestResults;
}

Expand Down
10 changes: 10 additions & 0 deletions e2e/select-projects-missing-name/__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-missing-name/__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);
});
20 changes: 20 additions & 0 deletions e2e/select-projects-missing-name/package.json
@@ -0,0 +1,20 @@
{
"description": "Testing the behaviour of --selectProjects when a project does not have a name",
"jest": {
"projects": [
{
"displayName": "first-project",
"testMatch": [
"<rootDir>/__tests__/first-project.test.js"
],
"testEnvironment": "node"
},
{
"testMatch": [
"<rootDir>/__tests__/second-project.test.js"
],
"testEnvironment": "node"
}
]
}
}
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
18 changes: 17 additions & 1 deletion packages/jest-core/src/cli/index.ts
Expand Up @@ -26,6 +26,9 @@ import TestWatcher from '../TestWatcher';
import watch from '../watch';
import pluralize from '../pluralize';
import logDebugMessages from '../lib/log_debug_messages';
import getConfigsOfProjectsToRun from '../getConfigsOfProjectsToRun';
import getProjectNamesMissingWarning from '../getProjectNamesMissingWarning';
import getSelectProjectsMessage from '../getSelectProjectsMessage';

const {print: preRunMessagePrint} = preRunMessage;

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

let configsOfProjectsToRun = configs;
if (argv.selectProjects) {
const namesMissingWarning = getProjectNamesMissingWarning(configs);
if (namesMissingWarning) {
outputStream.write(namesMissingWarning);
Copy link
Collaborator

Choose a reason for hiding this comment

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

we usually go with console.warn for warnings, but I don't think it matters this much here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

console.warn will always write the string to stdout, whereas with outputStream we will write to either stdout or stderr depending on what is appropriate.

Here, if I replace outputStream.write by console.warn the tests will fail as they rely on the JSON output, and the JSON output can only be valid if all messages are written to stderr instead of stdout.

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

await _run10000(
globalConfig,
configs,
configsOfProjectsToRun,
hasDeprecationWarnings,
outputStream,
r => (results = r),
Expand Down