scripts/jest/jest-cli.js (299 lines of code) (raw):

'use strict'; const {spawn} = require('child_process'); const chalk = require('chalk'); const yargs = require('yargs'); const fs = require('fs'); const path = require('path'); const ossConfig = './scripts/jest/config.source.js'; const wwwConfig = './scripts/jest/config.source-www.js'; const devToolsConfig = './scripts/jest/config.build-devtools.js'; // TODO: These configs are separate but should be rolled into the configs above // so that the CLI can provide them as options for any of the configs. const persistentConfig = './scripts/jest/config.source-persistent.js'; const buildConfig = './scripts/jest/config.build.js'; const argv = yargs .parserConfiguration({ // Important: This option tells yargs to move all other options not // specified here into the `_` key. We use this to send all of the // Jest options that we don't use through to Jest (like --watch). 'unknown-options-as-args': true, }) .wrap(yargs.terminalWidth()) .options({ debug: { alias: 'd', describe: 'Run with node debugger attached.', requiresArg: false, type: 'boolean', default: false, }, project: { alias: 'p', describe: 'Run the given project.', requiresArg: true, type: 'string', default: 'default', choices: ['default', 'devtools'], }, releaseChannel: { alias: 'r', describe: 'Run with the given release channel.', requiresArg: true, type: 'string', default: 'www-modern', choices: ['experimental', 'stable', 'www-classic', 'www-modern'], }, env: { alias: 'e', describe: 'Run with the given node environment.', requiresArg: true, type: 'string', choices: ['development', 'production'], }, prod: { describe: 'Run with NODE_ENV=production.', requiresArg: false, type: 'boolean', default: false, }, dev: { describe: 'Run with NODE_ENV=development.', requiresArg: false, type: 'boolean', default: false, }, variant: { alias: 'v', describe: 'Run with www variant set to true.', requiresArg: false, type: 'boolean', }, build: { alias: 'b', describe: 'Run tests on builds.', requiresArg: false, type: 'boolean', default: false, }, persistent: { alias: 'n', describe: 'Run with persistence.', requiresArg: false, type: 'boolean', default: false, }, ci: { describe: 'Run tests in CI', requiresArg: false, type: 'boolean', default: false, }, deprecated: { describe: 'Print deprecation message for command.', requiresArg: true, type: 'string', }, compactConsole: { alias: 'c', describe: 'Compact console output (hide file locations).', requiresArg: false, type: 'boolean', default: false, }, }).argv; function logError(message) { console.error(chalk.red(`\n${message}`)); } function isWWWConfig() { return ( (argv.releaseChannel === 'www-classic' || argv.releaseChannel === 'www-modern') && argv.project !== 'devtools' ); } function isOSSConfig() { return ( argv.releaseChannel === 'stable' || argv.releaseChannel === 'experimental' ); } function validateOptions() { let success = true; if (argv.project === 'devtools') { if (argv.prod) { logError( 'DevTool tests do not support --prod. Remove this option to continue.' ); success = false; } if (argv.dev) { logError( 'DevTool tests do not support --dev. Remove this option to continue.' ); success = false; } if (argv.env) { logError( 'DevTool tests do not support --env. Remove this option to continue.' ); success = false; } if (argv.persistent) { logError( 'DevTool tests do not support --persistent. Remove this option to continue.' ); success = false; } if (argv.variant) { logError( 'DevTool tests do not support --variant. Remove this option to continue.' ); success = false; } if (!argv.build) { logError('DevTool tests require --build.'); success = false; } } else { if (argv.compactConsole) { logError('Only DevTool tests support compactConsole flag.'); success = false; } } if (isWWWConfig()) { if (argv.variant === undefined) { // Turn internal experiments on by default argv.variant = true; } } else { if (argv.variant) { logError( 'Variant is only supported for the www release channels. Update these options to continue.' ); success = false; } } if (argv.build && argv.persistent) { logError( 'Persistence is not supported for build targets. Update these options to continue.' ); success = false; } if (!isOSSConfig() && argv.persistent) { logError( 'Persistence only supported for oss release channels. Update these options to continue.' ); success = false; } if (argv.build && isWWWConfig()) { logError( 'Build targets are only not supported for www release channels. Update these options to continue.' ); success = false; } if (argv.env && argv.env !== 'production' && argv.prod) { logError( 'Build type does not match --prod. Update these options to continue.' ); success = false; } if (argv.env && argv.env !== 'development' && argv.dev) { logError( 'Build type does not match --dev. Update these options to continue.' ); success = false; } if (argv.prod && argv.dev) { logError( 'Cannot supply both --prod and --dev. Remove one of these options to continue.' ); success = false; } if (argv.build) { // TODO: We could build this if it hasn't been built yet. const buildDir = path.resolve('./build'); if (!fs.existsSync(buildDir)) { logError( 'Build directory does not exist, please run `yarn build-combined` or remove the --build option.' ); success = false; } else if (Date.now() - fs.statSync(buildDir).mtimeMs > 1000 * 60 * 15) { logError( 'Warning: Running a build test with a build directory older than 15 minutes.\nPlease remember to run `yarn build` when using --build.' ); } } if (!success) { console.log(''); // Extra newline. process.exit(1); } } function getCommandArgs() { // Add the correct Jest config. const args = ['./scripts/jest/jest.js', '--config']; if (argv.project === 'devtools') { args.push(devToolsConfig); } else if (argv.build) { args.push(buildConfig); } else if (argv.persistent) { args.push(persistentConfig); } else if (isWWWConfig()) { args.push(wwwConfig); } else if (isOSSConfig()) { args.push(ossConfig); } else { // We should not get here. logError('Unrecognized release channel'); process.exit(1); } // Set the debug options, if necessary. if (argv.debug) { args.unshift('--inspect-brk'); args.push('--runInBand'); // Prevent console logs from being hidden until test completes. args.push('--useStderr'); } // CI Environments have limited workers. if (argv.ci) { args.push('--maxWorkers=2'); } // Push the remaining args onto the command. // This will send args like `--watch` to Jest. args.push(...argv._); return args; } function getEnvars() { const envars = { NODE_ENV: argv.env || 'development', RELEASE_CHANNEL: argv.releaseChannel.match(/modern|experimental/) ? 'experimental' : 'stable', // Pass this flag through to the config environment // so the base config can conditionally load the console setup file. compactConsole: argv.compactConsole, }; if (argv.prod) { envars.NODE_ENV = 'production'; } if (argv.dev) { envars.NODE_ENV = 'development'; } if (argv.variant) { envars.VARIANT = true; } return envars; } function main() { if (argv.deprecated) { console.log(chalk.red(`\nPlease run: \`${argv.deprecated}\` instead.\n`)); return; } validateOptions(); const args = getCommandArgs(); const envars = getEnvars(); const env = Object.entries(envars).map(([k, v]) => `${k}=${v}`); // Print the full command we're actually running. const command = `$ ${env.join(' ')} node ${args.join(' ')}`; console.log(chalk.dim(command)); // Print the release channel and project we're running for quick confirmation. console.log( chalk.blue( `\nRunning tests for ${argv.project} (${argv.releaseChannel})...` ) ); // Print a message that the debugger is starting just // for some extra feedback when running the debugger. if (argv.debug) { console.log(chalk.green('\nStarting debugger...')); console.log(chalk.green('Open chrome://inspect and press "inspect"\n')); } // Run Jest. const jest = spawn('node', args, { stdio: 'inherit', env: {...envars, ...process.env}, }); // Ensure we close our process when we get a failure case. jest.on('close', code => { // Forward the exit code from the Jest process. if (code === 1) { process.exit(1); } }); } main();