diff --git a/package.json b/package.json index a30de48f6446a..b16c0373b4f5e 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index d5a56d37fde45..42ccc6cdd4641 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -65,6 +65,7 @@ RUNTIME_DEPS = [ "@npm//jest-styled-components", "@npm//joi", "@npm//js-yaml", + "@npm//minimatch", "@npm//mustache", "@npm//normalize-path", "@npm//prettier", @@ -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", diff --git a/packages/kbn-test/src/jest/configs/get_all_jest_paths.ts b/packages/kbn-test/src/jest/configs/get_all_jest_paths.ts new file mode 100644 index 0000000000000..63a829225ca60 --- /dev/null +++ b/packages/kbn-test/src/jest/configs/get_all_jest_paths.ts @@ -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(); + const configs = new Set(); + + 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, + }; +} diff --git a/packages/kbn-test/src/jest/configs/get_tests_for_config_paths.ts b/packages/kbn-test/src/jest/configs/get_tests_for_config_paths.ts new file mode 100644 index 0000000000000..9d0105977a12e --- /dev/null +++ b/packages/kbn-test/src/jest/configs/get_tests_for_config_paths.ts @@ -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; +} + +export async function getTestsForConfigPaths( + configPaths: Iterable +): Promise { + 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)), + }; + }); +} diff --git a/packages/kbn-test/src/jest/configs/index.ts b/packages/kbn-test/src/jest/configs/index.ts index 155c385ec761d..9cb9ffc5877ec 100644 --- a/packages/kbn-test/src/jest/configs/index.ts +++ b/packages/kbn-test/src/jest/configs/index.ts @@ -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'; diff --git a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts index 6ada077c842e7..5adbe0afdbef0 100644 --- a/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts +++ b/packages/kbn-test/src/jest/run_check_jest_configs_cli.ts @@ -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: ['/{{{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: ['/{{{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) => [...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(); + 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(); + 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', } ); } diff --git a/src/core/jest.config.js b/src/core/jest.config.js deleted file mode 100644 index 66e23cc0ab12b..0000000000000 --- a/src/core/jest.config.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * 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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../..', - roots: ['/src/core'], - testRunner: 'jasmine2', -}; diff --git a/src/plugins/chart_expressions/jest.config.js b/src/plugins/chart_expressions/jest.config.js deleted file mode 100644 index 503ef441c0359..0000000000000 --- a/src/plugins/chart_expressions/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/chart_expressions'], -}; diff --git a/src/plugins/vis_types/jest.config.js b/src/plugins/vis_types/jest.config.js deleted file mode 100644 index af7f2b462b89f..0000000000000 --- a/src/plugins/vis_types/jest.config.js +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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. - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../..', - roots: ['/src/plugins/vis_types'], -}; diff --git a/yarn.lock b/yarn.lock index a4fea3c25b708..5c0b3a92ef801 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== @@ -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== @@ -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==