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 17 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. 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
168 changes: 168 additions & 0 deletions e2e/__tests__/selectProjects.test.ts
@@ -0,0 +1,168 @@
/**
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';

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`', () => {
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/);
});
});

describe('when Jest is started with `--selectProjects third-project`', () => {
const result = runWithJson('select-projects', [
Copy link
Member

Choose a reason for hiding this comment

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

can you stick this inside the test? describe runs before tests start. can combine the 2 its, I guess, or stick the runWithJson inside a beforeAll

'--passWithNoTests', // This is necessary to get a JSON output
'--selectProjects',
'third-project',
]);
it('does not run any tests', () => {
expect(result.json.success).toBe(true);
Copy link
Member

Choose a reason for hiding this comment

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

should be false, shouldn't it? If you pass a non-existing name, I think we should fail

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do fail. Here it is true because we also provide --passWithNoTests. If we don't we can't get JSON output.

I'll change the tests to look for failure though.

Copy link
Member

Choose a reason for hiding this comment

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

In the error cases we don't need JSON, we can just do the normal run (no json) and verify exitCode and stderr

expect(result.json.numTotalTests).toBe(0);
});
it('prints that no project was found', () => {
expect(result.stderr).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`', () => {
const 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 name', () => {
expect(result.stderr).toMatch(
/^You provided values for --selectProjects but a project does not a have 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`', () => {
const 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 a have name/,
);
});
});

describe('when Jest is started with `--selectProjects third-project`', () => {
const result = runWithJson('select-projects-missing-name', [
'--passWithNoTests',
'--selectProjects',
'third-project',
]);
it('does not run any tests', () => {
expect(result.json.success).toBe(true);
expect(result.json.numTotalTests).toBe(0);
});
it('prints that a project does not have name', () => {
expect(result.stderr).toMatch(
/^You provided values for --selectProjects but a project does not a have name/,
);
});
it('prints that no project was found', () => {
const stderrThirdLine = result.stderr.split('\n')[2];
expect(stderrThirdLine).toMatch(
/^You provided values for --selectProjects but no projects were found matching the selection/,
);
});
});
});
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
20 changes: 20 additions & 0 deletions packages/jest-core/src/getConfigsOfProjectsToRun.ts
@@ -0,0 +1,20 @@
/**
* 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(
namesOfProjectsToRun: Array<string>,
projectConfigs: Array<Config.ProjectConfig>,
): Array<Config.ProjectConfig> {
const setOfProjectsToRun = new Set<string>(namesOfProjectsToRun);
return projectConfigs.filter(config => {
const name = getProjectDisplayName(config);
return name && setOfProjectsToRun.has(name);
});
}