test_runner: refactor mock_loader

PR-URL: https://github.com/nodejs/node/pull/54223
Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
Antoine du Hamel 2024-08-21 17:44:29 +02:00 committed by GitHub
parent 6b6b97766c
commit b4344cf006
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 22 additions and 42 deletions

View file

@ -10,21 +10,16 @@ const {
}, },
} = primordials; } = primordials;
const { const {
ensureNodeScheme,
kBadExportsMessage, kBadExportsMessage,
kMockSearchParam, kMockSearchParam,
kMockSuccess, kMockSuccess,
kMockExists, kMockExists,
kMockUnknownMessage, kMockUnknownMessage,
} = require('internal/test_runner/mock/mock'); } = require('internal/test_runner/mock/mock');
const { pathToFileURL, URL } = require('internal/url'); const { URL, URLParse } = require('internal/url');
const { normalizeReferrerURL } = require('internal/modules/helpers');
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => { let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
debug = fn; debug = fn;
}); });
const { createRequire, isBuiltin } = require('module');
const { defaultGetFormatWithoutErrors } = require('internal/modules/esm/get_format');
const { defaultResolve } = require('internal/modules/esm/resolve');
// TODO(cjihrig): The mocks need to be thread aware because the exports are // TODO(cjihrig): The mocks need to be thread aware because the exports are
// evaluated on the thread that creates the mock. Before marking this API as // evaluated on the thread that creates the mock. Before marking this API as
@ -79,46 +74,18 @@ async function initialize(data) {
async function resolve(specifier, context, nextResolve) { async function resolve(specifier, context, nextResolve) {
debug('resolve hook entry, specifier = "%s", context = %o', specifier, context); debug('resolve hook entry, specifier = "%s", context = %o', specifier, context);
let mockSpecifier;
if (isBuiltin(specifier)) { const nextResolveResult = await nextResolve(specifier, context);
mockSpecifier = ensureNodeScheme(specifier); const mockSpecifier = nextResolveResult.url;
} else {
let format;
if (context.parentURL) {
format = defaultGetFormatWithoutErrors(pathToFileURL(context.parentURL));
}
try {
if (format === 'module') {
specifier = defaultResolve(specifier, context).url;
} else {
specifier = pathToFileURL(
createRequire(context.parentURL).resolve(specifier),
).href;
}
} catch {
const parentURL = normalizeReferrerURL(context.parentURL);
const parsedURL = URL.parse(specifier, parentURL)?.href;
if (parsedURL) {
specifier = parsedURL;
}
}
mockSpecifier = specifier;
}
const mock = mocks.get(mockSpecifier); const mock = mocks.get(mockSpecifier);
debug('resolve hook, specifier = "%s", mock = %o', specifier, mock); debug('resolve hook, specifier = "%s", mock = %o', specifier, mock);
if (mock?.active !== true) { if (mock?.active !== true) {
return nextResolve(specifier, context); return nextResolveResult;
} }
const url = new URL(mockSpecifier); const url = new URL(mockSpecifier);
url.searchParams.set(kMockSearchParam, mock.localVersion); url.searchParams.set(kMockSearchParam, mock.localVersion);
if (!mock.cache) { if (!mock.cache) {
@ -127,13 +94,14 @@ async function resolve(specifier, context, nextResolve) {
mock.localVersion++; mock.localVersion++;
} }
debug('resolve hook finished, url = "%s"', url.href); const { href } = url;
return nextResolve(url.href, context); debug('resolve hook finished, url = "%s"', href);
return { __proto__: null, url: href, format: nextResolveResult.format };
} }
async function load(url, context, nextLoad) { async function load(url, context, nextLoad) {
debug('load hook entry, url = "%s", context = %o', url, context); debug('load hook entry, url = "%s", context = %o', url, context);
const parsedURL = URL.parse(url); const parsedURL = URLParse(url);
if (parsedURL) { if (parsedURL) {
parsedURL.searchParams.delete(kMockSearchParam); parsedURL.searchParams.delete(kMockSearchParam);
} }

View file

@ -34,7 +34,13 @@ const {
} = require('internal/errors'); } = require('internal/errors');
const esmLoader = require('internal/modules/esm/loader'); const esmLoader = require('internal/modules/esm/loader');
const { getOptionValue } = require('internal/options'); const { getOptionValue } = require('internal/options');
const { fileURLToPath, toPathIfFileURL, URL, isURL } = require('internal/url'); const {
fileURLToPath,
isURL,
pathToFileURL,
toPathIfFileURL,
URL,
} = require('internal/url');
const { const {
emitExperimentalWarning, emitExperimentalWarning,
getStructuredStack, getStructuredStack,
@ -511,7 +517,7 @@ class MockTracker {
// Get the file that called this function. We need four stack frames: // Get the file that called this function. We need four stack frames:
// vm context -> getStructuredStack() -> this function -> actual caller. // vm context -> getStructuredStack() -> this function -> actual caller.
const caller = getStructuredStack()[3]?.getFileName(); const caller = pathToFileURL(getStructuredStack()[3]?.getFileName()).href;
const { format, url } = sharedState.moduleLoader.resolveSync( const { format, url } = sharedState.moduleLoader.resolveSync(
mockSpecifier, caller, null, mockSpecifier, caller, null,
); );

View file

@ -407,6 +407,12 @@ test('modules cannot be mocked multiple times at once', async (t) => {
assert.strictEqual(mocked.fn(), 42); assert.strictEqual(mocked.fn(), 42);
}); });
await t.test('Importing a Windows path should fail', { skip: !common.isWindows }, async (t) => {
const fixture = fixtures.path('module-mocking', 'wrong-path.js');
t.mock.module(fixture, { namedExports: { fn() { return 42; } } });
await assert.rejects(import(fixture), { code: 'ERR_UNSUPPORTED_ESM_URL_SCHEME' });
});
}); });
test('mocks are automatically restored', async (t) => { test('mocks are automatically restored', async (t) => {