From 51737f56f886d1910ed3245a046d8da49c2b187e Mon Sep 17 00:00:00 2001 From: sangwook Date: Sat, 27 Jun 2026 21:55:29 +0900 Subject: [PATCH 1/4] test_runner: exclude files with no matching tests from report Under process isolation, a test file whose tests are all filtered out by --test-name-pattern or --test-skip-pattern was still reported as a spurious passing entry and counted in the test totals. Drop such files from the parent's report, so they are neither shown nor counted. This matches the existing --test-isolation=none behavior. Refs: https://github.com/nodejs/node/issues/64099 Signed-off-by: sangwook --- lib/internal/test_runner/runner.js | 12 +- .../filtered-empty-files/has-match.test.js | 5 + .../filtered-empty-files/no-match.test.js | 5 + .../filtered-empty-files/throws.test.js | 9 ++ .../test-runner-filtered-empty-files.mjs | 109 ++++++++++++++++++ 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/test-runner/filtered-empty-files/has-match.test.js create mode 100644 test/fixtures/test-runner/filtered-empty-files/no-match.test.js create mode 100644 test/fixtures/test-runner/filtered-empty-files/throws.test.js create mode 100644 test/parallel/test-runner-filtered-empty-files.mjs diff --git a/lib/internal/test_runner/runner.js b/lib/internal/test_runner/runner.js index f9442be8ed164b..1535cce3dc59f0 100644 --- a/lib/internal/test_runner/runner.js +++ b/lib/internal/test_runner/runner.js @@ -285,7 +285,14 @@ class FileTest extends Test { } #skipReporting() { - return this.#reportedChildren > 0 && (!this.error || this.error.failureType === kSubtestsFailed); + const noError = !this.error || this.error.failureType === kSubtestsFailed; + if (!noError) { + return false; + } + if (this.#reportedChildren > 0) { + return true; + } + return this.root.harness.isFilteringByName; } #checkNestedComment(comment) { const firstSpaceIndex = StringPrototypeIndexOf(comment, ' '); @@ -933,6 +940,9 @@ function run(options = kEmptyObject) { }; const root = createTestTree(rootTestOptions, globalOptions); + root.harness.isFilteringByName ||= testNamePatterns != null || testSkipPatterns != null || + getOptionValue('--test-name-pattern').length > 0 || + getOptionValue('--test-skip-pattern').length > 0; let testFiles = files ?? createTestFileList(globPatterns, cwd); const { isTestRunner } = globalOptions; diff --git a/test/fixtures/test-runner/filtered-empty-files/has-match.test.js b/test/fixtures/test-runner/filtered-empty-files/has-match.test.js new file mode 100644 index 00000000000000..e4090551802111 --- /dev/null +++ b/test/fixtures/test-runner/filtered-empty-files/has-match.test.js @@ -0,0 +1,5 @@ +'use strict'; +const { test } = require('node:test'); + +test('alpha matches', () => {}); +test('beta does not', () => {}); diff --git a/test/fixtures/test-runner/filtered-empty-files/no-match.test.js b/test/fixtures/test-runner/filtered-empty-files/no-match.test.js new file mode 100644 index 00000000000000..8a83dda573d71a --- /dev/null +++ b/test/fixtures/test-runner/filtered-empty-files/no-match.test.js @@ -0,0 +1,5 @@ +'use strict'; +const { test } = require('node:test'); + +test('gamma', () => {}); +test('delta', () => {}); diff --git a/test/fixtures/test-runner/filtered-empty-files/throws.test.js b/test/fixtures/test-runner/filtered-empty-files/throws.test.js new file mode 100644 index 00000000000000..386b7e1e78989a --- /dev/null +++ b/test/fixtures/test-runner/filtered-empty-files/throws.test.js @@ -0,0 +1,9 @@ +'use strict'; +const { test } = require('node:test'); + +test('zeta', () => {}); + +// Even though every test in this file is filtered out by --test-name-pattern, +// a real load-time failure must still be reported (and must not be suppressed +// by the empty-file handling for issue #64099). +throw new Error('intentional load failure'); diff --git a/test/parallel/test-runner-filtered-empty-files.mjs b/test/parallel/test-runner-filtered-empty-files.mjs new file mode 100644 index 00000000000000..87017542215fa3 --- /dev/null +++ b/test/parallel/test-runner-filtered-empty-files.mjs @@ -0,0 +1,109 @@ +// Regression test for https://github.com/nodejs/node/issues/64099 +// +// Under the default (process) isolation, a test file whose tests are all +// filtered out by --test-name-pattern / --test-skip-pattern used to surface as +// a spurious passing entry and was counted in the "tests"/"pass" totals. It +// should instead be neither reported nor counted, matching what +// --test-isolation=none already does (see test-runner-no-isolation-filtering). +import '../common/index.mjs'; +import * as fixtures from '../common/fixtures.mjs'; +import assert from 'node:assert'; +import { spawnSync } from 'node:child_process'; +import { describe, it, run } from 'node:test'; +import { tap } from 'node:test/reporters'; +import consumers from 'node:stream/consumers'; + +const hasMatch = fixtures.path('test-runner', 'filtered-empty-files', 'has-match.test.js'); +const noMatch = fixtures.path('test-runner', 'filtered-empty-files', 'no-match.test.js'); +const throws = fixtures.path('test-runner', 'filtered-empty-files', 'throws.test.js'); + +describe('files with no tests matching the filter are not counted or reported', () => { + it('--test-name-pattern: counts only files with a match', () => { + const child = spawnSync(process.execPath, [ + '--test', + '--test-reporter=tap', + '--test-name-pattern=alpha', + hasMatch, + noMatch, + ]); + const stdout = child.stdout.toString(); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + assert.match(stdout, /# tests 1/); + assert.match(stdout, /# pass 1/); + assert.match(stdout, /# fail 0/); + assert.match(stdout, /ok \d+ - alpha matches/); + // The file without a matching test must not appear at all. + assert.doesNotMatch(stdout, /no-match\.test\.js/); + }); + + it('--test-name-pattern: a file with zero matches yields zero tests', () => { + const child = spawnSync(process.execPath, [ + '--test', + '--test-reporter=tap', + '--test-name-pattern=alpha', + noMatch, + ]); + const stdout = child.stdout.toString(); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + assert.match(stdout, /# tests 0/); + assert.match(stdout, /# suites 0/); + assert.doesNotMatch(stdout, /no-match\.test\.js/); + }); + + it('--test-skip-pattern: a fully skipped file is not counted or reported', () => { + const child = spawnSync(process.execPath, [ + '--test', + '--test-reporter=tap', + '--test-skip-pattern=/gamma|delta/', + hasMatch, + noMatch, + ]); + const stdout = child.stdout.toString(); + + assert.strictEqual(child.status, 0); + assert.strictEqual(child.signal, null); + // has-match keeps both of its tests; no-match is emptied by the skip pattern. + assert.match(stdout, /# tests 2/); + assert.match(stdout, /# pass 2/); + assert.doesNotMatch(stdout, /no-match\.test\.js/); + }); + + it('still reports a file that fails to load even when all its tests are filtered', () => { + const child = spawnSync(process.execPath, [ + '--test', + '--test-reporter=tap', + '--test-name-pattern=alpha', + hasMatch, + throws, + ]); + const stdout = child.stdout.toString(); + const combined = stdout + child.stderr.toString(); + + // A genuine load failure must surface, not be swallowed by the empty-file + // suppression: non-zero exit, the file is reported as failing, and the + // error is visible. + assert.notStrictEqual(child.status, 0); + assert.match(stdout, /# pass 1/); + assert.match(stdout, /# fail 1/); + assert.match(stdout, /not ok \d+ - .*throws\.test\.js/); + assert.match(combined, /intentional load failure/); + // The matching test in the other file is still reported. + assert.match(stdout, /ok \d+ - alpha matches/); + }); + + it('run() API: a file with no matching tests is not counted', async () => { + const stream = run({ + files: [hasMatch, noMatch], + testNamePatterns: [/alpha/], + }).compose(tap); + const output = await consumers.text(stream); + + assert.match(output, /# tests 1/); + assert.match(output, /ok \d+ - alpha matches/); + assert.doesNotMatch(output, /no-match\.test\.js/); + }); +}); From 4600e7d5fddedcef786a169d1960a0342e86867b Mon Sep 17 00:00:00 2001 From: sangwook Date: Sat, 27 Jun 2026 22:56:34 +0900 Subject: [PATCH 2/4] test_runner: re-run ci after infra timeout From 8b08f75b044a4a1f4d12c3b358804ab3902be6a5 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sat, 27 Jun 2026 23:57:31 +0900 Subject: [PATCH 3/4] build: trigger ci rerun Signed-off-by: sangwook From 6b4a94c064049cc7ac440c22de66544516bac675 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sun, 28 Jun 2026 01:13:15 +0900 Subject: [PATCH 4/4] build: trigger ci rerun Signed-off-by: sangwook