diff --git a/docs/05-command-line.md b/docs/05-command-line.md index cd16b9a99..c320fa37c 100644 --- a/docs/05-command-line.md +++ b/docs/05-command-line.md @@ -29,6 +29,8 @@ Options: --fail-fast Stop after first test failure [boolean] --match, -m Only run tests with matching title (can be repeated) [string] + --node-arguments Additional Node.js arguments for launching worker + processes (specify as a single string) [string] --serial, -s Run tests serially [boolean] --tap, -t Generate TAP output [boolean] --timeout, -T Set global timeout (milliseconds or human-readable, @@ -178,3 +180,17 @@ $ npx ava --tap | npx tap-nyan Please note that the TAP reporter is unavailable when using [watch mode](./recipes/watch-mode.md). + +## Node arguments + +The `--node-arguments` argument may be used to specify additional arguments for launching worker processes. These are combined with the `nodeArguments` configuration and any arguments passed to the `node` binary when starting AVA. + +**Only pass trusted values.** + +Specify the arguments as a single string: + +```console +npx ava --node-arguments="--throw-deprecation --zero-fill-buffers" +``` + +**Only pass trusted values.** diff --git a/docs/06-configuration.md b/docs/06-configuration.md index 6442ecf71..9e3578396 100644 --- a/docs/06-configuration.md +++ b/docs/06-configuration.md @@ -31,6 +31,10 @@ To ignore files, prefix the pattern with an `!` (exclamation mark). "verbose": true, "require": [ "./my-helper-module.js" + ], + "nodeArguments": [ + "--trace-deprecation", + "--napi-modules" ] } } @@ -53,6 +57,7 @@ Arguments passed to the CLI will always take precedence over the CLI options con - `extensions`: extensions of test files. Setting this overrides the default `["cjs", "mjs", "js"]` value, so make sure to include those extensions in the list - `require`: extra modules to require before tests are run. Modules are required in the [worker processes](./01-writing-tests.md#process-isolation) - `timeout`: Timeouts in AVA behave differently than in other test frameworks. AVA resets a timer after each test, forcing tests to quit if no new test results were received within the specified timeout. This can be used to handle stalled tests. See our [timeout documentation](./07-test-timeouts.md) for more options. +- `nodeArguments`: Configure Node.js arguments used to launch worker processes. Note that providing files on the CLI overrides the `files` option. @@ -219,4 +224,8 @@ export default { }; ``` +## Node arguments + +The `nodeArguments` configuration may be used to specify additional arguments for launching worker processes. These are combined with `--node-arguments` passed on the CLI and any arguments passed to the `node` binary when starting AVA. + [CLI]: ./05-command-line.md diff --git a/lib/api.js b/lib/api.js index bf4c9965b..013a3c9cf 100644 --- a/lib/api.js +++ b/lib/api.js @@ -219,7 +219,7 @@ class Api extends Emittery { options.updateSnapshots = true; } - const worker = fork(file, options, process.execArgv); + const worker = fork(file, options, apiOptions.nodeArguments); runStatus.observeWorker(worker, file); pendingWorkers.add(worker); diff --git a/lib/cli.js b/lib/cli.js index d5306ad7e..48974eb88 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -35,6 +35,11 @@ const FLAGS = { description: 'Only run tests with matching title (can be repeated)', type: 'string' }, + 'node-arguments': { + coerce: coerceLastValue, + description: 'Additional Node.js arguments for launching worker processes (specify as a single string)', + type: 'string' + }, serial: { alias: 's', coerce: coerceLastValue, @@ -161,7 +166,7 @@ exports.run = async () => { // eslint-disable-line complexity combined.failFast = argv[flag]; } else if (flag === 'update-snapshots') { combined.updateSnapshots = argv[flag]; - } else { + } else if (flag !== 'node-arguments') { combined[flag] = argv[flag]; } } @@ -254,6 +259,7 @@ exports.run = async () => { // eslint-disable-line complexity const babelManager = require('./babel-manager'); const normalizeExtensions = require('./extensions'); const {normalizeGlobs, normalizePatterns} = require('./globs'); + const normalizeNodeArguments = require('./node-arguments'); const validateEnvironmentVariables = require('./environment-variables'); let pkg; @@ -303,6 +309,13 @@ exports.run = async () => { // eslint-disable-line complexity exit(error.message); } + let nodeArguments; + try { + nodeArguments = normalizeNodeArguments(conf.nodeArguments, argv['node-arguments']); + } catch (error) { + exit(error.message); + } + let parallelRuns = null; if (isCi && ciParallelVars) { const {index: currentIndex, total: totalRuns} = ciParallelVars; @@ -328,6 +341,7 @@ exports.run = async () => { // eslint-disable-line complexity moduleTypes, environmentVariables, match, + nodeArguments, parallelRuns, projectDir, ranFromCli: true, diff --git a/lib/fork.js b/lib/fork.js index 8ca45f4e6..3cc1b6339 100644 --- a/lib/fork.js +++ b/lib/fork.js @@ -14,7 +14,7 @@ const AVA_PATH = path.resolve(__dirname, '..'); const workerPath = require.resolve('./worker/subprocess'); -module.exports = (file, opts, execArgv) => { +module.exports = (file, opts, execArgv = process.execArgv) => { let finished = false; const emitter = new Emittery(); @@ -34,7 +34,7 @@ module.exports = (file, opts, execArgv) => { cwd: opts.projectDir, silent: true, env: {NODE_ENV: 'test', ...process.env, ...opts.environmentVariables, AVA_PATH}, - execArgv: execArgv || process.execArgv + execArgv }); subprocess.stdout.on('data', chunk => { diff --git a/lib/node-arguments.js b/lib/node-arguments.js new file mode 100644 index 000000000..0acc4d97e --- /dev/null +++ b/lib/node-arguments.js @@ -0,0 +1,17 @@ +'use strict'; +const arrgv = require('arrgv'); + +function normalizeNodeArguments(fromConf = [], fromArgv = '') { + let parsedArgv = []; + if (fromArgv !== '') { + try { + parsedArgv = arrgv(fromArgv); + } catch { + throw new Error('Could not parse `--node-arguments` value. Make sure all strings are closed and backslashes are used correctly.'); + } + } + + return [...process.execArgv, ...fromConf, ...parsedArgv]; +} + +module.exports = normalizeNodeArguments; diff --git a/package-lock.json b/package-lock.json index 801a2ec29..3aee03e6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -716,6 +716,11 @@ "es-abstract": "^1.17.0-next.1" } }, + "arrgv": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/arrgv/-/arrgv-1.0.2.tgz", + "integrity": "sha512-a4eg4yhp7mmruZDQFqVMlxNRFGi/i1r87pt8SDHy0/I8PqSXoUTlWZRdAZo0VXgvEARcujbtTk8kiZRi1uDGRw==" + }, "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", diff --git a/package.json b/package.json index 7c0eb6d92..4ab9fcc12 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "dependencies": { "@concordance/react": "^2.0.0", "ansi-styles": "^4.2.1", + "arrgv": "^1.0.2", "arrify": "^2.0.1", "chalk": "^3.0.0", "chokidar": "^3.3.1", diff --git a/test/fixture/node-arguments.js b/test/fixture/node-arguments.js new file mode 100644 index 000000000..781743abf --- /dev/null +++ b/test/fixture/node-arguments.js @@ -0,0 +1,7 @@ +const test = require('../..'); + +test('exec arguments includes --throw-deprecation and --zero-fill-buffers', t => { + t.plan(2); + t.truthy(process.execArgv.includes('--throw-deprecation')); + t.truthy(process.execArgv.includes('--zero-fill-buffers')); +}); diff --git a/test/fixture/node-arguments/package.json b/test/fixture/node-arguments/package.json new file mode 100644 index 000000000..eef55b831 --- /dev/null +++ b/test/fixture/node-arguments/package.json @@ -0,0 +1,8 @@ +{ + "ava": { + "nodeArguments": [ + "--require", + "./setup.js" + ] + } +} diff --git a/test/fixture/node-arguments/setup.js b/test/fixture/node-arguments/setup.js new file mode 100644 index 000000000..5d620e626 --- /dev/null +++ b/test/fixture/node-arguments/setup.js @@ -0,0 +1 @@ +global.SETUP_CALLED = true; diff --git a/test/fixture/node-arguments/test.js b/test/fixture/node-arguments/test.js new file mode 100644 index 000000000..3ec80ebb8 --- /dev/null +++ b/test/fixture/node-arguments/test.js @@ -0,0 +1,7 @@ +const test = require('../../..'); + +test('works', t => { + t.plan(2); + t.truthy(global.SETUP_CALLED, 'setup variable set'); + t.truthy(process.execArgv.some(argv => argv.startsWith('--require')), 'require passed'); +}); diff --git a/test/helper/cli.js b/test/helper/cli.js index 537556418..79b417f31 100644 --- a/test/helper/cli.js +++ b/test/helper/cli.js @@ -26,7 +26,7 @@ function execCli(args, opts, cb) { const processPromise = new Promise(resolve => { // Spawning a child with piped IO means that the CLI will never see a TTY. // Inserting a shim here allows us to fake a TTY. - child = childProcess.spawn(process.execPath, ['-r', ttySimulator, cliPath].concat(args), { + child = childProcess.spawn(process.execPath, ['--require', ttySimulator, cliPath].concat(args), { cwd: dirname, env: {CI: '1', ...env}, // Force CI to ensure the correct reporter is selected // env, diff --git a/test/integration/node-arguments.js b/test/integration/node-arguments.js new file mode 100644 index 000000000..b807808e8 --- /dev/null +++ b/test/integration/node-arguments.js @@ -0,0 +1,24 @@ +'use strict'; +const {test} = require('tap'); +const {execCli} = require('../helper/cli'); + +test('passes node arguments to workers', t => { + t.plan(1); + execCli(['--node-arguments="--throw-deprecation --zero-fill-buffers"', 'node-arguments.js'], + (err, stdout, stderr) => t.ifError(err, null, {stdout, stderr})); +}); + +test('reads node arguments from config', t => { + t.plan(1); + execCli(['test.js'], { + dirname: 'fixture/node-arguments' + }, (err, stdout, stderr) => t.ifError(err, null, {stdout, stderr})); +}); + +test('detects incomplete --node-arguments', t => { + t.plan(2); + execCli(['--node-arguments="--foo=\'bar"', 'node-arguments.js'], (err, stdout, stderr) => { + t.ok(err); + t.match(stderr, /Could not parse `--node-arguments` value. Make sure all strings are closed and backslashes are used correctly./); + }); +}); diff --git a/test/node-arguments.js b/test/node-arguments.js new file mode 100644 index 000000000..7a858da88 --- /dev/null +++ b/test/node-arguments.js @@ -0,0 +1,12 @@ +'use strict'; + +const {test} = require('tap'); +const normalizeNodeArguments = require('../lib/node-arguments'); + +test('combines arguments', async t => { + t.deepEqual( + await normalizeNodeArguments(['--require setup.js'], '--throw-deprecation --zero-fill-buffers'), + [...process.execArgv, '--require setup.js', '--throw-deprecation', '--zero-fill-buffers'] + ); + t.end(); +});