diff --git a/CHANGELOG.md b/CHANGELOG.md index f27a8f2ebba9..1082888a5ec0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - `[jest-cli]` Do not execute any `globalSetup` or `globalTeardown` if there are no tests to execute ([#7745](https://github.com/facebook/jest/pull/7745)) - `[jest-runtime]` Lock down version of `write-file-atomic` ([#7725](https://github.com/facebook/jest/pull/7725)) - `[jest-cli]` Print log entries when logging happens after test environment is torn down ([#7731](https://github.com/facebook/jest/pull/7731)) +- `[jest-config]` Do not use a uuid as `name` since that breaks caching ([#7746](https://github.com/facebook/jest/pull/7746)) ### Chore & Maintenance diff --git a/e2e/__tests__/multiProjectRunner.test.js b/e2e/__tests__/multiProjectRunner.test.js index b3438cb90905..7fc070a53c32 100644 --- a/e2e/__tests__/multiProjectRunner.test.js +++ b/e2e/__tests__/multiProjectRunner.test.js @@ -400,40 +400,100 @@ test('Does transform files with the corresponding project transformer', () => { expect(stderr).toMatch('PASS project2/__tests__/project2.test.js'); }); -test("doesn't bleed module file extensions resolution with multiple workers", () => { - writeFiles(DIR, { - '.watchmanconfig': '', - 'file.js': 'module.exports = "file1"', - 'file.p2.js': 'module.exports = "file2"', - 'package.json': '{}', - 'project1/__tests__/project1.test.js': ` +describe("doesn't bleed module file extensions resolution with multiple workers", () => { + test('external config files', () => { + writeFiles(DIR, { + '.watchmanconfig': '', + 'file.js': 'module.exports = "file1"', + 'file.p2.js': 'module.exports = "file2"', + 'package.json': '{}', + 'project1/__tests__/project1.test.js': ` const file = require('../../file'); test('file 1', () => expect(file).toBe('file1')); `, - 'project1/jest.config.js': ` + 'project1/jest.config.js': ` module.exports = { rootDir: '..', };`, - 'project2/__tests__/project2.test.js': ` + 'project2/__tests__/project2.test.js': ` const file = require('../../file'); test('file 2', () => expect(file).toBe('file2')); `, - 'project2/jest.config.js': ` + 'project2/jest.config.js': ` module.exports = { rootDir: '..', moduleFileExtensions: ['p2.js', 'js'] };`, + }); + + const {stdout: configOutput} = runJest(DIR, [ + '--show-config', + '--projects', + 'project1', + 'project2', + ]); + + const {configs} = JSON.parse(configOutput); + + expect(configs).toHaveLength(2); + + const [{name: name1}, {name: name2}] = configs; + + expect(name1).toEqual(expect.any(String)); + expect(name2).toEqual(expect.any(String)); + expect(name1).toHaveLength(32); + expect(name2).toHaveLength(32); + expect(name1).not.toEqual(name2); + + const {stderr} = runJest(DIR, [ + '--no-watchman', + '-w=2', + '--projects', + 'project1', + 'project2', + ]); + + expect(stderr).toMatch('Ran all test suites in 2 projects.'); + expect(stderr).toMatch('PASS project1/__tests__/project1.test.js'); + expect(stderr).toMatch('PASS project2/__tests__/project2.test.js'); }); - const {stderr} = runJest(DIR, [ - '--no-watchman', - '-w=2', - '--projects', - 'project1', - 'project2', - ]); + test('inline config files', () => { + writeFiles(DIR, { + '.watchmanconfig': '', + 'file.js': 'module.exports = "file1"', + 'file.p2.js': 'module.exports = "file2"', + 'package.json': JSON.stringify({ + jest: {projects: [{}, {moduleFileExtensions: ['p2.js', 'js']}]}, + }), + 'project1/__tests__/project1.test.js': ` + const file = require('../../file'); + test('file 1', () => expect(file).toBe('file1')); + `, + 'project2/__tests__/project2.test.js': ` + const file = require('../../file'); + test('file 2', () => expect(file).toBe('file2')); + `, + }); - expect(stderr).toMatch('Ran all test suites in 2 projects.'); - expect(stderr).toMatch('PASS project1/__tests__/project1.test.js'); - expect(stderr).toMatch('PASS project2/__tests__/project2.test.js'); + const {stdout: configOutput} = runJest(DIR, ['--show-config']); + + const {configs} = JSON.parse(configOutput); + + expect(configs).toHaveLength(2); + + const [{name: name1}, {name: name2}] = configs; + + expect(name1).toEqual(expect.any(String)); + expect(name2).toEqual(expect.any(String)); + expect(name1).toHaveLength(32); + expect(name2).toHaveLength(32); + expect(name1).not.toEqual(name2); + + const {stderr} = runJest(DIR, ['--no-watchman', '-w=2']); + + expect(stderr).toMatch('Ran all test suites in 2 projects.'); + expect(stderr).toMatch('PASS project1/__tests__/project1.test.js'); + expect(stderr).toMatch('PASS project2/__tests__/project2.test.js'); + }); }); diff --git a/packages/jest-config/package.json b/packages/jest-config/package.json index 00e62a391070..e9a219bf1239 100644 --- a/packages/jest-config/package.json +++ b/packages/jest-config/package.json @@ -23,8 +23,7 @@ "jest-validate": "^24.0.0", "micromatch": "^3.1.10", "pretty-format": "^24.0.0", - "realpath-native": "^1.0.2", - "uuid": "^3.3.2" + "realpath-native": "^1.0.2" }, "engines": { "node": ">= 6" diff --git a/packages/jest-config/src/__tests__/normalize.test.js b/packages/jest-config/src/__tests__/normalize.test.js index 9090ec672c35..a1d37311d373 100644 --- a/packages/jest-config/src/__tests__/normalize.test.js +++ b/packages/jest-config/src/__tests__/normalize.test.js @@ -6,16 +6,17 @@ * */ +import crypto from 'crypto'; +import path from 'path'; import {escapeStrForRegex} from 'jest-regex-util'; import normalize from '../normalize'; -jest.mock('jest-resolve'); -jest.mock('path', () => jest.requireActual('path').posix); +import {DEFAULT_JS_PATTERN} from '../constants'; -const path = require('path'); -const DEFAULT_JS_PATTERN = require('../constants').DEFAULT_JS_PATTERN; const DEFAULT_CSS_PATTERN = '^.+\\.(css)$'; +jest.mock('jest-resolve').mock('path', () => jest.requireActual('path').posix); + let root; let expectedPathFooBar; let expectedPathFooQux; @@ -46,16 +47,34 @@ beforeEach(() => { require('jest-resolve').findNodeModule = findNodeModule; }); -it('assigns a random 32-byte hash as a name to avoid clashes', () => { +it('picks a name based on the rootDir', () => { const rootDir = '/root/path/foo'; - const {name: name1} = normalize({rootDir}, {}).options; - const {name: name2} = normalize({rootDir}, {}).options; - - expect(name1).toEqual(expect.any(String)); - expect(name1).toHaveLength(32); - expect(name2).toEqual(expect.any(String)); - expect(name2).toHaveLength(32); - expect(name1).not.toBe(name2); + const expected = crypto + .createHash('md5') + .update('/root/path/foo') + .update(String(Infinity)) + .digest('hex'); + expect( + normalize( + { + rootDir, + }, + {}, + ).options.name, + ).toBe(expected); +}); + +it('keeps custom project name based on the projects rootDir', () => { + const name = 'test'; + const options = normalize( + { + projects: [{name, rootDir: '/path/to/foo'}], + rootDir: '/root/path/baz', + }, + {}, + ); + + expect(options.options.projects[0].name).toBe(name); }); it('keeps custom names based on the rootDir', () => { @@ -70,6 +89,16 @@ it('keeps custom names based on the rootDir', () => { ).toBe('custom-name'); }); +it('minimal config is stable across runs', () => { + const firstNormalization = normalize({rootDir: '/root/path/foo'}, {}); + const secondNormalization = normalize({rootDir: '/root/path/foo'}, {}); + + expect(firstNormalization).toEqual(secondNormalization); + expect(JSON.stringify(firstNormalization)).toBe( + JSON.stringify(secondNormalization), + ); +}); + it('sets coverageReporters correctly when argv.json is set', () => { expect( normalize( diff --git a/packages/jest-config/src/index.js b/packages/jest-config/src/index.js index 16affb204add..e9ba698287be 100644 --- a/packages/jest-config/src/index.js +++ b/packages/jest-config/src/index.js @@ -39,6 +39,7 @@ export function readConfig( // read individual configs for every project. skipArgvConfigOption?: boolean, parentConfigPath: ?Path, + projectIndex?: number = Infinity, ): { configPath: ?Path, globalConfig: GlobalConfig, @@ -86,7 +87,13 @@ export function readConfig( rawOptions = readConfigFileAndSetRootDir(configPath); } - const {options, hasDeprecationWarnings} = normalize(rawOptions, argv); + const {options, hasDeprecationWarnings} = normalize( + rawOptions, + argv, + configPath, + projectIndex, + ); + const {globalConfig, projectConfig} = groupOptions(options); return { configPath, @@ -210,7 +217,7 @@ const groupOptions = ( }), }); -const ensureNoDuplicateConfigs = (parsedConfigs, projects, rootConfigPath) => { +const ensureNoDuplicateConfigs = (parsedConfigs, projects) => { const configPathMap = new Map(); for (const config of parsedConfigs) { @@ -297,9 +304,11 @@ export function readConfigs( return true; }) - .map(root => readConfig(argv, root, true, configPath)); + .map((root, projectIndex) => + readConfig(argv, root, true, configPath, projectIndex), + ); - ensureNoDuplicateConfigs(parsedConfigs, projects, configPath); + ensureNoDuplicateConfigs(parsedConfigs, projects); configs = parsedConfigs.map(({projectConfig}) => projectConfig); if (!hasDeprecationWarnings) { hasDeprecationWarnings = parsedConfigs.some( diff --git a/packages/jest-config/src/normalize.js b/packages/jest-config/src/normalize.js index 282315a2bfa1..6abbe34122e3 100644 --- a/packages/jest-config/src/normalize.js +++ b/packages/jest-config/src/normalize.js @@ -13,11 +13,11 @@ import type { DefaultOptions, ReporterConfig, GlobalConfig, + Path, ProjectConfig, } from 'types/Config'; import crypto from 'crypto'; -import uuid from 'uuid/v4'; import glob from 'glob'; import path from 'path'; import {ValidationError, validate} from 'jest-validate'; @@ -264,12 +264,18 @@ const normalizePreprocessor = (options: InitialOptions): InitialOptions => { return options; }; -const normalizeMissingOptions = (options: InitialOptions): InitialOptions => { +const normalizeMissingOptions = ( + options: InitialOptions, + configPath: ?Path, + projectIndex: number, +): InitialOptions => { if (!options.name) { options.name = crypto .createHash('md5') .update(options.rootDir) - .update(uuid()) + // In case we load config from some path that has the same root dir + .update(configPath || '') + .update(String(projectIndex)) .digest('hex'); } @@ -375,7 +381,12 @@ const showTestPathPatternError = (testPathPattern: string) => { ); }; -export default function normalize(options: InitialOptions, argv: Argv) { +export default function normalize( + options: InitialOptions, + argv: Argv, + configPath: ?Path, + projectIndex?: number = Infinity, +) { const {hasDeprecationWarnings} = validate(options, { comment: DOCUMENTATION_NOTE, deprecatedConfig: DEPRECATED_CONFIG, @@ -394,7 +405,11 @@ export default function normalize(options: InitialOptions, argv: Argv) { options = normalizePreprocessor( normalizeReporters( - normalizeMissingOptions(normalizeRootDir(setFromArgv(options, argv))), + normalizeMissingOptions( + normalizeRootDir(setFromArgv(options, argv)), + configPath, + projectIndex, + ), ), );