Skip to content
This repository has been archived by the owner on Mar 31, 2024. It is now read-only.

Commit

Permalink
[jest] refactor config check (elastic#135960)
Browse files Browse the repository at this point in the history
  • Loading branch information
Spencer committed Jul 11, 2022
1 parent a9f1f89 commit 4d47a9d
Show file tree
Hide file tree
Showing 10 changed files with 201 additions and 106 deletions.
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,7 @@
"@istanbuljs/schema": "^0.1.2",
"@jest/console": "^26.6.2",
"@jest/reporters": "^26.6.2",
"@jest/types": "^26",
"@kbn/axe-config": "link:bazel-bin/packages/kbn-axe-config",
"@kbn/babel-plugin-synthetic-packages": "link:bazel-bin/packages/kbn-babel-plugin-synthetic-packages",
"@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset",
Expand Down Expand Up @@ -896,10 +897,12 @@
"jest-canvas-mock": "^2.3.1",
"jest-circus": "^26.6.3",
"jest-cli": "^26.6.3",
"jest-config": "^26",
"jest-diff": "^26.6.2",
"jest-environment-jsdom": "^26.6.2",
"jest-environment-jsdom-thirteen": "^1.0.1",
"jest-raw-loader": "^1.0.1",
"jest-runtime": "^26",
"jest-silent-reporter": "^0.5.0",
"jest-snapshot": "^26.6.2",
"jest-specific-snapshot": "2.0.0",
Expand Down
2 changes: 2 additions & 0 deletions packages/kbn-test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ RUNTIME_DEPS = [
"@npm//jest-styled-components",
"@npm//joi",
"@npm//js-yaml",
"@npm//minimatch",
"@npm//mustache",
"@npm//normalize-path",
"@npm//prettier",
Expand Down Expand Up @@ -113,6 +114,7 @@ TYPES_DEPS = [
"@npm//@types/js-yaml",
"@npm//@types/joi",
"@npm//@types/lodash",
"@npm//@types/minimatch",
"@npm//@types/mustache",
"@npm//@types/normalize-path",
"@npm//@types/node",
Expand Down
74 changes: 74 additions & 0 deletions packages/kbn-test/src/jest/configs/get_all_jest_paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import Fs from 'fs';
import Path from 'path';

import execa from 'execa';
import minimatch from 'minimatch';
import { REPO_ROOT } from '@kbn/utils';

// @ts-expect-error jest-preset is necessarily a JS file
import { testMatch } from '../../../jest-preset';

const UNIT_CONFIG_NAME = 'jest.config.js';
const INTEGRATION_CONFIG_NAME = 'jest.integration.config.js';

export async function getAllJestPaths() {
const proc = await execa('git', ['ls-files', '-comt', '--exclude-standard'], {
cwd: REPO_ROOT,
stdio: ['ignore', 'pipe', 'pipe'],
buffer: true,
});

const testsRe = (testMatch as string[]).map((p) => minimatch.makeRe(p));
const classify = (rel: string) => {
if (testsRe.some((re) => re.test(rel))) {
return 'test' as const;
}

const basename = Path.basename(rel);
return basename === UNIT_CONFIG_NAME || basename === INTEGRATION_CONFIG_NAME
? ('config' as const)
: undefined;
};

const tests = new Set<string>();
const configs = new Set<string>();

for (const line of proc.stdout.split('\n').map((l) => l.trim())) {
if (!line) {
continue;
}

const rel = line.slice(2); // trim the single char status from the line
const type = classify(rel);

if (!type) {
continue;
}

const set = type === 'test' ? tests : configs;
const abs = Path.resolve(REPO_ROOT, rel);

if (line.startsWith('C ')) {
// this line indicates that the previous path is changed in the working tree, so we need to determine if
// it was deleted, and if so, remove it from the set we added it to
if (!Fs.existsSync(abs)) {
set.delete(abs);
}
} else {
set.add(abs);
}
}

return {
tests,
configs,
};
}
52 changes: 52 additions & 0 deletions packages/kbn-test/src/jest/configs/get_tests_for_config_paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { readConfig } from 'jest-config';
import { createContext } from 'jest-runtime';
import { SearchSource } from 'jest';
import { asyncMapWithLimit } from '@kbn/std';

const EMPTY_ARGV = {
$0: '',
_: [],
};

const NO_WARNINGS_CONSOLE = {
...console,
warn() {
// ignore haste-map warnings
},
};

export interface TestsForConfigPath {
path: string;
testPaths: Set<string>;
}

export async function getTestsForConfigPaths(
configPaths: Iterable<string>
): Promise<TestsForConfigPath[]> {
return await asyncMapWithLimit(configPaths, 60, async (path) => {
const config = await readConfig(EMPTY_ARGV, path);
const searchSource = new SearchSource(
await createContext(config.projectConfig, {
maxWorkers: 1,
watchman: false,
watch: false,
console: NO_WARNINGS_CONSOLE,
})
);

const results = await searchSource.getTestPaths(config.globalConfig, undefined, undefined);

return {
path,
testPaths: new Set(results.tests.map((t) => t.path)),
};
});
}
3 changes: 2 additions & 1 deletion packages/kbn-test/src/jest/configs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@
* Side Public License, v 1.
*/

export * from './jest_configs';
export * from './get_all_jest_paths';
export * from './get_tests_for_config_paths';
127 changes: 65 additions & 62 deletions packages/kbn-test/src/jest/run_check_jest_configs_cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,89 +6,92 @@
* Side Public License, v 1.
*/

import { writeFileSync } from 'fs';
import path from 'path';
import Mustache from 'mustache';

import Path from 'path';
import { run } from '@kbn/dev-cli-runner';
import { createFailError } from '@kbn/dev-cli-errors';
import { REPO_ROOT } from '@kbn/utils';
import { getAllRepoRelativeBazelPackageDirs } from '@kbn/bazel-packages';

import { JestConfigs, CONFIG_NAMES } from './configs';
import { getAllJestPaths, getTestsForConfigPaths } from './configs';

const unitTestingTemplate: string = `module.exports = {
preset: '@kbn/test/jest_node',
rootDir: '{{{relToRoot}}}',
roots: ['<rootDir>/{{{modulePath}}}'],
};
`;
const fmtMs = (ms: number) => {
if (ms < 1000) {
return `${Math.round(ms)} ms`;
}

const integrationTestingTemplate: string = `module.exports = {
preset: '@kbn/test/jest_integration_node',
rootDir: '{{{relToRoot}}}',
roots: ['<rootDir>/{{{modulePath}}}'],
return `${(Math.round(ms) / 1000).toFixed(2)} s`;
};
`;

const roots: string[] = [
'x-pack/plugins/security_solution/public',
'x-pack/plugins/security_solution/server',
'x-pack/plugins/security_solution',
'x-pack/plugins',
'src/plugins',
'test',
'src/core',
'src',
...getAllRepoRelativeBazelPackageDirs(),
];
const fmtList = (list: Iterable<string>) => [...list].map((i) => ` - ${i}`).join('\n');

export async function runCheckJestConfigsCli() {
run(
async ({ flags: { fix = false }, log }) => {
const jestConfigs = new JestConfigs(REPO_ROOT, roots);
async ({ log }) => {
const start = performance.now();

const jestPaths = await getAllJestPaths();
const allConfigs = await getTestsForConfigPaths(jestPaths.configs);
const missingConfigs = new Set<string>();
const multipleConfigs = new Set<{ configs: string[]; rel: string }>();

for (const testPath of jestPaths.tests) {
const configs = allConfigs
.filter((c) => c.testPaths.has(testPath))
.map((c) => Path.relative(REPO_ROOT, c.path))
.sort((a, b) => Path.dirname(a).localeCompare(Path.dirname(b)));

const missing = await jestConfigs.allMissing();
if (configs.length === 0) {
missingConfigs.add(Path.relative(REPO_ROOT, testPath));
} else if (configs.length > 1) {
multipleConfigs.add({
configs,
rel: Path.relative(REPO_ROOT, testPath),
});
}
}

if (missing.length) {
if (missingConfigs.size) {
log.error(
`The following Jest config files do not exist for which there are test files for:\n${[
...missing,
]
.map((file) => ` - ${file}`)
.join('\n')}`
`The following test files are not selected by any jest config file:\n${fmtList(
missingConfigs
)}`
);
}

if (fix) {
missing.forEach((file) => {
const template = file.endsWith(CONFIG_NAMES.unit)
? unitTestingTemplate
: integrationTestingTemplate;

const modulePath = path.dirname(file);
const content = Mustache.render(template, {
relToRoot: path.relative(modulePath, '.'),
modulePath,
if (multipleConfigs.size) {
const overlaps = new Map<string, { configs: string[]; rels: string[] }>();
for (const { configs, rel } of multipleConfigs) {
const key = configs.join(':');
const group = overlaps.get(key);
if (group) {
group.rels.push(rel);
} else {
overlaps.set(key, {
configs,
rels: [rel],
});

writeFileSync(file, content);
log.info('created %s', file);
});
} else {
throw createFailError(
`Run 'node scripts/check_jest_configs --fix' to create the missing config files`
);
}
}

const list = [...overlaps.values()]
.map(
({ configs, rels }) =>
`configs: ${configs
.map((c) => Path.relative(REPO_ROOT, c))
.join(', ')}\ntests:\n${fmtList(rels)}`
)
.join('\n\n');

log.error(`The following test files are selected by multiple config files:\n${list}`);
}

if (missingConfigs.size || multipleConfigs.size) {
throw createFailError('Please resolve the previously logged issues.');
}

log.success('Checked all jest config files in', fmtMs(performance.now() - start));
},
{
description: 'Check that all test files are covered by a Jest config',
flags: {
boolean: ['fix'],
help: `
--fix Attempt to create missing config files
`,
},
description: 'Check that all test files are covered by one, and only one, Jest config',
}
);
}
14 changes: 0 additions & 14 deletions src/core/jest.config.js

This file was deleted.

13 changes: 0 additions & 13 deletions src/plugins/chart_expressions/jest.config.js

This file was deleted.

13 changes: 0 additions & 13 deletions src/plugins/vis_types/jest.config.js

This file was deleted.

6 changes: 3 additions & 3 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2567,7 +2567,7 @@
"@types/istanbul-reports" "^1.1.1"
"@types/yargs" "^13.0.0"

"@jest/types@^26.6.2":
"@jest/types@^26", "@jest/types@^26.6.2":
version "26.6.2"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e"
integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==
Expand Down Expand Up @@ -17829,7 +17829,7 @@ jest-cli@^26.6.3:
prompts "^2.0.1"
yargs "^15.4.1"

jest-config@^26.6.3:
jest-config@^26, jest-config@^26.6.3:
version "26.6.3"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349"
integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==
Expand Down Expand Up @@ -18242,7 +18242,7 @@ jest-runner@^26.6.3:
source-map-support "^0.5.6"
throat "^5.0.0"

jest-runtime@^26.6.3:
jest-runtime@^26, jest-runtime@^26.6.3:
version "26.6.3"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b"
integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==
Expand Down

0 comments on commit 4d47a9d

Please sign in to comment.