mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
refactor jest config validation logic (#134496)
This commit is contained in:
parent
07e4ca46ef
commit
1656fe9304
12 changed files with 450 additions and 234 deletions
|
@ -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",
|
||||
|
|
|
@ -7,16 +7,27 @@
|
|||
*/
|
||||
import execa from 'execa';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
import Path from 'path';
|
||||
import { REPO_ROOT } from '@kbn/utils';
|
||||
import { run } from '@kbn/dev-cli-runner';
|
||||
import { createFailError } from '@kbn/dev-cli-errors';
|
||||
|
||||
import { FTR_CONFIGS_MANIFEST_PATHS } from './ftr_configs_manifest';
|
||||
|
||||
const THIS_PATH = Path.resolve(
|
||||
REPO_ROOT,
|
||||
'packages/kbn-test/src/functional_test_runner/lib/config/run_check_ftr_configs_cli.ts'
|
||||
);
|
||||
const THIS_REL = Path.relative(REPO_ROOT, THIS_PATH);
|
||||
|
||||
const IGNORED_PATHS = [
|
||||
THIS_PATH,
|
||||
Path.resolve(REPO_ROOT, 'packages/kbn-test/src/jest/run_check_jest_configs_cli.ts'),
|
||||
];
|
||||
|
||||
export async function runCheckFtrConfigsCli() {
|
||||
run(
|
||||
async () => {
|
||||
async ({ log }) => {
|
||||
const { stdout } = await execa('git', [
|
||||
'ls-tree',
|
||||
'--full-tree',
|
||||
|
@ -28,10 +39,10 @@ export async function runCheckFtrConfigsCli() {
|
|||
const files = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.map((file) => resolve(REPO_ROOT, file));
|
||||
.map((file) => Path.resolve(REPO_ROOT, file));
|
||||
|
||||
const possibleConfigs = files.filter((file) => {
|
||||
if (file.includes('run_check_ftr_configs_cli.ts')) {
|
||||
if (IGNORED_PATHS.includes(file)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -56,12 +67,15 @@ export async function runCheckFtrConfigsCli() {
|
|||
.match(/(testRunner)|(testFiles)/);
|
||||
});
|
||||
|
||||
for (const config of possibleConfigs) {
|
||||
if (!FTR_CONFIGS_MANIFEST_PATHS.includes(config)) {
|
||||
throw createFailError(
|
||||
`${config} looks like a new FTR config. Please add it to .buildkite/ftr_configs.yml. If it's not an FTR config, please contact #kibana-operations`
|
||||
);
|
||||
}
|
||||
const invalid = possibleConfigs.filter((path) => !FTR_CONFIGS_MANIFEST_PATHS.includes(path));
|
||||
if (invalid.length) {
|
||||
const invalidList = invalid.map((path) => Path.relative(REPO_ROOT, path)).join('\n - ');
|
||||
log.error(
|
||||
`The following files look like FTR configs which are not listed in .buildkite/ftr_configs.yml:\n - ${invalidList}`
|
||||
);
|
||||
throw createFailError(
|
||||
`Please add the listed paths to .buildkite/ftr_configs.yml. If it's not an FTR config, you can add it to the IGNORED_PATHS in ${THIS_REL} or contact #kibana-operations`
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`jestConfigs #expected throws if test file outside root 1`] = `[Error: Test file (bad.test.js) can not exist outside roots (packages/b/nested, packages). Move it to a root or configure additional root.]`;
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 mockFs from 'mock-fs';
|
||||
|
||||
import { GroupedTestFiles } from './group_test_files';
|
||||
import {
|
||||
findMissingConfigFiles,
|
||||
INTEGRATION_CONFIG_NAME,
|
||||
UNIT_CONFIG_NAME,
|
||||
} from './find_missing_config_files';
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFs({
|
||||
'/packages': {
|
||||
a: {
|
||||
[UNIT_CONFIG_NAME]: '{}',
|
||||
},
|
||||
},
|
||||
'/src': {
|
||||
c: {
|
||||
[UNIT_CONFIG_NAME]: '{}',
|
||||
},
|
||||
d: {
|
||||
[INTEGRATION_CONFIG_NAME]: '{}',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(mockFs.restore);
|
||||
|
||||
it('returns a list of config files which are not found on disk, or are not files', async () => {
|
||||
const groups: GroupedTestFiles = new Map([
|
||||
[
|
||||
{
|
||||
type: 'pkg',
|
||||
path: '/packages/a',
|
||||
},
|
||||
{
|
||||
unit: ['/packages/a/test.js'],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'pkg',
|
||||
path: '/packages/b',
|
||||
},
|
||||
{
|
||||
integration: ['/packages/b/integration_tests/test.js'],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'src',
|
||||
path: '/src/c',
|
||||
},
|
||||
{
|
||||
unit: ['/src/c/test.js'],
|
||||
integration: ['/src/c/integration_tests/test.js'],
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'src',
|
||||
path: '/src/d',
|
||||
},
|
||||
{
|
||||
unit: ['/src/d/test.js'],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
await expect(findMissingConfigFiles(groups)).resolves.toEqual([
|
||||
'/packages/b/jest.integration.config.js',
|
||||
'/src/c/jest.integration.config.js',
|
||||
'/src/d/jest.config.js',
|
||||
]);
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* 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 Fsp from 'fs/promises';
|
||||
import Path from 'path';
|
||||
|
||||
import { asyncMapWithLimit } from '@kbn/std';
|
||||
|
||||
import { GroupedTestFiles } from './group_test_files';
|
||||
|
||||
export const UNIT_CONFIG_NAME = 'jest.config.js';
|
||||
export const INTEGRATION_CONFIG_NAME = 'jest.integration.config.js';
|
||||
|
||||
async function isFile(path: string) {
|
||||
try {
|
||||
const stats = await Fsp.stat(path);
|
||||
return stats.isFile();
|
||||
} catch (error) {
|
||||
if (error.code === 'ENOENT') {
|
||||
return false;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function findMissingConfigFiles(groups: GroupedTestFiles) {
|
||||
const expectedConfigs = [...groups].flatMap(([owner, tests]) => {
|
||||
const configs: string[] = [];
|
||||
if (tests.unit?.length) {
|
||||
configs.push(Path.resolve(owner.path, UNIT_CONFIG_NAME));
|
||||
}
|
||||
if (tests.integration?.length) {
|
||||
configs.push(Path.resolve(owner.path, INTEGRATION_CONFIG_NAME));
|
||||
}
|
||||
return configs;
|
||||
});
|
||||
|
||||
return (
|
||||
await asyncMapWithLimit(expectedConfigs, 20, async (path) =>
|
||||
!(await isFile(path)) ? [path] : []
|
||||
)
|
||||
).flat();
|
||||
}
|
32
packages/kbn-test/src/jest/configs/get_all_test_files.ts
Normal file
32
packages/kbn-test/src/jest/configs/get_all_test_files.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* 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 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';
|
||||
|
||||
export async function getAllTestFiles() {
|
||||
const proc = await execa('git', ['ls-files', '-co', '--exclude-standard'], {
|
||||
cwd: REPO_ROOT,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
buffer: true,
|
||||
});
|
||||
|
||||
const patterns: RegExp[] = testMatch.map((p: string) => minimatch.makeRe(p));
|
||||
|
||||
return proc.stdout
|
||||
.split('\n')
|
||||
.flatMap((l) => l.trim() || [])
|
||||
.filter((l) => patterns.some((p) => p.test(l)))
|
||||
.map((p) => Path.resolve(REPO_ROOT, p));
|
||||
}
|
117
packages/kbn-test/src/jest/configs/group_test_files.test.ts
Normal file
117
packages/kbn-test/src/jest/configs/group_test_files.test.ts
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 { groupTestFiles } from './group_test_files';
|
||||
|
||||
it('properly assigns tests to src roots and packages based on location', () => {
|
||||
const grouped = groupTestFiles(
|
||||
[
|
||||
'/packages/pkg1/test.js',
|
||||
'/packages/pkg1/integration_tests/test.js',
|
||||
'/packages/pkg2/integration_tests/test.js',
|
||||
'/packages/group/pkg3/test.js',
|
||||
'/packages/group/subgroup/pkg4/test.js',
|
||||
'/packages/group/subgroup/pkg4/integration_tests/test.js',
|
||||
'/src/a/integration_tests/test.js',
|
||||
'/src/b/test.js',
|
||||
'/tests/b/test.js',
|
||||
'/src/group/c/test.js',
|
||||
'/src/group/c/integration_tests/test.js',
|
||||
'/src/group/subgroup/d/test.js',
|
||||
'/src/group/subgroup/d/integration_tests/test.js',
|
||||
],
|
||||
['/src/group/subgroup', '/src/group', '/src'],
|
||||
['/packages/pkg1', '/packages/pkg2', '/packages/group/pkg3', '/packages/group/subgroup/pkg4']
|
||||
);
|
||||
|
||||
expect(grouped).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"grouped": Map {
|
||||
Object {
|
||||
"path": "/packages/pkg1",
|
||||
"type": "pkg",
|
||||
} => Object {
|
||||
"integration": Array [
|
||||
"/packages/pkg1/integration_tests/test.js",
|
||||
],
|
||||
"unit": Array [
|
||||
"/packages/pkg1/test.js",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"path": "/packages/pkg2",
|
||||
"type": "pkg",
|
||||
} => Object {
|
||||
"integration": Array [
|
||||
"/packages/pkg2/integration_tests/test.js",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"path": "/packages/group/pkg3",
|
||||
"type": "pkg",
|
||||
} => Object {
|
||||
"unit": Array [
|
||||
"/packages/group/pkg3/test.js",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"path": "/packages/group/subgroup/pkg4",
|
||||
"type": "pkg",
|
||||
} => Object {
|
||||
"integration": Array [
|
||||
"/packages/group/subgroup/pkg4/integration_tests/test.js",
|
||||
],
|
||||
"unit": Array [
|
||||
"/packages/group/subgroup/pkg4/test.js",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"path": "/src/a",
|
||||
"type": "src",
|
||||
} => Object {
|
||||
"integration": Array [
|
||||
"/src/a/integration_tests/test.js",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"path": "/src/b",
|
||||
"type": "src",
|
||||
} => Object {
|
||||
"unit": Array [
|
||||
"/src/b/test.js",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"path": "/src/group/c",
|
||||
"type": "src",
|
||||
} => Object {
|
||||
"integration": Array [
|
||||
"/src/group/c/integration_tests/test.js",
|
||||
],
|
||||
"unit": Array [
|
||||
"/src/group/c/test.js",
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"path": "/src/group/subgroup/d",
|
||||
"type": "src",
|
||||
} => Object {
|
||||
"integration": Array [
|
||||
"/src/group/subgroup/d/integration_tests/test.js",
|
||||
],
|
||||
"unit": Array [
|
||||
"/src/group/subgroup/d/test.js",
|
||||
],
|
||||
},
|
||||
},
|
||||
"invalid": Array [
|
||||
"/tests/b/test.js",
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
95
packages/kbn-test/src/jest/configs/group_test_files.ts
Normal file
95
packages/kbn-test/src/jest/configs/group_test_files.ts
Normal file
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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 Path from 'path';
|
||||
import isPathInside from 'is-path-inside';
|
||||
|
||||
export interface Owner {
|
||||
type: 'pkg' | 'src';
|
||||
path: string;
|
||||
}
|
||||
export interface TestGroups {
|
||||
unit?: string[];
|
||||
integration?: string[];
|
||||
}
|
||||
export type GroupedTestFiles = Map<Owner, TestGroups>;
|
||||
|
||||
/**
|
||||
* Consumes the list of test files discovered along with the srcRoots and packageDirs to assign
|
||||
* each test file to a specific "owner", either a package or src directory, were we will eventually
|
||||
* expect to find relevant config files
|
||||
*/
|
||||
export function groupTestFiles(
|
||||
testFiles: string[],
|
||||
srcRoots: string[],
|
||||
packageDirs: string[]
|
||||
): { grouped: GroupedTestFiles; invalid: string[] } {
|
||||
const invalid: string[] = [];
|
||||
const testsByOwner = new Map<string, TestGroups>();
|
||||
|
||||
for (const testFile of testFiles) {
|
||||
const type = testFile.includes('integration_tests') ? 'integration' : 'unit';
|
||||
let ownerKey;
|
||||
// try to match the test file to a package first
|
||||
for (const pkgDir of packageDirs) {
|
||||
if (isPathInside(testFile, pkgDir)) {
|
||||
ownerKey = `pkg:${pkgDir}`;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// try to match the test file to a src root
|
||||
if (!ownerKey) {
|
||||
for (const srcRoot of srcRoots) {
|
||||
if (isPathInside(testFile, srcRoot)) {
|
||||
const segments = Path.relative(srcRoot, testFile).split(Path.sep);
|
||||
if (segments.length > 1) {
|
||||
ownerKey = `src:${Path.join(srcRoot, segments[0])}`;
|
||||
break;
|
||||
}
|
||||
|
||||
// if there are <= 1 relative segments then this file is directly in the "root"
|
||||
// which isn't supported, roots are directories which have test dirs in them.
|
||||
// We should ignore this match and match a higher-level root if possible
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!ownerKey) {
|
||||
invalid.push(testFile);
|
||||
continue;
|
||||
}
|
||||
|
||||
const tests = testsByOwner.get(ownerKey);
|
||||
if (!tests) {
|
||||
testsByOwner.set(ownerKey, { [type]: [testFile] });
|
||||
} else {
|
||||
const byType = tests[type];
|
||||
if (!byType) {
|
||||
tests[type] = [testFile];
|
||||
} else {
|
||||
byType.push(testFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
invalid,
|
||||
grouped: new Map<Owner, TestGroups>(
|
||||
[...testsByOwner.entries()].map(([key, tests]) => {
|
||||
const [type, ...path] = key.split(':');
|
||||
const owner: Owner = {
|
||||
type: type as Owner['type'],
|
||||
path: path.join(':'),
|
||||
};
|
||||
return [owner, tests];
|
||||
})
|
||||
),
|
||||
};
|
||||
}
|
|
@ -6,4 +6,10 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
export * from './jest_configs';
|
||||
export { getAllTestFiles } from './get_all_test_files';
|
||||
export { groupTestFiles } from './group_test_files';
|
||||
export {
|
||||
findMissingConfigFiles,
|
||||
UNIT_CONFIG_NAME,
|
||||
INTEGRATION_CONFIG_NAME,
|
||||
} from './find_missing_config_files';
|
||||
|
|
|
@ -1,116 +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.
|
||||
*/
|
||||
|
||||
import mockFs from 'mock-fs';
|
||||
import fs from 'fs';
|
||||
|
||||
import { JestConfigs } from './jest_configs';
|
||||
|
||||
describe('jestConfigs', () => {
|
||||
let jestConfigs: JestConfigs;
|
||||
|
||||
beforeEach(async () => {
|
||||
mockFs({
|
||||
'/kbn-test/packages': {
|
||||
a: {
|
||||
'jest.config.js': '',
|
||||
'a_first.test.js': '',
|
||||
'a_second.test.js': '',
|
||||
},
|
||||
b: {
|
||||
'b.test.js': '',
|
||||
integration_tests: {
|
||||
'b_integration.test.js': '',
|
||||
},
|
||||
nested: {
|
||||
d: {
|
||||
'd.test.js': '',
|
||||
},
|
||||
},
|
||||
},
|
||||
c: {
|
||||
'jest.integration.config.js': '',
|
||||
integration_tests: {
|
||||
'c_integration.test.js': '',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
jestConfigs = new JestConfigs('/kbn-test', ['packages/b/nested', 'packages']);
|
||||
});
|
||||
|
||||
afterEach(mockFs.restore);
|
||||
|
||||
describe('#files', () => {
|
||||
it('lists unit test files', async () => {
|
||||
const files = await jestConfigs.files('unit');
|
||||
expect(files).toEqual([
|
||||
'packages/a/a_first.test.js',
|
||||
'packages/a/a_second.test.js',
|
||||
'packages/b/b.test.js',
|
||||
'packages/b/nested/d/d.test.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('lists integration test files', async () => {
|
||||
const files = await jestConfigs.files('integration');
|
||||
expect(files).toEqual([
|
||||
'packages/b/integration_tests/b_integration.test.js',
|
||||
'packages/c/integration_tests/c_integration.test.js',
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#expected', () => {
|
||||
it('expects unit config files', async () => {
|
||||
const files = await jestConfigs.expected('unit');
|
||||
expect(files).toEqual([
|
||||
'packages/a/jest.config.js',
|
||||
'packages/b/jest.config.js',
|
||||
'packages/b/nested/d/jest.config.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('expects integration config files', async () => {
|
||||
const files = await jestConfigs.expected('integration');
|
||||
expect(files).toEqual([
|
||||
'packages/b/jest.integration.config.js',
|
||||
'packages/c/jest.integration.config.js',
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws if test file outside root', async () => {
|
||||
fs.writeFileSync('/kbn-test/bad.test.js', '');
|
||||
await expect(() => jestConfigs.expected('unit')).rejects.toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#existing', () => {
|
||||
it('lists existing unit test config files', async () => {
|
||||
const files = await jestConfigs.existing('unit');
|
||||
expect(files).toEqual(['packages/a/jest.config.js']);
|
||||
});
|
||||
|
||||
it('lists existing integration test config files', async () => {
|
||||
const files = await jestConfigs.existing('integration');
|
||||
expect(files).toEqual(['packages/c/jest.integration.config.js']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#missing', () => {
|
||||
it('lists existing unit test config files', async () => {
|
||||
const files = await jestConfigs.missing('unit');
|
||||
expect(files).toEqual(['packages/b/jest.config.js', 'packages/b/nested/d/jest.config.js']);
|
||||
});
|
||||
|
||||
it('lists existing integration test config files', async () => {
|
||||
const files = await jestConfigs.missing('integration');
|
||||
expect(files).toEqual(['packages/b/jest.integration.config.js']);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,86 +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.
|
||||
*/
|
||||
|
||||
import path from 'path';
|
||||
import globby from 'globby';
|
||||
|
||||
// @ts-ignore
|
||||
import { testMatch } from '../../../jest-preset';
|
||||
|
||||
export const CONFIG_NAMES = {
|
||||
unit: 'jest.config.js',
|
||||
integration: 'jest.integration.config.js',
|
||||
};
|
||||
|
||||
export class JestConfigs {
|
||||
cwd: string;
|
||||
roots: string[];
|
||||
allFiles: string[] | undefined;
|
||||
|
||||
constructor(cwd: string, roots: string[]) {
|
||||
this.cwd = cwd;
|
||||
// sort roots by length so when we use `file.startsWith()` we will find the most specific root first
|
||||
this.roots = roots.slice().sort((a, b) => b.length - a.length);
|
||||
}
|
||||
|
||||
async files(type: 'unit' | 'integration') {
|
||||
if (!this.allFiles) {
|
||||
this.allFiles = await globby(testMatch, {
|
||||
gitignore: true,
|
||||
cwd: this.cwd,
|
||||
});
|
||||
}
|
||||
|
||||
return this.allFiles.filter((f) =>
|
||||
type === 'integration' ? f.includes('integration_tests') : !f.includes('integration_tests')
|
||||
);
|
||||
}
|
||||
|
||||
async expected(type: 'unit' | 'integration') {
|
||||
const filesForType = await this.files(type);
|
||||
const directories: Set<string> = new Set();
|
||||
|
||||
filesForType.forEach((file) => {
|
||||
const root = this.roots.find((r) => file.startsWith(r));
|
||||
|
||||
if (root) {
|
||||
const splitPath = file.substring(root.length).split(path.sep);
|
||||
|
||||
if (splitPath.length > 2) {
|
||||
const name = splitPath[1];
|
||||
directories.add([root, name].join(path.sep));
|
||||
}
|
||||
} else {
|
||||
throw new Error(
|
||||
`Test file (${file}) can not exist outside roots (${this.roots.join(
|
||||
', '
|
||||
)}). Move it to a root or configure additional root.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return [...directories].map((d) => [d, CONFIG_NAMES[type]].join(path.sep));
|
||||
}
|
||||
|
||||
async existing(type: 'unit' | 'integration') {
|
||||
return await globby(`**/${CONFIG_NAMES[type]}`, {
|
||||
gitignore: true,
|
||||
cwd: this.cwd,
|
||||
});
|
||||
}
|
||||
|
||||
async missing(type: 'unit' | 'integration') {
|
||||
const expectedConfigs = await this.expected(type);
|
||||
const existingConfigs = await this.existing(type);
|
||||
return await expectedConfigs.filter((x) => !existingConfigs.includes(x));
|
||||
}
|
||||
|
||||
async allMissing() {
|
||||
return (await this.missing('unit')).concat(await this.missing('integration'));
|
||||
}
|
||||
}
|
|
@ -6,16 +6,21 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'fs';
|
||||
import path from 'path';
|
||||
import Fsp from 'fs/promises';
|
||||
import Path from 'path';
|
||||
import Mustache from 'mustache';
|
||||
|
||||
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 { discoverBazelPackageLocations } from '@kbn/bazel-packages';
|
||||
|
||||
import { JestConfigs, CONFIG_NAMES } from './configs';
|
||||
import {
|
||||
getAllTestFiles,
|
||||
groupTestFiles,
|
||||
findMissingConfigFiles,
|
||||
UNIT_CONFIG_NAME,
|
||||
} from './configs';
|
||||
|
||||
const unitTestingTemplate: string = `module.exports = {
|
||||
preset: '@kbn/test/jest_node',
|
||||
|
@ -40,15 +45,31 @@ const roots: string[] = [
|
|||
'test',
|
||||
'src/core',
|
||||
'src',
|
||||
...getAllRepoRelativeBazelPackageDirs(),
|
||||
];
|
||||
].map((rel) => Path.resolve(REPO_ROOT, rel));
|
||||
|
||||
export async function runCheckJestConfigsCli() {
|
||||
run(
|
||||
async ({ flags: { fix = false }, log }) => {
|
||||
const jestConfigs = new JestConfigs(REPO_ROOT, roots);
|
||||
const packageDirs = [
|
||||
...discoverBazelPackageLocations(REPO_ROOT),
|
||||
// kbn-pm is a weird package currently and needs to be added explicitly
|
||||
Path.resolve(REPO_ROOT, 'packages/kbn-pm'),
|
||||
];
|
||||
|
||||
const missing = await jestConfigs.allMissing();
|
||||
const testFiles = await getAllTestFiles();
|
||||
const { grouped, invalid } = groupTestFiles(testFiles, roots, packageDirs);
|
||||
|
||||
if (invalid.length) {
|
||||
const paths = invalid.map((path) => Path.relative(REPO_ROOT, path)).join('\n - ');
|
||||
log.error(
|
||||
`The following test files exist outside packages or pre-defined roots:\n - ${paths}`
|
||||
);
|
||||
throw createFailError(
|
||||
`Move the above files a pre-defined test root, a package, or configure an additional root to handle this file.`
|
||||
);
|
||||
}
|
||||
|
||||
const missing = await findMissingConfigFiles(grouped);
|
||||
|
||||
if (missing.length) {
|
||||
log.error(
|
||||
|
@ -60,20 +81,21 @@ export async function runCheckJestConfigsCli() {
|
|||
);
|
||||
|
||||
if (fix) {
|
||||
missing.forEach((file) => {
|
||||
const template = file.endsWith(CONFIG_NAMES.unit)
|
||||
? unitTestingTemplate
|
||||
: integrationTestingTemplate;
|
||||
for (const file of missing) {
|
||||
const template =
|
||||
Path.basename(file) === UNIT_CONFIG_NAME
|
||||
? unitTestingTemplate
|
||||
: integrationTestingTemplate;
|
||||
|
||||
const modulePath = path.dirname(file);
|
||||
const modulePath = Path.dirname(file);
|
||||
const content = Mustache.render(template, {
|
||||
relToRoot: path.relative(modulePath, '.'),
|
||||
relToRoot: Path.relative(modulePath, REPO_ROOT),
|
||||
modulePath,
|
||||
});
|
||||
|
||||
writeFileSync(file, content);
|
||||
await Fsp.writeFile(file, content);
|
||||
log.info('created %s', file);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw createFailError(
|
||||
`Run 'node scripts/check_jest_configs --fix' to create the missing config files`
|
||||
|
@ -86,8 +108,8 @@ export async function runCheckJestConfigsCli() {
|
|||
flags: {
|
||||
boolean: ['fix'],
|
||||
help: `
|
||||
--fix Attempt to create missing config files
|
||||
`,
|
||||
--fix Attempt to create missing config files
|
||||
`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue