From c9e72e34abec1eb29fcaae3bf56fb4db76b532ec Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Sun, 24 Sep 2023 13:46:13 +0300 Subject: [PATCH] test_runner: accept `testOnly` in `run` PR-URL: https://github.com/nodejs/node/pull/49753 Fixes: https://github.com/nodejs/node/issues/49733 Reviewed-By: Chemi Atlow Reviewed-By: Benjamin Gruenbaum Reviewed-By: Raz Luvaton --- doc/api/test.md | 2 ++ lib/internal/test_runner/runner.js | 41 +++++++++++++++----------- test/fixtures/test-runner/test_only.js | 5 ++++ test/parallel/test-runner-run.mjs | 12 ++++++++ 4 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 test/fixtures/test-runner/test_only.js diff --git a/doc/api/test.md b/doc/api/test.md index d84d4c9f81a501..a5bb79c5e63d71 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -887,6 +887,8 @@ changes: number. If a nullish value is provided, each process gets its own port, incremented from the primary's `process.debugPort`. **Default:** `undefined`. + * `only`: {boolean} If truthy, the test context will only run tests that + have the `only` option set * `setup` {Function} A function that accepts the `TestsStream` instance and can be used to setup listeners before any tests are run. **Default:** `undefined`. diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index 51d01114696030..08f9b48dda10d3 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -110,14 +110,17 @@ function filterExecArgv(arg, i, arr) { !ArrayPrototypeSome(kFilterArgValues, (p) => arg === p || (i > 0 && arr[i - 1] === p) || StringPrototypeStartsWith(arg, `${p}=`)); } -function getRunArgs({ path, inspectPort, testNamePatterns }) { +function getRunArgs(path, { inspectPort, testNamePatterns, only }) { const argv = ArrayPrototypeFilter(process.execArgv, filterExecArgv); if (isUsingInspector()) { ArrayPrototypePush(argv, `--inspect-port=${getInspectPort(inspectPort)}`); } - if (testNamePatterns) { + if (testNamePatterns != null) { ArrayPrototypeForEach(testNamePatterns, (pattern) => ArrayPrototypePush(argv, `--test-name-pattern=${pattern}`)); } + if (only === true) { + ArrayPrototypePush(argv, '--test-only'); + } ArrayPrototypePush(argv, path); return argv; @@ -301,17 +304,17 @@ class FileTest extends Test { } } -function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { +function runTestFile(path, filesWatcher, opts) { const watchMode = filesWatcher != null; - const subtest = root.createSubtest(FileTest, path, async (t) => { - const args = getRunArgs({ __proto__: null, path, inspectPort, testNamePatterns }); + const subtest = opts.root.createSubtest(FileTest, path, async (t) => { + const args = getRunArgs(path, opts); const stdio = ['pipe', 'pipe', 'pipe']; const env = { __proto__: null, ...process.env, NODE_TEST_CONTEXT: 'child-v8' }; if (watchMode) { stdio.push('ipc'); env.WATCH_REPORT_DEPENDENCIES = '1'; } - if (root.harness.shouldColorizeTestFiles) { + if (opts.root.harness.shouldColorizeTestFiles) { env.FORCE_COLOR = '1'; } @@ -358,7 +361,7 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { filesWatcher.runningProcesses.delete(path); filesWatcher.runningSubtests.delete(path); if (filesWatcher.runningSubtests.size === 0) { - root.reporter[kEmitMessage]('test:watch:drained'); + opts.root.reporter[kEmitMessage]('test:watch:drained'); } } @@ -381,10 +384,10 @@ function runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns) { return subtest.start(); } -function watchFiles(testFiles, root, inspectPort, signal, testNamePatterns) { +function watchFiles(testFiles, opts) { const runningProcesses = new SafeMap(); const runningSubtests = new SafeMap(); - const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: 'filter', signal }); + const watcher = new FilesWatcher({ __proto__: null, debounce: 200, mode: 'filter', signal: opts.signal }); const filesWatcher = { __proto__: null, watcher, runningProcesses, runningSubtests }; watcher.on('changed', ({ owners }) => { @@ -400,19 +403,19 @@ function watchFiles(testFiles, root, inspectPort, signal, testNamePatterns) { } if (!runningSubtests.size) { // Reset the topLevel counter - root.harness.counters.topLevel = 0; + opts.root.harness.counters.topLevel = 0; } await runningSubtests.get(file); - runningSubtests.set(file, runTestFile(file, root, inspectPort, filesWatcher, testNamePatterns)); + runningSubtests.set(file, runTestFile(file, filesWatcher, opts)); }, undefined, (error) => { triggerUncaughtException(error, true /* fromPromise */); })); }); - if (signal) { + if (opts.signal) { kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation; - signal.addEventListener( + opts.signal.addEventListener( 'abort', - () => root.postRun(), + () => opts.root.postRun(), { __proto__: null, once: true, [kResistStopPropagation]: true }, ); } @@ -425,7 +428,7 @@ function run(options) { options = kEmptyObject; } let { testNamePatterns, shard } = options; - const { concurrency, timeout, signal, files, inspectPort, watch, setup } = options; + const { concurrency, timeout, signal, files, inspectPort, watch, setup, only } = options; if (files != null) { validateArray(files, 'options.files'); @@ -433,6 +436,9 @@ function run(options) { if (watch != null) { validateBoolean(watch, 'options.watch'); } + if (only != null) { + validateBoolean(only, 'options.only'); + } if (shard != null) { validateObject(shard, 'options.shard'); // Avoid re-evaluating the shard object in case it's a getter @@ -478,14 +484,15 @@ function run(options) { let postRun = () => root.postRun(); let filesWatcher; + const opts = { __proto__: null, root, signal, inspectPort, testNamePatterns, only }; if (watch) { - filesWatcher = watchFiles(testFiles, root, inspectPort, signal, testNamePatterns); + filesWatcher = watchFiles(testFiles, opts); postRun = undefined; } const runFiles = () => { root.harness.bootstrapComplete = true; return SafePromiseAllSettledReturnVoid(testFiles, (path) => { - const subtest = runTestFile(path, root, inspectPort, filesWatcher, testNamePatterns); + const subtest = runTestFile(path, filesWatcher, opts); filesWatcher?.runningSubtests.set(path, subtest); return subtest; }); diff --git a/test/fixtures/test-runner/test_only.js b/test/fixtures/test-runner/test_only.js new file mode 100644 index 00000000000000..efc79b9dfadca6 --- /dev/null +++ b/test/fixtures/test-runner/test_only.js @@ -0,0 +1,5 @@ +'use strict'; +const test = require('node:test'); + +test('this should be skipped'); +test.only('this should be executed'); diff --git a/test/parallel/test-runner-run.mjs b/test/parallel/test-runner-run.mjs index be15c42d465fca..02fed7a3659162 100644 --- a/test/parallel/test-runner-run.mjs +++ b/test/parallel/test-runner-run.mjs @@ -148,6 +148,18 @@ describe('require(\'node:test\').run', { concurrency: true }, () => { assert.strictEqual(result[5], 'ok 2 - this should be executed\n'); }); + it('should pass only to children', async () => { + const result = await run({ + files: [join(testFixtures, 'test_only.js')], + only: true + }) + .compose(tap) + .toArray(); + + assert.strictEqual(result[2], 'ok 1 - this should be skipped # SKIP \'only\' option not set\n'); + assert.strictEqual(result[5], 'ok 2 - this should be executed\n'); + }); + it('should emit "test:watch:drained" event on watch mode', async () => { const controller = new AbortController(); await run({