From 70ec5c009d87c93e4f9f8fb03b40897b4515f327 Mon Sep 17 00:00:00 2001 From: Daijiro Wachi Date: Sat, 17 Jan 2026 19:14:31 +0900 Subject: [PATCH 1/3] meta: fix typos in issue template config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR-URL: https://github.com/nodejs/node/pull/61399 Reviewed-By: Antoine du Hamel Reviewed-By: Ulises Gascón Reviewed-By: Richard Lau Reviewed-By: Gürgün Dayıoğlu Reviewed-By: Rafael Gonzaga Reviewed-By: Luigi Pinca Reviewed-By: Colin Ihrig Reviewed-By: Benjamin Gruenbaum --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 0ec17d486e7712..dc01fd3fdac591 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,10 +5,10 @@ contact_links: about: Please file an issue in our help repo. - name: 📦 Have an issue with npm? url: https://github.com/npm/cli/issues - about: npm has a seperate issue tracker. + about: npm has a separate issue tracker. - name: 📡 Have an issue with undici? (`WebSocket`, `fetch`, etc.) url: https://github.com/nodejs/undici/issues - about: Undici has a seperate issue tracker. + about: Undici has a separate issue tracker. - name: 🌐 Found a problem with nodejs.org beyond the API reference docs? url: https://github.com/nodejs/nodejs.org/issues/new/choose about: Please file an issue in the Node.js website repo. From 13a662f55986717313bc5d66e9476e138b681cc8 Mon Sep 17 00:00:00 2001 From: sangwook Date: Sat, 17 Jan 2026 22:33:55 +0900 Subject: [PATCH 2/3] fs: fix ENOTDIR in globSync when file is treated as dir MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `fs.globSync` failed with `ENOTDIR` when a path component in a glob pattern was a file but used as a directory (e.g., 'foo{,/bar}' when 'foo' is a file). This change aligns `getDirentSync` with the asynchronous `getDirent` by wrapping the `lstatSync` call in a `try-catch` block to safely return `null` on such errors. Fixes: https://github.com/nodejs/node/issues/61257 PR-URL: https://github.com/nodejs/node/pull/61259 Reviewed-By: René --- lib/internal/fs/glob.js | 6 ++++-- test/parallel/test-fs-glob.mjs | 20 +++++++++++++++++++- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/lib/internal/fs/glob.js b/lib/internal/fs/glob.js index 1bfa39150e5196..526efd4c010d7c 100644 --- a/lib/internal/fs/glob.js +++ b/lib/internal/fs/glob.js @@ -65,8 +65,10 @@ async function getDirent(path) { * @returns {DirentFromStats|null} */ function getDirentSync(path) { - const stat = lstatSync(path, { throwIfNoEntry: false }); - if (stat === undefined) { + let stat; + try { + stat = lstatSync(path); + } catch { return null; } return new DirentFromStats(basename(path), stat, dirname(path)); diff --git a/test/parallel/test-fs-glob.mjs b/test/parallel/test-fs-glob.mjs index 54523219cc2d97..74791deba373e3 100644 --- a/test/parallel/test-fs-glob.mjs +++ b/test/parallel/test-fs-glob.mjs @@ -2,7 +2,7 @@ import * as common from '../common/index.mjs'; import tmpdir from '../common/tmpdir.js'; import { resolve, dirname, sep, relative, join, isAbsolute } from 'node:path'; import { mkdir, writeFile, symlink, glob as asyncGlob } from 'node:fs/promises'; -import { glob, globSync, Dirent, chmodSync } from 'node:fs'; +import { glob, globSync, Dirent, chmodSync, writeFileSync, rmSync } from 'node:fs'; import { test, describe } from 'node:test'; import { pathToFileURL } from 'node:url'; import { promisify } from 'node:util'; @@ -543,3 +543,21 @@ describe('glob - with restricted directory', function() { } }); }); + +describe('globSync - ENOTDIR', function() { + test('should return empty array when a file is treated as a directory', () => { + const file = tmpdir.resolve('foo'); + writeFileSync(file, ''); + try { + const pattern = 'foo{,/bar}'; + const actual = globSync(pattern, { cwd: tmpdir.path }).sort(); + assert.deepStrictEqual(actual, ['foo']); + } finally { + try { + rmSync(file); + } catch { + // ignore + } + } + }); +}); From 8365edcbd0d98fdd79993a61302e35d299c3d557 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Sat, 17 Jan 2026 16:02:35 +0200 Subject: [PATCH 3/3] test_runner: fix rerun ambiguous test failures PR-URL: https://github.com/nodejs/node/pull/61392 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Chemi Atlow Reviewed-By: Jacob Smith Reviewed-By: Zeyu "Alex" Yang --- lib/internal/test_runner/reporter/rerun.js | 39 ++++++++++- lib/internal/test_runner/test.js | 14 +++- test/fixtures/test-runner/rerun.js | 17 ++++- .../test-runner-test-rerun-failures.js | 68 +++++++++++++------ 4 files changed, 113 insertions(+), 25 deletions(-) diff --git a/lib/internal/test_runner/reporter/rerun.js b/lib/internal/test_runner/reporter/rerun.js index 1862dafc04ea71..9658a7fb70ff0b 100644 --- a/lib/internal/test_runner/reporter/rerun.js +++ b/lib/internal/test_runner/reporter/rerun.js @@ -1,6 +1,7 @@ 'use strict'; const { + ArrayPrototypeMap, ArrayPrototypePush, JSONStringify, } = primordials; @@ -11,19 +12,55 @@ function reportReruns(previousRuns, globalOptions) { return async function reporter(source) { const obj = { __proto__: null }; const disambiguator = { __proto__: null }; + let currentSuite = null; + const roots = []; + + function getTestId(data) { + return `${relative(globalOptions.cwd, data.file)}:${data.line}:${data.column}`; + } + + function startTest(data) { + const originalSuite = currentSuite; + currentSuite = { __proto__: null, data, parent: currentSuite, children: [] }; + if (originalSuite?.children) { + ArrayPrototypePush(originalSuite.children, currentSuite); + } + if (!currentSuite.parent) { + ArrayPrototypePush(roots, currentSuite); + } + } for await (const { type, data } of source) { + let currentTest; + if (type === 'test:start') { + startTest(data); + } else if (type === 'test:fail' || type === 'test:pass') { + if (!currentSuite) { + startTest({ __proto__: null, name: 'root', nesting: 0 }); + } + if (currentSuite.data.name !== data.name || currentSuite.data.nesting !== data.nesting) { + startTest(data); + } + currentTest = currentSuite; + if (currentSuite?.data.nesting === data.nesting) { + currentSuite = currentSuite.parent; + } + } + + if (type === 'test:pass') { - let identifier = `${relative(globalOptions.cwd, data.file)}:${data.line}:${data.column}`; + let identifier = getTestId(data); if (disambiguator[identifier] !== undefined) { identifier += `:(${disambiguator[identifier]})`; disambiguator[identifier] += 1; } else { disambiguator[identifier] = 1; } + const children = ArrayPrototypeMap(currentTest.children, (child) => child.data); obj[identifier] = { __proto__: null, name: data.name, + children, passed_on_attempt: data.details.passed_on_attempt ?? data.details.attempt, }; } diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 3f28c6b4626715..e426438faba75f 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -699,10 +699,18 @@ class Test extends AsyncResource { this.root.testDisambiguator.set(testIdentifier, 1); } this.attempt = this.root.harness.previousRuns.length; - const previousAttempt = this.root.harness.previousRuns[this.attempt - 1]?.[testIdentifier]?.passed_on_attempt; + const previousAttempt = this.root.harness.previousRuns[this.attempt - 1]?.[testIdentifier]; if (previousAttempt != null) { - this.passedAttempt = previousAttempt; - this.fn = noop; + this.passedAttempt = previousAttempt.passed_on_attempt; + this.fn = () => { + for (let i = 0; i < (previousAttempt.children?.length ?? 0); i++) { + const child = previousAttempt.children[i]; + this.createSubtest(Test, child.name, { __proto__: null }, noop, { + __proto__: null, + loc: [child.line, child.column, child.file], + }, noop).start(); + } + }; } } } diff --git a/test/fixtures/test-runner/rerun.js b/test/fixtures/test-runner/rerun.js index 94f3708431990a..a76a1bd00dd12a 100644 --- a/test/fixtures/test-runner/rerun.js +++ b/test/fixtures/test-runner/rerun.js @@ -22,4 +22,19 @@ function ambiguousTest(expectedAttempts) { } ambiguousTest(0); -ambiguousTest(1); \ No newline at end of file +ambiguousTest(1); + +function nestedAmbiguousTest(expectedAttempts) { + return async (t) => { + await t.test('nested', async (tt) => { + await tt.test('2 levels deep', () => {}); + if (t.attempt < expectedAttempts) { + throw new Error(`This test is expected to fail on the first ${expectedAttempts} attempts`); + } + }); + await t.test('ok', () => {}); + }; +} + +test('nested ambiguous (expectedAttempts=0)', nestedAmbiguousTest(0)); +test('nested ambiguous (expectedAttempts=1)', nestedAmbiguousTest(2)); diff --git a/test/parallel/test-runner-test-rerun-failures.js b/test/parallel/test-runner-test-rerun-failures.js index 482af5172de4ec..220ec140b368a6 100644 --- a/test/parallel/test-runner-test-rerun-failures.js +++ b/test/parallel/test-runner-test-rerun-failures.js @@ -15,23 +15,51 @@ afterEach(() => rm(stateFile, { force: true })); const expectedStateFile = [ { - 'test/fixtures/test-runner/rerun.js:17:3': { passed_on_attempt: 0, name: 'ambiguous (expectedAttempts=0)' }, 'test/fixtures/test-runner/rerun.js:9:1': { passed_on_attempt: 0, name: 'ok' }, + 'test/fixtures/test-runner/rerun.js:17:3': { passed_on_attempt: 0, name: 'ambiguous (expectedAttempts=0)' }, + 'test/fixtures/test-runner/rerun.js:30:16': { passed_on_attempt: 0, name: '2 levels deep' }, + 'test/fixtures/test-runner/rerun.js:29:13': { passed_on_attempt: 0, name: 'nested' }, + 'test/fixtures/test-runner/rerun.js:35:13': { passed_on_attempt: 0, name: 'ok' }, + 'test/fixtures/test-runner/rerun.js:39:1': { passed_on_attempt: 0, name: 'nested ambiguous (expectedAttempts=0)' }, + 'test/fixtures/test-runner/rerun.js:30:16:(1)': { passed_on_attempt: 0, name: '2 levels deep' }, + 'test/fixtures/test-runner/rerun.js:35:13:(1)': { passed_on_attempt: 0, name: 'ok' }, }, { + 'test/fixtures/test-runner/rerun.js:9:1': { passed_on_attempt: 0, name: 'ok' }, 'test/fixtures/test-runner/rerun.js:17:3': { passed_on_attempt: 0, name: 'ambiguous (expectedAttempts=0)' }, 'test/fixtures/test-runner/rerun.js:17:3:(1)': { passed_on_attempt: 1, name: 'ambiguous (expectedAttempts=1)' }, - 'test/fixtures/test-runner/rerun.js:9:1': { passed_on_attempt: 0, name: 'ok' }, + 'test/fixtures/test-runner/rerun.js:30:16': { passed_on_attempt: 0, name: '2 levels deep' }, + 'test/fixtures/test-runner/rerun.js:29:13': { passed_on_attempt: 0, name: 'nested' }, + 'test/fixtures/test-runner/rerun.js:35:13': { passed_on_attempt: 0, name: 'ok' }, + 'test/fixtures/test-runner/rerun.js:39:1': { passed_on_attempt: 0, name: 'nested ambiguous (expectedAttempts=0)' }, + 'test/fixtures/test-runner/rerun.js:30:16:(1)': { passed_on_attempt: 0, name: '2 levels deep' }, + 'test/fixtures/test-runner/rerun.js:35:13:(1)': { passed_on_attempt: 0, name: 'ok' }, }, { + 'test/fixtures/test-runner/rerun.js:3:1': { passed_on_attempt: 2, name: 'should fail on first two attempts' }, + 'test/fixtures/test-runner/rerun.js:9:1': { passed_on_attempt: 0, name: 'ok' }, 'test/fixtures/test-runner/rerun.js:17:3': { passed_on_attempt: 0, name: 'ambiguous (expectedAttempts=0)' }, 'test/fixtures/test-runner/rerun.js:17:3:(1)': { passed_on_attempt: 1, name: 'ambiguous (expectedAttempts=1)' }, - 'test/fixtures/test-runner/rerun.js:9:1': { passed_on_attempt: 0, name: 'ok' }, - 'test/fixtures/test-runner/rerun.js:3:1': { passed_on_attempt: 2, name: 'should fail on first two attempts' }, + 'test/fixtures/test-runner/rerun.js:30:16': { passed_on_attempt: 0, name: '2 levels deep' }, + 'test/fixtures/test-runner/rerun.js:29:13': { passed_on_attempt: 0, name: 'nested' }, + 'test/fixtures/test-runner/rerun.js:35:13': { passed_on_attempt: 0, name: 'ok' }, + 'test/fixtures/test-runner/rerun.js:39:1': { passed_on_attempt: 0, name: 'nested ambiguous (expectedAttempts=0)' }, + 'test/fixtures/test-runner/rerun.js:29:13:(1)': { passed_on_attempt: 2, name: 'nested' }, + 'test/fixtures/test-runner/rerun.js:30:16:(1)': { passed_on_attempt: 0, name: '2 levels deep' }, + 'test/fixtures/test-runner/rerun.js:35:13:(1)': { passed_on_attempt: 0, name: 'ok' }, + 'test/fixtures/test-runner/rerun.js:40:1': { passed_on_attempt: 2, name: 'nested ambiguous (expectedAttempts=1)' }, }, ]; -const getStateFile = async () => JSON.parse((await readFile(stateFile, 'utf8')).replaceAll('\\\\', '/')); +const getStateFile = async () => { + const res = JSON.parse((await readFile(stateFile, 'utf8')).replaceAll('\\\\', '/')); + res.forEach((entry) => { + for (const item in entry) { + delete entry[item].children; + } + }); + return res; +}; test('test should pass on third rerun', async () => { const args = ['--test-rerun-failures', stateFile, fixture]; @@ -39,22 +67,22 @@ test('test should pass on third rerun', async () => { let { code, stdout, signal } = await common.spawnPromisified(process.execPath, args); assert.strictEqual(code, 1); assert.strictEqual(signal, null); - assert.match(stdout, /pass 2/); - assert.match(stdout, /fail 2/); + assert.match(stdout, /pass 8/); + assert.match(stdout, /fail 4/); assert.deepStrictEqual(await getStateFile(), expectedStateFile.slice(0, 1)); ({ code, stdout, signal } = await common.spawnPromisified(process.execPath, args)); assert.strictEqual(code, 1); assert.strictEqual(signal, null); - assert.match(stdout, /pass 3/); - assert.match(stdout, /fail 1/); + assert.match(stdout, /pass 9/); + assert.match(stdout, /fail 3/); assert.deepStrictEqual(await getStateFile(), expectedStateFile.slice(0, 2)); ({ code, stdout, signal } = await common.spawnPromisified(process.execPath, args)); assert.strictEqual(code, 0); assert.strictEqual(signal, null); - assert.match(stdout, /pass 4/); + assert.match(stdout, /pass 12/); assert.match(stdout, /fail 0/); assert.deepStrictEqual(await getStateFile(), expectedStateFile); }); @@ -65,30 +93,30 @@ test('test should pass on third rerun with `--test`', async () => { let { code, stdout, signal } = await common.spawnPromisified(process.execPath, args); assert.strictEqual(code, 1); assert.strictEqual(signal, null); - assert.match(stdout, /pass 2/); - assert.match(stdout, /fail 2/); + assert.match(stdout, /pass 8/); + assert.match(stdout, /fail 4/); assert.deepStrictEqual(await getStateFile(), expectedStateFile.slice(0, 1)); ({ code, stdout, signal } = await common.spawnPromisified(process.execPath, args)); assert.strictEqual(code, 1); assert.strictEqual(signal, null); - assert.match(stdout, /pass 3/); - assert.match(stdout, /fail 1/); + assert.match(stdout, /pass 9/); + assert.match(stdout, /fail 3/); assert.deepStrictEqual(await getStateFile(), expectedStateFile.slice(0, 2)); ({ code, stdout, signal } = await common.spawnPromisified(process.execPath, args)); assert.strictEqual(code, 0); assert.strictEqual(signal, null); - assert.match(stdout, /pass 4/); + assert.match(stdout, /pass 12/); assert.match(stdout, /fail 0/); assert.deepStrictEqual(await getStateFile(), expectedStateFile); }); test('using `run` api', async () => { let stream = run({ files: [fixture], rerunFailuresFilePath: stateFile }); - stream.on('test:pass', common.mustCall(2)); - stream.on('test:fail', common.mustCall(2)); + stream.on('test:pass', common.mustCall(8)); + stream.on('test:fail', common.mustCall(4)); // eslint-disable-next-line no-unused-vars for await (const _ of stream); @@ -97,8 +125,8 @@ test('using `run` api', async () => { stream = run({ files: [fixture], rerunFailuresFilePath: stateFile }); - stream.on('test:pass', common.mustCall(3)); - stream.on('test:fail', common.mustCall(1)); + stream.on('test:pass', common.mustCall(9)); + stream.on('test:fail', common.mustCall(3)); // eslint-disable-next-line no-unused-vars for await (const _ of stream); @@ -107,7 +135,7 @@ test('using `run` api', async () => { stream = run({ files: [fixture], rerunFailuresFilePath: stateFile }); - stream.on('test:pass', common.mustCall(4)); + stream.on('test:pass', common.mustCall(12)); stream.on('test:fail', common.mustNotCall()); // eslint-disable-next-line no-unused-vars