Skip to content

Commit

Permalink
fix: make sure generated name in config is stable across runs of Je…
Browse files Browse the repository at this point in the history
…st (jestjs#7746)
  • Loading branch information
SimenB authored and captain-yossarian committed Jul 18, 2019
1 parent 167b543 commit eb58be8
Show file tree
Hide file tree
Showing 6 changed files with 157 additions and 44 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
100 changes: 80 additions & 20 deletions e2e/__tests__/multiProjectRunner.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
});
});
3 changes: 1 addition & 2 deletions packages/jest-config/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
55 changes: 42 additions & 13 deletions packages/jest-config/src/__tests__/normalize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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', () => {
Expand All @@ -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(
Expand Down
17 changes: 13 additions & 4 deletions packages/jest-config/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export function readConfig(
// read individual configs for every project.
skipArgvConfigOption?: boolean,
parentConfigPath: ?Path,
projectIndex?: number = Infinity,
): {
configPath: ?Path,
globalConfig: GlobalConfig,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -210,7 +217,7 @@ const groupOptions = (
}),
});

const ensureNoDuplicateConfigs = (parsedConfigs, projects, rootConfigPath) => {
const ensureNoDuplicateConfigs = (parsedConfigs, projects) => {
const configPathMap = new Map();

for (const config of parsedConfigs) {
Expand Down Expand Up @@ -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(
Expand Down
25 changes: 20 additions & 5 deletions packages/jest-config/src/normalize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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');
}

Expand Down Expand Up @@ -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,
Expand All @@ -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,
),
),
);

Expand Down

0 comments on commit eb58be8

Please sign in to comment.