[jest] parse CLI flags correctly (#146844)

Fixes https://github.com/elastic/kibana/issues/144051

Rather than just parsing process.argv with the default config of
getopts, which treats flags like `-u` into a "value collecting" flag,
this updates the way we call getopts so that all known jest CLI flags
are properly handled. This is accomplished by parsing the output of
`yarn jest --help` and then using that information to power `node
scripts/jest`.

To update the known CLI flags we just need to run `yarn jest --help |
node scripts/read_jest_help.mjs` (for some reason I don't understand,
Jest does not produce it's entire `--help` output when called from node,
only when called from a terminal).
This commit is contained in:
Spencer 2022-12-02 10:04:15 -07:00 committed by GitHub
parent 061517fe3e
commit c38315f91c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 288 additions and 1 deletions

View file

@ -9,6 +9,7 @@ PKG_REQUIRE_NAME = "@kbn/test"
SOURCE_FILES = glob(
[
"src/failed_tests_reporter/es_config",
"src/jest/jest_flags.json",
"**/*.html",
"**/*.js",
"**/*.ts",
@ -148,6 +149,10 @@ jsts_transpiler(
name = "target_node",
srcs = SRCS,
build_pkg_name = package_name(),
additional_args = [
"--copy-files",
"--quiet"
]
)
ts_config(

View file

@ -0,0 +1,121 @@
{
"string": [
"cacheDirectory",
"changedSince",
"collectCoverageFrom",
"config",
"coverageDirectory",
"coveragePathIgnorePatterns",
"coverageProvider",
"coverageReporters",
"coverageThreshold",
"env",
"filter",
"globals",
"globalSetup",
"globalTeardown",
"haste",
"ignoreProjects",
"maxConcurrency",
"maxWorkers",
"moduleDirectories",
"moduleFileExtensions",
"moduleNameMapper",
"modulePathIgnorePatterns",
"modulePaths",
"notifyMode",
"outputFile",
"preset",
"prettierPath",
"projects",
"reporters",
"resolver",
"rootDir",
"roots",
"runner",
"seed",
"selectProjects",
"setupFiles",
"setupFilesAfterEnv",
"shard",
"snapshotSerializers",
"testEnvironment",
"testEnvironmentOptions",
"testFailureExitCode",
"testMatch",
"testNamePattern",
"testPathIgnorePatterns",
"testPathPattern",
"testRegex",
"testResultsProcessor",
"testRunner",
"testSequencer",
"testTimeout",
"transform",
"transformIgnorePatterns",
"unmockedModulePathPatterns",
"watchPathIgnorePatterns"
],
"boolean": [
"all",
"automock",
"bail",
"cache",
"changedFilesWithAncestor",
"ci",
"clearCache",
"clearMocks",
"collectCoverage",
"color",
"colors",
"coverage",
"debug",
"detectLeaks",
"detectOpenHandles",
"errorOnDeprecated",
"expand",
"findRelatedTests",
"forceExit",
"help",
"init",
"injectGlobals",
"json",
"lastCommit",
"listTests",
"logHeapUsage",
"noStackTrace",
"notify",
"onlyChanged",
"onlyFailures",
"passWithNoTests",
"resetMocks",
"resetModules",
"restoreMocks",
"runInBand",
"runTestsByPath",
"showConfig",
"showSeed",
"silent",
"skipFilter",
"testLocationInResults",
"updateSnapshot",
"useStderr",
"verbose",
"version",
"watch",
"watchAll",
"watchman"
],
"alias": {
"b": "bail",
"c": "config",
"e": "expand",
"f": "onlyFailures",
"h": "help",
"i": "runInBand",
"o": "onlyChanged",
"t": "testNamePattern",
"u": "updateSnapshot",
"w": "maxWorkers"
}
}

View file

@ -22,16 +22,46 @@ import { existsSync } from 'fs';
import { run } from 'jest';
import { ToolingLog } from '@kbn/tooling-log';
import { getTimeReporter } from '@kbn/ci-stats-reporter';
import { createFailError } from '@kbn/dev-cli-errors';
import { REPO_ROOT } from '@kbn/utils';
import { map } from 'lodash';
import getopts from 'getopts';
import jestFlags from './jest_flags.json';
// yarn test:jest src/core/server/saved_objects
// yarn test:jest src/core/public/core_system.test.ts
// :kibana/src/core/server/saved_objects yarn test:jest
export function runJest(configName = 'jest.config.js') {
const argv = getopts(process.argv.slice(2));
const unknownFlag: string[] = [];
const argv = getopts(process.argv.slice(2), {
...jestFlags,
unknown(v) {
unknownFlag.push(v);
return false;
},
});
if (argv.help) {
run();
process.exit(0);
}
if (unknownFlag.length) {
const flags = unknownFlag.join(', ');
throw createFailError(
`unexpected flag: ${flags}
If this flag is valid you might need to update the flags in "packages/kbn-test/src/jest/run.js".
Run 'yarn jest --help | node scripts/read_jest_help.mjs' to update this scripts knowledge of what
flags jest supports
`
);
}
const devConfigName = 'jest.config.dev.js';
const log = new ToolingLog({

131
scripts/read_jest_help.mjs Normal file
View file

@ -0,0 +1,131 @@
/*
* 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 { createFailError } from '@kbn/dev-cli-errors';
import { run } from '@kbn/dev-cli-runner';
import { REPO_ROOT } from '@kbn/utils';
const FLAGS_FILE = 'packages/kbn-test/src/jest/jest_flags.json';
function readStdin() {
return new Promise((resolve, reject) => {
let buffer = '';
let timer = setTimeout(() => {
reject(
createFailError('you must pipe the output of `yarn jest --help` to this script', {
showHelp: true,
})
);
}, 1000);
process.stdin
.on('data', (chunk) => {
if (timer) {
clearTimeout(timer);
timer = undefined;
}
buffer += chunk;
})
.on('end', () => resolve(buffer))
.on('error', reject);
});
}
run(
async ({ log }) => {
const lines = (await readStdin()).split('\n');
/** @type {{ string: string[], boolean: string[], alias: Record<string, string> }} */
const flags = { string: [], boolean: [], alias: {} };
/** @type {string | undefined} */
let currentFlag;
for (const line of lines) {
const flagMatch = line.match(/^\s+(?:-(\w), )?--(\w+)\s+/);
const typeMatch = line.match(/\[(boolean|string|array|number|choices: [^\]]+)\]/);
if (flagMatch && currentFlag) {
throw createFailError(`unable to determine type for flag [${currentFlag}]`);
}
if (flagMatch) {
currentFlag = flagMatch[2];
if (flagMatch[1]) {
flags.alias[flagMatch[1]] = flagMatch[2];
}
}
if (currentFlag && typeMatch) {
switch (typeMatch[1]) {
case 'string':
case 'array':
case 'number':
flags.string.push(currentFlag);
break;
case 'boolean':
flags.boolean.push(currentFlag);
break;
default:
if (typeMatch[1].startsWith('choices: ')) {
flags.string.push(currentFlag);
break;
}
throw createFailError(`unexpected flag type [${typeMatch[1]}]`);
}
currentFlag = undefined;
}
}
await Fsp.writeFile(
Path.resolve(REPO_ROOT, FLAGS_FILE),
JSON.stringify(
{
string: flags.string.sort(function (a, b) {
return a.localeCompare(b);
}),
boolean: flags.boolean.sort(function (a, b) {
return a.localeCompare(b);
}),
alias: Object.fromEntries(
Object.entries(flags.alias).sort(function (a, b) {
return a[0].localeCompare(b[0]);
})
),
},
null,
2
)
);
log.success('wrote jest flag info to', FLAGS_FILE);
log.warning('make sure you bootstrap to rebuild @kbn/test');
},
{
usage: `yarn jest --help | node scripts/read_jest_help.mjs`,
description: `
Jest no longer exposes the ability to parse CLI flags externally, so we use this
script to read the help output and convert it into parameters we can pass to getopts()
which will parse the flags similar to how Jest does it.
getopts() doesn't support things like enums, or number flags, but if we use the generated
config then it will at least interpret which flags are expected, which are invalid, and
allow us to determine the correct config path based on the provided path while passing
the rest of the args directly to jest.
`,
flags: {
allowUnexpected: true,
guessTypesForUnexpectedFlags: false,
},
}
);