From c9d0306f32d283c8b570ab0ec03dcedc3db8f9c1 Mon Sep 17 00:00:00 2001 From: monam2 Date: Tue, 13 Jan 2026 02:58:33 +0900 Subject: [PATCH 1/2] test: make cwd path stripping url-safe --- test/common/assertSnapshot.js | 55 ++++++++++++++++++- test/es-module/test-esm-import-meta.mjs | 20 +++---- ...-assert-snapshot-transform-project-root.js | 45 +++++++++++++++ test/parallel/test-cli-permission-deny-fs.js | 7 ++- test/parallel/test-node-output-console.mjs | 9 ++- test/parallel/test-node-output-errors.mjs | 11 ++-- test/parallel/test-node-output-eval.mjs | 8 +-- test/parallel/test-node-output-v8-warning.mjs | 16 +++--- test/parallel/test-node-output-vm.mjs | 10 +++- 9 files changed, 143 insertions(+), 38 deletions(-) create mode 100644 test/parallel/test-assert-snapshot-transform-project-root.js diff --git a/test/common/assertSnapshot.js b/test/common/assertSnapshot.js index af4345f5111f24..2fbc6862310e7a 100644 --- a/test/common/assertSnapshot.js +++ b/test/common/assertSnapshot.js @@ -4,6 +4,7 @@ const path = require('node:path'); const test = require('node:test'); const fs = require('node:fs/promises'); const assert = require('node:assert/strict'); +const { pathToFileURL } = require('node:url'); const { hostname } = require('node:os'); const stackFramesRegexp = /(?<=\n)(\s+)((.+?)\s+\()?(?:\(?(.+?):(\d+)(?::(\d+))?)\)?(\s+\{)?(\[\d+m)?(\n|$)/g; @@ -33,8 +34,60 @@ function replaceWindowsPaths(str) { function transformProjectRoot(replacement = '') { const projectRoot = path.resolve(__dirname, '../..'); + const fsRoot = path.parse(projectRoot).root; + + // If projectRoot is fs root, skip stripping to avoid corrupting URL separators. + if (projectRoot === fsRoot) { + return (str) => str.replaceAll('\\\'', "'"); + } + + // Stack output may use '/' on Windows; handle both separators. + const projectRootPosix = projectRoot.replaceAll(path.win32.sep, path.posix.sep); + const fileUrlPrefix = 'file:///'; + const fileUrl = pathToFileURL(projectRoot).href; + const fileUrlRoot = fileUrl.startsWith(fileUrlPrefix) ? + fileUrl.slice(fileUrlPrefix.length) : + null; + + const stripPrefixAtBoundary = (str, prefix) => { + if (!prefix) return str; + let out = ''; + let index = 0; + while (true) { + const match = str.indexOf(prefix, index); + if (match === -1) return out + str.slice(index); + const after = match + prefix.length; + const nextChar = str[after]; + const isBoundary = after === str.length || nextChar === '/' || nextChar === '\\'; + out += str.slice(index, match); + out += isBoundary ? replacement : prefix; + index = after; + } + }; + + // Strip repo prefix from file:// URLs only; keep other schemes intact. + const stripFileUrlRoot = (str) => { + if (!fileUrlRoot) return str; + const needle = `${fileUrlPrefix}${fileUrlRoot}`; + let out = ''; + let index = 0; + while (true) { + const match = str.indexOf(needle, index); + if (match === -1) return out + str.slice(index); + const after = match + needle.length; + const nextChar = str[after]; + const isBoundary = after === str.length || nextChar === '/'; + out += str.slice(index, match); + out += isBoundary ? `${fileUrlPrefix}${replacement}` : needle; + index = after; + } + }; return (str) => { - return str.replaceAll('\\\'', "'").replaceAll(projectRoot, replacement); + let out = str.replaceAll('\\\'', "'"); + out = stripFileUrlRoot(out); + out = stripPrefixAtBoundary(out, projectRoot); + if (projectRootPosix !== projectRoot) out = stripPrefixAtBoundary(out, projectRootPosix); + return out; }; } diff --git a/test/es-module/test-esm-import-meta.mjs b/test/es-module/test-esm-import-meta.mjs index 638989724bbfd4..76e05ab74a2028 100644 --- a/test/es-module/test-esm-import-meta.mjs +++ b/test/es-module/test-esm-import-meta.mjs @@ -1,5 +1,7 @@ import '../common/index.mjs'; import assert from 'assert'; +import path from 'path'; +import { fileURLToPath, pathToFileURL } from 'url'; assert.strictEqual(Object.getPrototypeOf(import.meta), null); @@ -16,18 +18,14 @@ for (const descriptor of Object.values(descriptors)) { }); } -const urlReg = /^file:\/\/\/.*\/test\/es-module\/test-esm-import-meta\.mjs$/; -assert.match(import.meta.url, urlReg); +const filePath = fileURLToPath(import.meta.url); +assert.ok(path.isAbsolute(filePath)); +assert.strictEqual(import.meta.url, pathToFileURL(filePath).href); -// Match *nix paths: `/some/path/test/es-module` -// Match Windows paths: `d:\\some\\path\\test\\es-module` -const dirReg = /^(\/|\w:\\).*(\/|\\)test(\/|\\)es-module$/; -assert.match(import.meta.dirname, dirReg); - -// Match *nix paths: `/some/path/test/es-module/test-esm-import-meta.mjs` -// Match Windows paths: `d:\\some\\path\\test\\es-module\\test-esm-import-meta.js` -const fileReg = /^(\/|\w:\\).*(\/|\\)test(\/|\\)es-module(\/|\\)test-esm-import-meta\.mjs$/; -assert.match(import.meta.filename, fileReg); +const suffix = path.join('test', 'es-module', 'test-esm-import-meta.mjs'); +assert.ok(filePath.endsWith(suffix)); +assert.strictEqual(import.meta.filename, filePath); +assert.strictEqual(import.meta.dirname, path.dirname(filePath)); // Verify that `data:` imports do not behave like `file:` imports. import dataDirname from 'data:text/javascript,export default "dirname" in import.meta'; diff --git a/test/parallel/test-assert-snapshot-transform-project-root.js b/test/parallel/test-assert-snapshot-transform-project-root.js new file mode 100644 index 00000000000000..bd23732dcacb90 --- /dev/null +++ b/test/parallel/test-assert-snapshot-transform-project-root.js @@ -0,0 +1,45 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const path = require('node:path'); +const { pathToFileURL } = require('node:url'); + +const snapshot = require('../common/assertSnapshot'); + +// Coverage: boundary strip, URL tokens, file:// root, % encoding. +{ + const stripProjectRoot = snapshot.transformProjectRoot(''); + const projectRoot = path.resolve(__dirname, '..', '..'); + + assert.strictEqual( + stripProjectRoot(`${projectRoot}${path.sep}test${path.sep}fixtures`), + `${path.sep}test${path.sep}fixtures`, + ); + + const shouldNotStrip = `${projectRoot}_modules`; + assert.strictEqual(stripProjectRoot(shouldNotStrip), shouldNotStrip); + + const urlLike = `https://${projectRoot}js.org`; + assert.strictEqual(stripProjectRoot(urlLike), urlLike); + + const shouldNotStripSuffix = `${projectRoot}suffix/test`; + assert.strictEqual(stripProjectRoot(shouldNotStripSuffix), shouldNotStripSuffix); + + const fileUrl = pathToFileURL(projectRoot).href; + assert.strictEqual(stripProjectRoot(`${fileUrl}/test/fixtures`), 'file:///test/fixtures'); + + const fileUrlWithSpace = pathToFileURL(path.join(projectRoot, 'spaced dir')).href; + assert.strictEqual(stripProjectRoot(fileUrlWithSpace), 'file:///spaced%20dir'); + + if (process.platform === 'win32') { + const projectRootPosix = projectRoot.replaceAll(path.win32.sep, path.posix.sep); + + assert.strictEqual( + stripProjectRoot(`${projectRootPosix}/test/fixtures`), + '/test/fixtures', + ); + + const shouldNotStripPosix = `${projectRootPosix}_modules`; + assert.strictEqual(stripProjectRoot(shouldNotStripPosix), shouldNotStripPosix); + } +} diff --git a/test/parallel/test-cli-permission-deny-fs.js b/test/parallel/test-cli-permission-deny-fs.js index d5744cac94db3d..1810b375ac7776 100644 --- a/test/parallel/test-cli-permission-deny-fs.js +++ b/test/parallel/test-cli-permission-deny-fs.js @@ -132,9 +132,10 @@ const path = require('path'); } { - const { root } = path.parse(process.cwd()); - const abs = (p) => path.join(root, p); - const firstPath = abs(path.sep + process.cwd().split(path.sep, 2)[1]); + const repoRoot = path.resolve(process.cwd()); + const root = path.parse(repoRoot).root; + const [firstDir] = path.relative(root, repoRoot).split(path.sep); + const firstPath = firstDir ? path.join(root, firstDir) : path.join(root, 'test'); if (firstPath.startsWith('/etc')) { common.skip('/etc as firstPath'); } diff --git a/test/parallel/test-node-output-console.mjs b/test/parallel/test-node-output-console.mjs index e989b79ab505f6..1757bc33457860 100644 --- a/test/parallel/test-node-output-console.mjs +++ b/test/parallel/test-node-output-console.mjs @@ -12,9 +12,12 @@ function replaceStackTrace(str) { } describe('console output', { concurrency: !process.env.TEST_PARALLEL }, () => { - function normalize(str) { - return str.replaceAll(snapshot.replaceWindowsPaths(process.cwd()), '').replaceAll('/', '*').replaceAll(process.version, '*').replaceAll(/\d+/g, '*'); - } + const normalize = snapshot.transform( + snapshot.transformProjectRoot(''), + (str) => str.replaceAll('/', '*') + .replaceAll(process.version, '*') + .replaceAll(/\d+/g, '*'), + ); const tests = [ { name: 'console/2100bytes.js' }, { name: 'console/console_low_stack_space.js' }, diff --git a/test/parallel/test-node-output-errors.mjs b/test/parallel/test-node-output-errors.mjs index 3d913475d8d2a0..0d594c4bc6ed57 100644 --- a/test/parallel/test-node-output-errors.mjs +++ b/test/parallel/test-node-output-errors.mjs @@ -4,12 +4,10 @@ import * as snapshot from '../common/assertSnapshot.js'; import * as os from 'node:os'; import { describe, it } from 'node:test'; import { basename } from 'node:path'; -import { pathToFileURL } from 'node:url'; const skipForceColors = (common.isWindows && (Number(os.release().split('.')[0]) !== 10 || Number(os.release().split('.')[2]) < 14393)); // See https://github.com/nodejs/node/pull/33132 - function replaceStackTrace(str) { return snapshot.replaceStackTrace(str, '$1at *$7\n'); } @@ -22,8 +20,7 @@ function replaceForceColorsStackTrace(str) { describe('errors output', { concurrency: !process.env.TEST_PARALLEL }, () => { function normalize(str) { const baseName = basename(process.argv0 || 'node', '.exe'); - return str.replaceAll(snapshot.replaceWindowsPaths(process.cwd()), '') - .replaceAll(pathToFileURL(process.cwd()).pathname, '') + return str .replaceAll('//', '*') .replaceAll(/\/(\w)/g, '*$1') .replaceAll('*test*', '*') @@ -36,7 +33,11 @@ describe('errors output', { concurrency: !process.env.TEST_PARALLEL }, () => { return normalize(str).replaceAll(/\d+:\d+/g, '*:*').replaceAll(/:\d+/g, ':*').replaceAll('*fixtures*message*', '*'); } const common = snapshot - .transform(snapshot.replaceWindowsLineEndings, snapshot.replaceWindowsPaths); + .transform( + snapshot.replaceWindowsLineEndings, + snapshot.replaceWindowsPaths, + snapshot.transformProjectRoot(''), + ); const defaultTransform = snapshot.transform(common, normalize, snapshot.replaceNodeVersion); const errTransform = snapshot.transform(common, normalizeNoNumbers, snapshot.replaceNodeVersion); const promiseTransform = snapshot.transform(common, replaceStackTrace, diff --git a/test/parallel/test-node-output-eval.mjs b/test/parallel/test-node-output-eval.mjs index 56a880c8adfee1..4d73de7611aa01 100644 --- a/test/parallel/test-node-output-eval.mjs +++ b/test/parallel/test-node-output-eval.mjs @@ -8,10 +8,10 @@ import { basename } from 'node:path'; import { describe, it } from 'node:test'; describe('eval output', { concurrency: true }, () => { - function normalize(str) { - return str.replaceAll(snapshot.replaceWindowsPaths(process.cwd()), '') - .replaceAll(/\d+:\d+/g, '*:*'); - } + const normalize = snapshot.transform( + snapshot.transformProjectRoot(''), + (str) => str.replaceAll(/\d+:\d+/g, '*:*'), + ); const defaultTransform = snapshot.transform( normalize, diff --git a/test/parallel/test-node-output-v8-warning.mjs b/test/parallel/test-node-output-v8-warning.mjs index b5e52d493baf55..9e3264931582c2 100644 --- a/test/parallel/test-node-output-v8-warning.mjs +++ b/test/parallel/test-node-output-v8-warning.mjs @@ -14,14 +14,14 @@ function replaceExecName(str) { } describe('v8 output', { concurrency: !process.env.TEST_PARALLEL }, () => { - function normalize(str) { - return str.replaceAll(snapshot.replaceWindowsPaths(process.cwd()), '') - .replaceAll(/:\d+/g, ':*') - .replaceAll('/', '*') - .replaceAll('*test*', '*') - .replaceAll(/.*?\*fixtures\*v8\*/g, '(node:*) V8: *') // Replace entire path before fixtures/v8 - .replaceAll('*fixtures*v8*', '*'); - } + const normalize = snapshot.transform( + snapshot.transformProjectRoot(''), + (str) => str.replaceAll(/:\d+/g, ':*') + .replaceAll('/', '*') + .replaceAll('*test*', '*') + .replaceAll(/.*?\*fixtures\*v8\*/g, '(node:*) V8: *') // Replace entire path before fixtures/v8 + .replaceAll('*fixtures*v8*', '*'), + ); const common = snapshot .transform(snapshot.replaceWindowsLineEndings, snapshot.replaceWindowsPaths, replaceNodeVersion, replaceExecName); const defaultTransform = snapshot.transform(common, normalize); diff --git a/test/parallel/test-node-output-vm.mjs b/test/parallel/test-node-output-vm.mjs index aa5072007b4fd7..60190603a82ff1 100644 --- a/test/parallel/test-node-output-vm.mjs +++ b/test/parallel/test-node-output-vm.mjs @@ -8,9 +8,13 @@ function replaceNodeVersion(str) { } describe('vm output', { concurrency: !process.env.TEST_PARALLEL }, () => { - function normalize(str) { - return str.replaceAll(snapshot.replaceWindowsPaths(process.cwd()), '').replaceAll('//', '*').replaceAll(/\/(\w)/g, '*$1').replaceAll('*test*', '*').replaceAll(/node:vm:\d+:\d+/g, 'node:vm:*'); - } + const normalize = snapshot.transform( + snapshot.transformProjectRoot(''), + (str) => str.replaceAll('//', '*') + .replaceAll(/\/(\w)/g, '*$1') + .replaceAll('*test*', '*') + .replaceAll(/node:vm:\d+:\d+/g, 'node:vm:*'), + ); const defaultTransform = snapshot .transform(snapshot.replaceWindowsLineEndings, snapshot.replaceWindowsPaths, normalize, replaceNodeVersion); From c63b5e62309a6cfc935ac0249d3e7eda54913082 Mon Sep 17 00:00:00 2001 From: cwkang Date: Tue, 13 Jan 2026 13:15:46 +0900 Subject: [PATCH 2/2] test: fix file URL root stripping in snapshot normalization --- test/common/assertSnapshot.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/common/assertSnapshot.js b/test/common/assertSnapshot.js index 2fbc6862310e7a..95f8c20584bb95 100644 --- a/test/common/assertSnapshot.js +++ b/test/common/assertSnapshot.js @@ -79,7 +79,13 @@ function transformProjectRoot(replacement = '') { const isBoundary = after === str.length || nextChar === '/'; out += str.slice(index, match); out += isBoundary ? `${fileUrlPrefix}${replacement}` : needle; - index = after; + + // If the next character is a '/' and the replacement is empty, skip the next character. + if (isBoundary && replacement === '' && nextChar === '/') { + index = after + 1; + } else { + index = after; + } } }; return (str) => {