diff --git a/CHANGELOG.md b/CHANGELOG.md index e4aca3a8512d..b8e80c85e0f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ ## master +### Features +* `[jest-cli]` `--changedSince`: allow selectively running tests for code + changed since arbitrary revisions. + ([#5312](https://github.com/facebook/jest/pull/5312)) + ## jest 22.1.3 ### Fixes diff --git a/docs/CLI.md b/docs/CLI.md index b9aa40ecbefb..8cfc0a5d3acd 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -108,6 +108,14 @@ If you want to inspect the cache, use `--showConfig` and look at the Runs tests related to the current changes and the changes made in the last commit. Behaves similarly to `--onlyChanged`. +### `--changedSince` + +##### available in Jest **22.2.0+** + +Runs tests related the changes since the provided branch. If the current branch +has diverged from the given branch, then only changes made locally will be +tested. Behaves similarly to `--onlyChanged`. + ### `--ci` When this option is provided, Jest will assume it is running in a CI diff --git a/integration-tests/__tests__/jest_changed_files.test.js b/integration-tests/__tests__/jest_changed_files.test.js index 759f246f49e8..72f1ad93ccac 100644 --- a/integration-tests/__tests__/jest_changed_files.test.js +++ b/integration-tests/__tests__/jest_changed_files.test.js @@ -191,6 +191,37 @@ test('gets changed files for git', async () => { .map(filePath => path.basename(filePath)) .sort(), ).toEqual(['file1.txt', 'file4.txt']); + + run(`${GIT} add file4.txt`, DIR); + run(`${GIT} commit -m "test3"`, DIR); + + ({changedFiles: files} = await getChangedFilesForRoots(roots, { + changedSince: 'HEAD^^', + })); + // Returns files from the last 2 commits + expect( + Array.from(files) + .map(filePath => path.basename(filePath)) + .sort(), + ).toEqual(['file1.txt', 'file4.txt']); + + run(`${GIT} checkout HEAD^^ -b feature-branch`, DIR); + + writeFiles(DIR, { + 'file5.txt': 'file5', + }); + run(`${GIT} add file5.txt`, DIR); + run(`${GIT} commit -m "test5"`, DIR); + + ({changedFiles: files} = await getChangedFilesForRoots(roots, { + changedSince: 'master', + })); + // Returns files from this branch but not ones that only exist on master + expect( + Array.from(files) + .map(filePath => path.basename(filePath)) + .sort(), + ).toEqual(['file5.txt']); }); test('gets changed files for hg', async () => { @@ -261,4 +292,37 @@ test('gets changed files for hg', async () => { .map(filePath => path.basename(filePath)) .sort(), ).toEqual(['file1.txt', 'file4.txt']); + + run(`${HG} add file4.txt`, DIR); + run(`${HG} commit -m "test3"`, DIR); + + ({changedFiles: files} = await getChangedFilesForRoots(roots, { + changedSince: '-3', + })); + // Returns files from the last 2 commits + expect( + Array.from(files) + .map(filePath => path.basename(filePath)) + .sort(), + ).toEqual(['file1.txt', 'file4.txt']); + + run(`${HG} bookmark master`, DIR); + // Back up and develop on a different branch + run(`${HG} checkout --rev=-2`, DIR); + + writeFiles(DIR, { + 'file5.txt': 'file5', + }); + run(`${HG} add file5.txt`, DIR); + run(`${HG} commit -m "test4"`, DIR); + + ({changedFiles: files} = await getChangedFilesForRoots(roots, { + changedSince: 'master', + })); + // Returns files from this branch but not ones that only exist on master + expect( + Array.from(files) + .map(filePath => path.basename(filePath)) + .sort(), + ).toEqual(['file5.txt']); }); diff --git a/packages/jest-changed-files/src/git.js b/packages/jest-changed-files/src/git.js index ef02aaea3dbc..5450c7ce81ec 100644 --- a/packages/jest-changed-files/src/git.js +++ b/packages/jest-changed-files/src/git.js @@ -13,7 +13,10 @@ import type {Options, SCMAdapter} from 'types/ChangedFiles'; import path from 'path'; import childProcess from 'child_process'; -const findChangedFilesUsingCommand = async (args, cwd) => { +const findChangedFilesUsingCommand = async ( + args: Array, + cwd: Path, +): Promise> => { return new Promise((resolve, reject) => { const child = childProcess.spawn('git', args, {cwd}); let stdout = ''; @@ -30,6 +33,7 @@ const findChangedFilesUsingCommand = async (args, cwd) => { resolve( stdout .split('\n') + .filter(s => s !== '') .map(changedPath => path.resolve(cwd, changedPath)), ); } @@ -45,21 +49,28 @@ const adapter: SCMAdapter = { cwd: string, options?: Options, ): Promise> => { + const changedSince: ?string = + options && (options.withAncestor ? 'HEAD^' : options.changedSince); + if (options && options.lastCommit) { return await findChangedFilesUsingCommand( ['show', '--name-only', '--pretty=%b', 'HEAD'], cwd, ); - } else if (options && options.withAncestor) { - const changed = await findChangedFilesUsingCommand( - ['diff', '--name-only', 'HEAD^'], + } else if (changedSince) { + const committed = await findChangedFilesUsingCommand( + ['log', '--name-only', '--pretty=%b', 'HEAD', `^${changedSince}`], + cwd, + ); + const staged = await findChangedFilesUsingCommand( + ['diff', '--cached', '--name-only'], cwd, ); - const untracked = await findChangedFilesUsingCommand( - ['ls-files', '--other', '--exclude-standard'], + const unstaged = await findChangedFilesUsingCommand( + ['ls-files', '--other', '--modified', '--exclude-standard'], cwd, ); - return changed.concat(untracked); + return [...committed, ...staged, ...unstaged]; } else { return await findChangedFilesUsingCommand( ['ls-files', '--other', '--modified', '--exclude-standard'], diff --git a/packages/jest-changed-files/src/hg.js b/packages/jest-changed-files/src/hg.js index b02859c66de8..ccf4b923e818 100644 --- a/packages/jest-changed-files/src/hg.js +++ b/packages/jest-changed-files/src/hg.js @@ -26,6 +26,8 @@ const adapter: SCMAdapter = { let args = ['status', '-amnu']; if (options && options.withAncestor) { args.push('--rev', 'ancestor(.^)'); + } else if (options && options.changedSince) { + args.push('--rev', `ancestor(., ${options.changedSince})`); } else if (options && options.lastCommit === true) { args = ['tip', '--template', '{files%"{file}\n"}']; } diff --git a/packages/jest-cli/src/cli/args.js b/packages/jest-cli/src/cli/args.js index 578f6aeaa41d..a0b4c9366515 100644 --- a/packages/jest-cli/src/cli/args.js +++ b/packages/jest-cli/src/cli/args.js @@ -20,7 +20,12 @@ export const check = (argv: Argv) => { ); } - for (const key of ['onlyChanged', 'lastCommit', 'changedFilesWithAncestor']) { + for (const key of [ + 'onlyChanged', + 'lastCommit', + 'changedFilesWithAncestor', + 'changedSince', + ]) { if (argv[key]) { argv.onlyChanged = true; } @@ -113,6 +118,14 @@ export const options = { 'last commit. Behaves similarly to `--onlyChanged`.', type: 'boolean', }, + changedSince: { + description: + 'Runs tests related the changes since the provided branch. If the ' + + 'current branch has diverged from the given branch, then only changes ' + + 'made locally will be tested. Behaves similarly to `--onlyChanged`.', + nargs: 1, + type: 'string', + }, ci: { default: isCI, description: diff --git a/packages/jest-cli/src/get_changed_files_promise.js b/packages/jest-cli/src/get_changed_files_promise.js index 68005453f5b6..d8bc8ed59e84 100644 --- a/packages/jest-cli/src/get_changed_files_promise.js +++ b/packages/jest-cli/src/get_changed_files_promise.js @@ -21,6 +21,7 @@ export default ( [], ); return getChangedFilesForRoots(allRootsForAllProjects, { + changedSince: globalConfig.changedSince, lastCommit: globalConfig.lastCommit, withAncestor: globalConfig.changedFilesWithAncestor, }); diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index c4dbf27c90c3..ccfa0e2db4bf 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -94,6 +94,7 @@ const getConfigs = ( globalConfig: Object.freeze({ bail: options.bail, changedFilesWithAncestor: options.changedFilesWithAncestor, + changedSince: options.changedSince, collectCoverage: options.collectCoverage, collectCoverageFrom: options.collectCoverageFrom, collectCoverageOnlyFrom: options.collectCoverageOnlyFrom, diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 1349360745b1..1bd405676c10 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -461,6 +461,7 @@ export default function normalize(options: InitialOptions, argv: Argv) { case 'bail': case 'browser': case 'cache': + case 'changedSince': case 'changedFilesWithAncestor': case 'clearMocks': case 'collectCoverage': diff --git a/packages/jest-config/src/valid_config.js b/packages/jest-config/src/valid_config.js index 43947aee5b40..dba9ab45f210 100644 --- a/packages/jest-config/src/valid_config.js +++ b/packages/jest-config/src/valid_config.js @@ -21,6 +21,7 @@ export default ({ cache: true, cacheDirectory: '/tmp/user/jest', changedFilesWithAncestor: false, + changedSince: '', clearMocks: false, collectCoverage: true, collectCoverageFrom: ['src', '!public'], diff --git a/test_utils.js b/test_utils.js index 7349b7d33dba..5f9645826635 100644 --- a/test_utils.js +++ b/test_utils.js @@ -14,6 +14,7 @@ import type {GlobalConfig, ProjectConfig} from 'types/Config'; const DEFAULT_GLOBAL_CONFIG: GlobalConfig = { bail: false, changedFilesWithAncestor: false, + changedSince: '', collectCoverage: false, collectCoverageFrom: [], collectCoverageOnlyFrom: null, diff --git a/types/Argv.js b/types/Argv.js index 137d46ce2591..a46c5aa14bd2 100644 --- a/types/Argv.js +++ b/types/Argv.js @@ -18,6 +18,7 @@ export type Argv = {| cache: boolean, cacheDirectory: string, changedFilesWithAncestor: boolean, + changedSince: string, clearMocks: boolean, ci: boolean, collectCoverage: boolean, diff --git a/types/ChangedFiles.js b/types/ChangedFiles.js index e69fa343899e..1b7249710a9b 100644 --- a/types/ChangedFiles.js +++ b/types/ChangedFiles.js @@ -12,6 +12,7 @@ import type {Path} from 'types/Config'; export type Options = {| lastCommit?: boolean, withAncestor?: boolean, + changedSince?: string, |}; export type ChangedFiles = Set; diff --git a/types/Config.js b/types/Config.js index e9625660445f..f5ca383ad1b1 100644 --- a/types/Config.js +++ b/types/Config.js @@ -77,6 +77,7 @@ export type InitialOptions = { cacheDirectory?: Path, clearMocks?: boolean, changedFilesWithAncestor?: boolean, + changedSince?: string, collectCoverage?: boolean, collectCoverageFrom?: Array, collectCoverageOnlyFrom?: {[key: string]: boolean}, @@ -159,6 +160,7 @@ export type SnapshotUpdateState = 'all' | 'new' | 'none'; export type GlobalConfig = {| bail: boolean, + changedSince: string, changedFilesWithAncestor: boolean, collectCoverage: boolean, collectCoverageFrom: Array,