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

add only-failures mode #4886

Merged
merged 2 commits into from Dec 15, 2017
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
Expand Up @@ -12,6 +12,7 @@ Array [
"
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press q to quit watch mode.
Expand All @@ -26,6 +27,7 @@ Array [
"
Watch Usage
› Press a to run all tests.
› Press f to run only failed tests.
› Press p to filter by a filename regex pattern.
› Press t to filter by a test name regex pattern.
› Press s to do nothing.
Expand Down
62 changes: 62 additions & 0 deletions packages/jest-cli/src/__tests__/failed_tests_cache.test.js
@@ -0,0 +1,62 @@
import FailedTestsCache from '../failed_tests_cache';

describe('FailedTestsCache', () => {
test('should filter tests', () => {
const failedTestsCache = new FailedTestsCache();
failedTestsCache.setTestResults([
{
numFailingTests: 0,
testFilePath: '/path/to/passing.js',
testResults: [
{fullName: 'test 1', status: 'passed'},
{fullName: 'test 2', status: 'passed'},
],
},
{
numFailingTests: 2,
testFilePath: '/path/to/failed_1.js',
testResults: [
{fullName: 'test 3', status: 'failed'},
{fullName: 'test 4', status: 'failed'},
],
},
{
numFailingTests: 1,
testFilePath: '/path/to/failed_2.js',
testResults: [
{fullName: 'test 5', status: 'failed'},
{fullName: 'test 6', status: 'passed'},
],
},
]);

const result = failedTestsCache.filterTests([
{
path: '/path/to/passing.js',
},
{
path: '/path/to/failed_1.js',
},
{
path: '/path/to/failed_2.js',
},
{
path: '/path/to/unknown.js',
},
]);
expect(result).toMatchObject([
{
path: '/path/to/failed_1.js',
},
{
path: '/path/to/failed_2.js',
},
]);
expect(failedTestsCache.updateConfig({})).toMatchObject({
enabledTestsMap: {
'/path/to/failed_1.js': {'test 3': true, 'test 4': true},
'/path/to/failed_2.js': {'test 5': true},
},
});
});
});
6 changes: 6 additions & 0 deletions packages/jest-cli/src/cli/args.js
Expand Up @@ -349,6 +349,12 @@ export const options = {
'running tests in a git repository at the moment.',
type: 'boolean',
},
onlyFailures: {
alias: 'f',
default: undefined,
description: 'Run tests that failed in the previous execution.',
type: 'boolean',
},
outputFile: {
description:
'Write test results to a file when the --json option is ' +
Expand Down
1 change: 1 addition & 0 deletions packages/jest-cli/src/cli/index.js
Expand Up @@ -368,6 +368,7 @@ const runWithoutWatch = async (
return await runJest({
changedFilesPromise,
contexts,
failedTestsCache: null,
globalConfig,
onComplete,
outputStream,
Expand Down
1 change: 1 addition & 0 deletions packages/jest-cli/src/constants.js
Expand Up @@ -23,6 +23,7 @@ export const KEYS = {
CONTROL_D: '04',
ENTER: '0d',
ESCAPE: '1b',
F: '66',
O: '6f',
P: '70',
Q: '71',
Expand Down
49 changes: 49 additions & 0 deletions packages/jest-cli/src/failed_tests_cache.js
@@ -0,0 +1,49 @@
/**
* Copyright (c) 2014-present, Facebook, Inc. 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.
*
* @flow
*/

import type {Test} from 'types/TestRunner';
import type {TestResult} from 'types/TestResult';
import type {GlobalConfig} from 'types/Config';

export default class FailedTestsCache {
_enabledTestsMap: ?{[key: string]: {[key: string]: boolean}};

filterTests(tests: Array<Test>): Array<Test> {
if (!this._enabledTestsMap) {
return tests;
}
// $FlowFixMe
return tests.filter(testResult => this._enabledTestsMap[testResult.path]);
}

setTestResults(testResults: Array<TestResult>) {
this._enabledTestsMap = (testResults || [])
.filter(testResult => testResult.numFailingTests)
.reduce((suiteMap, testResult) => {
suiteMap[testResult.testFilePath] = testResult.testResults
.filter(test => test.status === 'failed')
.reduce((testMap, test) => {
testMap[test.fullName] = true;
return testMap;
}, {});
return suiteMap;
}, {});
this._enabledTestsMap = Object.freeze(this._enabledTestsMap);
}

updateConfig(globalConfig: GlobalConfig): GlobalConfig {
if (!this._enabledTestsMap) {
return globalConfig;
}
// $FlowFixMe Object.assign
const newConfig: GlobalConfig = Object.assign({}, globalConfig);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this should be done through import updateGlobalConfig from './lib/update_global_config';

newConfig.enabledTestsMap = this._enabledTestsMap;
return Object.freeze(newConfig);
}
}
8 changes: 8 additions & 0 deletions packages/jest-cli/src/get_no_test_found_failed.js
@@ -0,0 +1,8 @@
import chalk from 'chalk';

export default function getNoTestFoundFailed() {
return (
chalk.bold('No failed test found.\n') +
chalk.dim('Press `f` to run all tests.')
);
}
4 changes: 4 additions & 0 deletions packages/jest-cli/src/get_no_test_found_message.js
@@ -1,11 +1,15 @@
import getNoTestFound from './get_no_test_found';
import getNoTestFoundRelatedToChangedFiles from './get_no_test_found_related_to_changed_files';
import getNoTestFoundVerbose from './get_no_test_found_verbose';
import getNoTestFoundFailed from './get_no_test_found_failed';

export default function getNoTestsFoundMessage(
testRunData,
globalConfig,
): string {
if (globalConfig.onlyFailures) {
return getNoTestFoundFailed();
}
if (globalConfig.onlyChanged) {
return getNoTestFoundRelatedToChangedFiles(globalConfig);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/jest-cli/src/lib/update_global_config.js
Expand Up @@ -16,6 +16,7 @@ type Options = {
updateSnapshot?: SnapshotUpdateState,
mode?: 'watch' | 'watchAll',
passWithNoTests?: boolean,
onlyFailures?: boolean,
};

export default (globalConfig: GlobalConfig, options: Options): GlobalConfig => {
Expand Down Expand Up @@ -60,5 +61,9 @@ export default (globalConfig: GlobalConfig, options: Options): GlobalConfig => {
newConfig.passWithNoTests = true;
}

if ('onlyFailures' in options) {
newConfig.onlyFailures = options.onlyFailures || false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to cast onlyFailures to boolean?

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 did that because of Flow warning.

}

return Object.freeze(newConfig);
};
9 changes: 9 additions & 0 deletions packages/jest-cli/src/run_jest.js
Expand Up @@ -22,6 +22,7 @@ import SearchSource from './search_source';
import TestScheduler from './test_scheduler';
import TestSequencer from './test_sequencer';
import {makeEmptyAggregatedTestResult} from './test_result_helpers';
import FailedTestsCache from './failed_tests_cache';

const setConfig = (contexts, newConfig) =>
contexts.forEach(
Expand Down Expand Up @@ -86,6 +87,7 @@ export default (async function runJest({
startRun,
changedFilesPromise,
onComplete,
failedTestsCache,
}: {
globalConfig: GlobalConfig,
contexts: Array<Context>,
Expand All @@ -94,6 +96,7 @@ export default (async function runJest({
startRun: (globalConfig: GlobalConfig) => *,
changedFilesPromise: ?ChangedFilesPromise,
onComplete: (testResults: AggregatedResult) => any,
failedTestsCache: ?FailedTestsCache,
}) {
const sequencer = new TestSequencer();
let allTests = [];
Expand Down Expand Up @@ -138,6 +141,12 @@ export default (async function runJest({
onComplete && onComplete(makeEmptyAggregatedTestResult());
return null;
}

if (globalConfig.onlyFailures && failedTestsCache) {
allTests = failedTestsCache.filterTests(allTests);
globalConfig = failedTestsCache.updateConfig(globalConfig);
}

if (!allTests.length) {
const noTestsFoundMessage = getNoTestsFoundMessage(
testRunData,
Expand Down
21 changes: 19 additions & 2 deletions packages/jest-cli/src/watch.js
Expand Up @@ -27,6 +27,7 @@ import TestWatcher from './test_watcher';
import Prompt from './lib/Prompt';
import TestPathPatternPrompt from './test_path_pattern_prompt';
import TestNamePatternPrompt from './test_name_pattern_prompt';
import FailedTestsCache from './failed_tests_cache';
import WatchPluginRegistry from './lib/watch_plugin_registry';
import {KEYS, CLEAR} from './constants';

Expand Down Expand Up @@ -55,6 +56,7 @@ export default function watch(
}
}

const failedTestsCache = new FailedTestsCache();
const prompt = new Prompt();
const testPathPatternPrompt = new TestPathPatternPrompt(outputStream, prompt);
const testNamePatternPrompt = new TestNamePatternPrompt(outputStream, prompt);
Expand Down Expand Up @@ -120,6 +122,7 @@ export default function watch(
return runJest({
changedFilesPromise,
contexts,
failedTestsCache,
globalConfig,
onComplete: results => {
isRunning = false;
Expand All @@ -146,7 +149,7 @@ export default function watch(
} else {
outputStream.write('\n');
}

failedTestsCache.setTestResults(results.testResults);
testNamePatternPrompt.updateCachedTestResults(results.testResults);
},
outputStream,
Expand Down Expand Up @@ -177,7 +180,9 @@ export default function watch(
if (
isRunning &&
testWatcher &&
[KEYS.Q, KEYS.ENTER, KEYS.A, KEYS.O, KEYS.P, KEYS.T].indexOf(key) !== -1
[KEYS.Q, KEYS.ENTER, KEYS.A, KEYS.O, KEYS.P, KEYS.T, KEYS.F].indexOf(
key,
) !== -1
) {
testWatcher.setState({interrupted: true});
return;
Expand Down Expand Up @@ -230,6 +235,12 @@ export default function watch(
});
startRun(globalConfig);
break;
case KEYS.F:
Copy link
Contributor

Choose a reason for hiding this comment

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

Should pressing C also clear this filter?

globalConfig = updateGlobalConfig(globalConfig, {
onlyFailures: !globalConfig.onlyFailures,
});
startRun(globalConfig);
break;
case KEYS.O:
globalConfig = updateGlobalConfig(globalConfig, {
mode: 'watch',
Expand Down Expand Up @@ -342,6 +353,12 @@ const usage = (
? chalk.dim(' \u203A Press ') + 'a' + chalk.dim(' to run all tests.')
: null,

globalConfig.onlyFailures
? chalk.dim(' \u203A Press ') + 'f' + chalk.dim(' to run all tests.')
: chalk.dim(' \u203A Press ') +
'f' +
chalk.dim(' to run only failed tests.'),

(globalConfig.watchAll ||
globalConfig.testPathPattern ||
globalConfig.testNamePattern) &&
Expand Down
2 changes: 2 additions & 0 deletions packages/jest-config/src/index.js
Expand Up @@ -82,6 +82,7 @@ const getConfigs = (
coverageReporters: options.coverageReporters,
coverageThreshold: options.coverageThreshold,
detectLeaks: options.detectLeaks,
enabledTestsMap: options.enabledTestsMap,
expand: options.expand,
findRelatedTests: options.findRelatedTests,
forceExit: options.forceExit,
Expand All @@ -96,6 +97,7 @@ const getConfigs = (
nonFlagArgs: options.nonFlagArgs,
notify: options.notify,
onlyChanged: options.onlyChanged,
onlyFailures: options.onlyFailures,
outputFile: options.outputFile,
passWithNoTests: options.passWithNoTests,
projects: options.projects,
Expand Down
9 changes: 8 additions & 1 deletion packages/jest-jasmine2/src/index.js
Expand Up @@ -132,7 +132,14 @@ async function jasmine2(
},
});

if (globalConfig.testNamePattern) {
if (globalConfig.enabledTestsMap) {
Copy link
Contributor

Choose a reason for hiding this comment

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

How would this work with jest-circus? cc: @aaronabramov @cpojer

env.specFilter = spec => {
const suiteMap =
globalConfig.enabledTestsMap &&
globalConfig.enabledTestsMap[spec.result.testPath];
return suiteMap && suiteMap[spec.result.fullName];
};
} else if (globalConfig.testNamePattern) {
const testNameRegex = new RegExp(globalConfig.testNamePattern, 'i');
env.specFilter = spec => testNameRegex.test(spec.getFullName());
}
Expand Down
2 changes: 2 additions & 0 deletions test_utils.js
Expand Up @@ -21,6 +21,7 @@ const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
coverageReporters: [],
coverageThreshold: {global: {}},
detectLeaks: false,
enabledTestsMap: null,
expand: false,
findRelatedTests: false,
forceExit: false,
Expand All @@ -35,6 +36,7 @@ const DEFAULT_GLOBAL_CONFIG: GlobalConfig = {
nonFlagArgs: [],
notify: false,
onlyChanged: false,
onlyFailures: false,
outputFile: null,
passWithNoTests: false,
projects: [],
Expand Down
2 changes: 2 additions & 0 deletions types/Config.js
Expand Up @@ -159,6 +159,7 @@ export type GlobalConfig = {|
coverageReporters: Array<string>,
coverageThreshold: {global: {[key: string]: number}},
detectLeaks: boolean,
enabledTestsMap: ?{[key: string]: {[key: string]: boolean}},
expand: boolean,
findRelatedTests: boolean,
forceExit: boolean,
Expand All @@ -174,6 +175,7 @@ export type GlobalConfig = {|
notify: boolean,
outputFile: ?Path,
onlyChanged: boolean,
onlyFailures: boolean,
passWithNoTests: boolean,
projects: Array<Glob>,
replname: ?string,
Expand Down