Upload Kmake

This commit is contained in:
Gorochu
2026-05-26 23:36:42 -07:00
parent ba051b2f74
commit 555ec72358
41615 changed files with 13344630 additions and 1 deletions

View File

@ -0,0 +1,208 @@
'use strict';
const {
JSONStringify,
SafeMap,
globalThis: {
Atomics: {
notify: AtomicsNotify,
store: AtomicsStore,
},
},
} = primordials;
const {
kBadExportsMessage,
kMockSearchParam,
kMockSuccess,
kMockExists,
kMockUnknownMessage,
} = require('internal/test_runner/mock/mock');
const { URL, URLParse } = require('internal/url');
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
debug = fn;
});
// 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
// stable, one of the following issues needs to be implemented:
// https://github.com/nodejs/node/issues/49472
// or https://github.com/nodejs/node/issues/52219
const mocks = new SafeMap();
async function initialize(data) {
data?.port.on('message', ({ type, payload }) => {
debug('mock loader received message type "%s" with payload %o', type, payload);
if (type === 'node:test:register') {
const { baseURL } = payload;
const mock = mocks.get(baseURL);
if (mock?.active) {
debug('already mocking "%s"', baseURL);
sendAck(payload.ack, kMockExists);
return;
}
const localVersion = mock?.localVersion ?? 0;
debug('new mock version %d for "%s"', localVersion, baseURL);
mocks.set(baseURL, {
__proto__: null,
active: true,
cache: payload.cache,
exportNames: payload.exportNames,
format: payload.format,
hasDefaultExport: payload.hasDefaultExport,
localVersion,
url: baseURL,
});
sendAck(payload.ack);
} else if (type === 'node:test:unregister') {
const mock = mocks.get(payload.baseURL);
if (mock !== undefined) {
mock.active = false;
mock.localVersion++;
}
sendAck(payload.ack);
} else {
sendAck(payload.ack, kMockUnknownMessage);
}
});
}
async function resolve(specifier, context, nextResolve) {
debug('resolve hook entry, specifier = "%s", context = %o', specifier, context);
const nextResolveResult = await nextResolve(specifier, context);
const mockSpecifier = nextResolveResult.url;
const mock = mocks.get(mockSpecifier);
debug('resolve hook, specifier = "%s", mock = %o', specifier, mock);
if (mock?.active !== true) {
return nextResolveResult;
}
const url = new URL(mockSpecifier);
url.searchParams.set(kMockSearchParam, mock.localVersion);
if (!mock.cache) {
// With ESM, we can't remove modules from the cache. Bump the module's
// version instead so that the next import will be uncached.
mock.localVersion++;
}
const { href } = url;
debug('resolve hook finished, url = "%s"', href);
return { __proto__: null, url: href, format: nextResolveResult.format };
}
async function load(url, context, nextLoad) {
debug('load hook entry, url = "%s", context = %o', url, context);
const parsedURL = URLParse(url);
if (parsedURL) {
parsedURL.searchParams.delete(kMockSearchParam);
}
const baseURL = parsedURL ? parsedURL.href : url;
const mock = mocks.get(baseURL);
const original = await nextLoad(url, context);
debug('load hook, mock = %o', mock);
if (mock?.active !== true) {
return original;
}
// Treat builtins as commonjs because customization hooks do not allow a
// core module to be replaced.
// Also collapse 'commonjs-sync' and 'require-commonjs' to 'commonjs'.
let format = original.format;
switch (original.format) {
case 'builtin': // Deliberate fallthrough
case 'commonjs-sync': // Deliberate fallthrough
case 'require-commonjs':
format = 'commonjs';
break;
case 'json':
format = 'module';
break;
}
const result = {
__proto__: null,
format,
shortCircuit: true,
source: await createSourceFromMock(mock, format),
};
debug('load hook finished, result = %o', result);
return result;
}
async function createSourceFromMock(mock, format) {
// Create mock implementation from provided exports.
const { exportNames, hasDefaultExport, url } = mock;
const useESM = format === 'module' || format === 'module-typescript';
const source = `${testImportSource(useESM)}
if (!$__test.mock._mockExports.has(${JSONStringify(url)})) {
throw new Error(${JSONStringify(`mock exports not found for "${url}"`)});
}
const $__exports = $__test.mock._mockExports.get(${JSONStringify(url)});
${defaultExportSource(useESM, hasDefaultExport)}
${namedExportsSource(useESM, exportNames)}
`;
return source;
}
function testImportSource(useESM) {
if (useESM) {
return "import $__test from 'node:test';";
}
return "const $__test = require('node:test');";
}
function defaultExportSource(useESM, hasDefaultExport) {
if (!hasDefaultExport) {
return '';
} else if (useESM) {
return 'export default $__exports.defaultExport;';
}
return 'module.exports = $__exports.defaultExport;';
}
function namedExportsSource(useESM, exportNames) {
let source = '';
if (!useESM && exportNames.length > 0) {
source += `
if (module.exports === null || typeof module.exports !== 'object') {
throw new Error('${JSONStringify(kBadExportsMessage)}');
}
`;
}
for (let i = 0; i < exportNames.length; ++i) {
const name = exportNames[i];
if (useESM) {
source += `export let ${name} = $__exports.namedExports[${JSONStringify(name)}];\n`;
} else {
source += `module.exports[${JSONStringify(name)}] = $__exports.namedExports[${JSONStringify(name)}];\n`;
}
}
return source;
}
function sendAck(buf, status = kMockSuccess) {
AtomicsStore(buf, 0, status);
AtomicsNotify(buf, 0);
}
module.exports = { initialize, load, resolve };

View File

@ -0,0 +1,811 @@
'use strict';
const {
ArrayPrototypePush,
ArrayPrototypeSlice,
Error,
FunctionPrototypeBind,
FunctionPrototypeCall,
Int32Array,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetPrototypeOf,
ObjectKeys,
Proxy,
ReflectApply,
ReflectConstruct,
ReflectGet,
SafeMap,
StringPrototypeSlice,
StringPrototypeStartsWith,
globalThis: {
Atomics: {
store: AtomicsStore,
wait: AtomicsWait,
},
SharedArrayBuffer,
},
} = primordials;
const {
codes: {
ERR_INVALID_ARG_TYPE,
ERR_INVALID_ARG_VALUE,
ERR_INVALID_STATE,
},
} = require('internal/errors');
const esmLoader = require('internal/modules/esm/loader');
const { getOptionValue } = require('internal/options');
const {
fileURLToPath,
isURL,
pathToFileURL,
toPathIfFileURL,
URL,
} = require('internal/url');
const {
emitExperimentalWarning,
getStructuredStack,
kEmptyObject,
} = require('internal/util');
let debug = require('internal/util/debuglog').debuglog('test_runner', (fn) => {
debug = fn;
});
const {
validateBoolean,
validateFunction,
validateInteger,
validateObject,
validateOneOf,
} = require('internal/validators');
const { MockTimers } = require('internal/test_runner/mock/mock_timers');
const { strictEqual, notStrictEqual } = require('assert');
const { Module } = require('internal/modules/cjs/loader');
const { MessageChannel } = require('worker_threads');
const { _load, _nodeModulePaths, _resolveFilename, isBuiltin } = Module;
function kDefaultFunction() {}
const enableModuleMocking = getOptionValue('--experimental-test-module-mocks');
const kMockSearchParam = 'node-test-mock';
const kMockSuccess = 1;
const kMockExists = 2;
const kMockUnknownMessage = 3;
const kWaitTimeout = 5_000;
const kBadExportsMessage = 'Cannot create mock because named exports ' +
'cannot be applied to the provided default export.';
const kSupportedFormats = [
'builtin',
'commonjs-typescript',
'commonjs',
'json',
'module-typescript',
'module',
];
let sharedModuleState;
class MockFunctionContext {
#calls;
#mocks;
#implementation;
#restore;
#times;
constructor(implementation, restore, times) {
this.#calls = [];
this.#mocks = new SafeMap();
this.#implementation = implementation;
this.#restore = restore;
this.#times = times;
}
/**
* Gets an array of recorded calls made to the mock function.
* @returns {Array} An array of recorded calls.
*/
get calls() {
return ArrayPrototypeSlice(this.#calls, 0);
}
/**
* Retrieves the number of times the mock function has been called.
* @returns {number} The call count.
*/
callCount() {
return this.#calls.length;
}
/**
* Sets a new implementation for the mock function.
* @param {Function} implementation - The new implementation for the mock function.
*/
mockImplementation(implementation) {
validateFunction(implementation, 'implementation');
this.#implementation = implementation;
}
/**
* Replaces the implementation of the function only once.
* @param {Function} implementation - The substitute function.
* @param {number} [onCall] - The call index to be replaced.
*/
mockImplementationOnce(implementation, onCall) {
validateFunction(implementation, 'implementation');
const nextCall = this.#calls.length;
const call = onCall ?? nextCall;
validateInteger(call, 'onCall', nextCall);
this.#mocks.set(call, implementation);
}
/**
* Restores the original function that was mocked.
*/
restore() {
const { descriptor, object, original, methodName } = this.#restore;
if (typeof methodName === 'string') {
// This is an object method spy.
ObjectDefineProperty(object, methodName, descriptor);
} else {
// This is a bare function spy. There isn't much to do here but make
// the mock call the original function.
this.#implementation = original;
}
}
/**
* Resets the recorded calls to the mock function
*/
resetCalls() {
this.#calls = [];
}
/**
* Tracks a call made to the mock function.
* @param {object} call - The call details.
*/
trackCall(call) {
ArrayPrototypePush(this.#calls, call);
}
/**
* Gets the next implementation to use for the mock function.
* @returns {Function} The next implementation.
*/
nextImpl() {
const nextCall = this.#calls.length;
const mock = this.#mocks.get(nextCall);
const impl = mock ?? this.#implementation;
if (nextCall + 1 === this.#times) {
this.restore();
}
this.#mocks.delete(nextCall);
return impl;
}
}
const {
nextImpl,
restore: restoreFn,
trackCall,
} = MockFunctionContext.prototype;
delete MockFunctionContext.prototype.trackCall;
delete MockFunctionContext.prototype.nextImpl;
class MockModuleContext {
#restore;
#sharedState;
constructor({
baseURL,
cache,
caller,
defaultExport,
format,
fullPath,
hasDefaultExport,
namedExports,
sharedState,
}) {
const ack = new Int32Array(new SharedArrayBuffer(4));
const config = {
__proto__: null,
cache,
defaultExport,
hasDefaultExport,
namedExports,
caller: toPathIfFileURL(caller),
};
sharedState.mockMap.set(baseURL, config);
sharedState.mockMap.set(fullPath, config);
this.#sharedState = sharedState;
this.#restore = {
__proto__: null,
ack,
baseURL,
cached: fullPath in Module._cache,
format,
fullPath,
value: Module._cache[fullPath],
};
sharedState.loaderPort.postMessage({
__proto__: null,
type: 'node:test:register',
payload: {
__proto__: null,
ack,
baseURL,
cache,
exportNames: ObjectKeys(namedExports),
hasDefaultExport,
format,
},
});
waitForAck(ack);
delete Module._cache[fullPath];
sharedState.mockExports.set(baseURL, {
__proto__: null,
defaultExport,
namedExports,
});
}
restore() {
if (this.#restore === undefined) {
return;
}
// Delete the mock CJS cache entry. If the module was previously in the
// cache then restore the old value.
delete Module._cache[this.#restore.fullPath];
if (this.#restore.cached) {
Module._cache[this.#restore.fullPath] = this.#restore.value;
}
AtomicsStore(this.#restore.ack, 0, 0);
this.#sharedState.loaderPort.postMessage({
__proto__: null,
type: 'node:test:unregister',
payload: {
__proto__: null,
ack: this.#restore.ack,
baseURL: this.#restore.baseURL,
},
});
waitForAck(this.#restore.ack);
this.#sharedState.mockMap.delete(this.#restore.baseURL);
this.#sharedState.mockMap.delete(this.#restore.fullPath);
this.#restore = undefined;
}
}
const { restore: restoreModule } = MockModuleContext.prototype;
class MockTracker {
#mocks = [];
#timers;
/**
* Returns the mock timers of this MockTracker instance.
* @returns {MockTimers} The mock timers instance.
*/
get timers() {
this.#timers ??= new MockTimers();
return this.#timers;
}
/**
* Creates a mock function tracker.
* @param {Function} [original] - The original function to be tracked.
* @param {Function} [implementation] - An optional replacement function for the original one.
* @param {object} [options] - Additional tracking options.
* @param {number} [options.times=Infinity] - The maximum number of times the mock function can be called.
* @returns {ProxyConstructor} The mock function tracker.
*/
fn(
original = function() {},
implementation = original,
options = kEmptyObject,
) {
if (original !== null && typeof original === 'object') {
options = original;
original = function() {};
implementation = original;
} else if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = original;
}
validateFunction(original, 'original');
validateFunction(implementation, 'implementation');
validateObject(options, 'options');
const { times = Infinity } = options;
validateTimes(times, 'options.times');
const ctx = new MockFunctionContext(implementation, { __proto__: null, original }, times);
return this.#setupMock(ctx, original);
}
/**
* Creates a method tracker for a specified object or function.
* @param {(object | Function)} objectOrFunction - The object or function containing the method to be tracked.
* @param {string} methodName - The name of the method to be tracked.
* @param {Function} [implementation] - An optional replacement function for the original method.
* @param {object} [options] - Additional tracking options.
* @param {boolean} [options.getter=false] - Indicates whether this is a getter method.
* @param {boolean} [options.setter=false] - Indicates whether this is a setter method.
* @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called.
* @returns {ProxyConstructor} The mock method tracker.
*/
method(
objectOrFunction,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject,
) {
validateStringOrSymbol(methodName, 'methodName');
if (typeof objectOrFunction !== 'function') {
validateObject(objectOrFunction, 'object');
}
if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = kDefaultFunction;
}
validateFunction(implementation, 'implementation');
validateObject(options, 'options');
const {
getter = false,
setter = false,
times = Infinity,
} = options;
validateBoolean(getter, 'options.getter');
validateBoolean(setter, 'options.setter');
validateTimes(times, 'options.times');
if (setter && getter) {
throw new ERR_INVALID_ARG_VALUE(
'options.setter', setter, "cannot be used with 'options.getter'",
);
}
const descriptor = findMethodOnPrototypeChain(objectOrFunction, methodName);
let original;
if (getter) {
original = descriptor?.get;
} else if (setter) {
original = descriptor?.set;
} else {
original = descriptor?.value;
}
if (typeof original !== 'function') {
throw new ERR_INVALID_ARG_VALUE(
'methodName', original, 'must be a method',
);
}
const restore = { __proto__: null, descriptor, object: objectOrFunction, methodName };
const impl = implementation === kDefaultFunction ?
original : implementation;
const ctx = new MockFunctionContext(impl, restore, times);
const mock = this.#setupMock(ctx, original);
const mockDescriptor = {
__proto__: null,
configurable: descriptor.configurable,
enumerable: descriptor.enumerable,
};
if (getter) {
mockDescriptor.get = mock;
mockDescriptor.set = descriptor.set;
} else if (setter) {
mockDescriptor.get = descriptor.get;
mockDescriptor.set = mock;
} else {
mockDescriptor.writable = descriptor.writable;
mockDescriptor.value = mock;
}
ObjectDefineProperty(objectOrFunction, methodName, mockDescriptor);
return mock;
}
/**
* Mocks a getter method of an object.
* This is a syntax sugar for the MockTracker.method with options.getter set to true
* @param {object} object - The target object.
* @param {string} methodName - The name of the getter method to be mocked.
* @param {Function} [implementation] - An optional replacement function for the targeted method.
* @param {object} [options] - Additional tracking options.
* @param {boolean} [options.getter=true] - Indicates whether this is a getter method.
* @param {boolean} [options.setter=false] - Indicates whether this is a setter method.
* @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called.
* @returns {ProxyConstructor} The mock method tracker.
*/
getter(
object,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject,
) {
if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = kDefaultFunction;
} else {
validateObject(options, 'options');
}
const { getter = true } = options;
if (getter === false) {
throw new ERR_INVALID_ARG_VALUE(
'options.getter', getter, 'cannot be false',
);
}
return this.method(object, methodName, implementation, {
__proto__: null,
...options,
getter,
});
}
/**
* Mocks a setter method of an object.
* This function is a syntax sugar for MockTracker.method with options.setter set to true.
* @param {object} object - The target object.
* @param {string} methodName - The setter method to be mocked.
* @param {Function} [implementation] - An optional replacement function for the targeted method.
* @param {object} [options] - Additional tracking options.
* @param {boolean} [options.getter=false] - Indicates whether this is a getter method.
* @param {boolean} [options.setter=true] - Indicates whether this is a setter method.
* @param {number} [options.times=Infinity] - The maximum number of times the mock method can be called.
* @returns {ProxyConstructor} The mock method tracker.
*/
setter(
object,
methodName,
implementation = kDefaultFunction,
options = kEmptyObject,
) {
if (implementation !== null && typeof implementation === 'object') {
options = implementation;
implementation = kDefaultFunction;
} else {
validateObject(options, 'options');
}
const { setter = true } = options;
if (setter === false) {
throw new ERR_INVALID_ARG_VALUE(
'options.setter', setter, 'cannot be false',
);
}
return this.method(object, methodName, implementation, {
__proto__: null,
...options,
setter,
});
}
module(specifier, options = kEmptyObject) {
emitExperimentalWarning('Module mocking');
if (typeof specifier !== 'string') {
if (!isURL(specifier))
throw new ERR_INVALID_ARG_TYPE('specifier', ['string', 'URL'], specifier);
specifier = `${specifier}`;
}
validateObject(options, 'options');
debug('module mock entry, specifier = "%s", options = %o', specifier, options);
const {
cache = false,
namedExports = kEmptyObject,
defaultExport,
} = options;
const hasDefaultExport = 'defaultExport' in options;
validateBoolean(cache, 'options.cache');
validateObject(namedExports, 'options.namedExports');
const sharedState = setupSharedModuleState();
const mockSpecifier = StringPrototypeStartsWith(specifier, 'node:') ?
StringPrototypeSlice(specifier, 5) : specifier;
// Get the file that called this function. We need four stack frames:
// vm context -> getStructuredStack() -> this function -> actual caller.
const filename = getStructuredStack()[3]?.getFileName();
// If the caller is already a file URL, use it as is. Otherwise, convert it.
const hasFileProtocol = StringPrototypeStartsWith(filename, 'file://');
const caller = hasFileProtocol ? filename : pathToFileURL(filename).href;
const { format, url } = sharedState.moduleLoader.resolveSync(
mockSpecifier, caller, null,
);
debug('module mock, url = "%s", format = "%s", caller = "%s"', url, format, caller);
if (format) { // Format is not yet known for ambiguous files when detection is enabled.
validateOneOf(format, 'format', kSupportedFormats);
}
const baseURL = URL.parse(url);
if (!baseURL) {
throw new ERR_INVALID_ARG_VALUE(
'specifier', specifier, 'cannot compute URL',
);
}
if (baseURL.searchParams.has(kMockSearchParam)) {
throw new ERR_INVALID_STATE(
`Cannot mock '${specifier}.' The module is already mocked.`,
);
}
const fullPath = StringPrototypeStartsWith(url, 'file://') ?
fileURLToPath(url) : null;
const ctx = new MockModuleContext({
__proto__: null,
baseURL: baseURL.href,
cache,
caller,
defaultExport,
format,
fullPath,
hasDefaultExport,
namedExports,
sharedState,
specifier: mockSpecifier,
});
ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreModule,
});
return ctx;
}
/**
* Resets the mock tracker, restoring all mocks and clearing timers.
*/
reset() {
this.restoreAll();
this.#timers?.reset();
this.#mocks = [];
}
/**
* Restore all mocks created by this MockTracker instance.
*/
restoreAll() {
for (let i = 0; i < this.#mocks.length; i++) {
const { ctx, restore } = this.#mocks[i];
FunctionPrototypeCall(restore, ctx);
}
}
#setupMock(ctx, fnToMatch) {
const mock = new Proxy(fnToMatch, {
__proto__: null,
apply(_fn, thisArg, argList) {
const fn = FunctionPrototypeCall(nextImpl, ctx);
let result;
let error;
try {
result = ReflectApply(fn, thisArg, argList);
} catch (err) {
error = err;
throw err;
} finally {
FunctionPrototypeCall(trackCall, ctx, {
__proto__: null,
arguments: argList,
error,
result,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
target: undefined,
this: thisArg,
});
}
return result;
},
construct(target, argList, newTarget) {
const realTarget = FunctionPrototypeCall(nextImpl, ctx);
let result;
let error;
try {
result = ReflectConstruct(realTarget, argList, newTarget);
} catch (err) {
error = err;
throw err;
} finally {
FunctionPrototypeCall(trackCall, ctx, {
__proto__: null,
arguments: argList,
error,
result,
// eslint-disable-next-line no-restricted-syntax
stack: new Error(),
target,
this: result,
});
}
return result;
},
get(target, property, receiver) {
if (property === 'mock') {
return ctx;
}
return ReflectGet(target, property, receiver);
},
});
ArrayPrototypePush(this.#mocks, {
__proto__: null,
ctx,
restore: restoreFn,
});
return mock;
}
}
function setupSharedModuleState() {
if (sharedModuleState === undefined) {
const { mock } = require('test');
const mockExports = new SafeMap();
const { port1, port2 } = new MessageChannel();
const moduleLoader = esmLoader.getOrInitializeCascadedLoader();
moduleLoader.register(
'internal/test_runner/mock/loader',
'node:',
{ __proto__: null, port: port2 },
[port2],
true,
);
sharedModuleState = {
__proto__: null,
loaderPort: port1,
mockExports,
mockMap: new SafeMap(),
moduleLoader,
};
mock._mockExports = mockExports;
Module._load = FunctionPrototypeBind(cjsMockModuleLoad, sharedModuleState);
}
return sharedModuleState;
}
function cjsMockModuleLoad(request, parent, isMain) {
let resolved;
if (isBuiltin(request)) {
resolved = ensureNodeScheme(request);
} else {
resolved = _resolveFilename(request, parent, isMain);
}
const config = this.mockMap.get(resolved);
if (config === undefined) {
return _load(request, parent, isMain);
}
const {
cache,
caller,
defaultExport,
hasDefaultExport,
namedExports,
} = config;
if (cache && Module._cache[resolved]) {
// The CJS cache entry is deleted when the mock is configured. If it has
// been repopulated, return the exports from that entry.
return Module._cache[resolved].exports;
}
// eslint-disable-next-line node-core/set-proto-to-null-in-object
const modExports = hasDefaultExport ? defaultExport : {};
const exportNames = ObjectKeys(namedExports);
if ((typeof modExports !== 'object' || modExports === null) &&
exportNames.length > 0) {
// eslint-disable-next-line no-restricted-syntax
throw new Error(kBadExportsMessage);
}
for (let i = 0; i < exportNames.length; ++i) {
const name = exportNames[i];
const descriptor = ObjectGetOwnPropertyDescriptor(namedExports, name);
ObjectDefineProperty(modExports, name, descriptor);
}
if (cache) {
const entry = new Module(resolved, caller);
entry.exports = modExports;
entry.filename = resolved;
entry.loaded = true;
entry.paths = _nodeModulePaths(entry.path);
Module._cache[resolved] = entry;
}
return modExports;
}
function validateStringOrSymbol(value, name) {
if (typeof value !== 'string' && typeof value !== 'symbol') {
throw new ERR_INVALID_ARG_TYPE(name, ['string', 'symbol'], value);
}
}
function validateTimes(value, name) {
if (value === Infinity) {
return;
}
validateInteger(value, name, 1);
}
function findMethodOnPrototypeChain(instance, methodName) {
let host = instance;
let descriptor;
while (host !== null) {
descriptor = ObjectGetOwnPropertyDescriptor(host, methodName);
if (descriptor) {
break;
}
host = ObjectGetPrototypeOf(host);
}
return descriptor;
}
function waitForAck(buf) {
const result = AtomicsWait(buf, 0, 0, kWaitTimeout);
notStrictEqual(result, 'timed-out', 'test mocking synchronization failed');
strictEqual(buf[0], kMockSuccess);
}
function ensureNodeScheme(specifier) {
if (!StringPrototypeStartsWith(specifier, 'node:')) {
return `node:${specifier}`;
}
return specifier;
}
if (!enableModuleMocking) {
delete MockTracker.prototype.module;
}
module.exports = {
ensureNodeScheme,
kBadExportsMessage,
kMockSearchParam,
kMockSuccess,
kMockExists,
kMockUnknownMessage,
MockTracker,
};

View File

@ -0,0 +1,796 @@
'use strict';
const {
ArrayPrototypeAt,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
DatePrototypeGetTime,
DatePrototypeToString,
FunctionPrototypeApply,
FunctionPrototypeBind,
FunctionPrototypeToString,
NumberIsNaN,
ObjectDefineProperties,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
ObjectGetOwnPropertyDescriptors,
Promise,
Symbol,
SymbolAsyncIterator,
SymbolDispose,
globalThis,
} = primordials;
const {
validateAbortSignal,
validateNumber,
validateStringArray,
} = require('internal/validators');
const {
AbortError,
codes: {
ERR_INVALID_ARG_VALUE,
ERR_INVALID_STATE,
},
} = require('internal/errors');
const { TIMEOUT_MAX } = require('internal/timers');
const PriorityQueue = require('internal/priority_queue');
const nodeTimers = require('timers');
const nodeTimersPromises = require('timers/promises');
const EventEmitter = require('events');
let kResistStopPropagation;
// Internal reference to the MockTimers class inside MockDate
let kMock;
// Initial epoch to which #now should be set to
const kInitialEpoch = 0;
function compareTimersLists(a, b) {
return (a.runAt - b.runAt) || (a.id - b.id);
}
function setPosition(node, pos) {
node.priorityQueuePosition = pos;
}
function abortIt(signal) {
return new AbortError(undefined, { __proto__: null, cause: signal.reason });
}
/**
* @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date', 'scheduler.wait')[]} Supported timers
*/
const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date', 'scheduler.wait'];
const TIMERS_DEFAULT_INTERVAL = {
__proto__: null,
setImmediate: -1,
};
class Timeout {
constructor(opts) {
this.id = opts.id;
this.callback = opts.callback;
this.runAt = opts.runAt;
this.interval = opts.interval;
this.args = opts.args;
}
hasRef() {
return true;
}
ref() {
return this;
}
unref() {
return this;
}
refresh() {
return this;
}
}
class MockTimers {
#realSetTimeout;
#realClearTimeout;
#realSetInterval;
#realClearInterval;
#realSetImmediate;
#realClearImmediate;
#realPromisifiedSetTimeout;
#realPromisifiedSetInterval;
#realTimersPromisifiedSchedulerWait;
#realTimersSetTimeout;
#realTimersClearTimeout;
#realTimersSetInterval;
#realTimersClearInterval;
#realTimersSetImmediate;
#realTimersClearImmediate;
#realPromisifiedSetImmediate;
#nativeDateDescriptor;
#timersInContext = [];
#isEnabled = false;
#currentTimer = 1;
#now = kInitialEpoch;
#executionQueue = new PriorityQueue(compareTimersLists, setPosition);
#setTimeout = FunctionPrototypeBind(this.#createTimer, this, false);
#clearTimeout = FunctionPrototypeBind(this.#clearTimer, this);
#setInterval = FunctionPrototypeBind(this.#createTimer, this, true);
#clearInterval = FunctionPrototypeBind(this.#clearTimer, this);
#clearImmediate = FunctionPrototypeBind(this.#clearTimer, this);
#restoreSetImmediate() {
ObjectDefineProperty(
globalThis,
'setImmediate',
this.#realSetImmediate,
);
ObjectDefineProperty(
globalThis,
'clearImmediate',
this.#realClearImmediate,
);
ObjectDefineProperty(
nodeTimers,
'setImmediate',
this.#realTimersSetImmediate,
);
ObjectDefineProperty(
nodeTimers,
'clearImmediate',
this.#realTimersClearImmediate,
);
ObjectDefineProperty(
nodeTimersPromises,
'setImmediate',
this.#realPromisifiedSetImmediate,
);
}
#restoreOriginalSetInterval() {
ObjectDefineProperty(
globalThis,
'setInterval',
this.#realSetInterval,
);
ObjectDefineProperty(
globalThis,
'clearInterval',
this.#realClearInterval,
);
ObjectDefineProperty(
nodeTimers,
'setInterval',
this.#realTimersSetInterval,
);
ObjectDefineProperty(
nodeTimers,
'clearInterval',
this.#realTimersClearInterval,
);
ObjectDefineProperty(
nodeTimersPromises,
'setInterval',
this.#realPromisifiedSetInterval,
);
}
#restoreOriginalSchedulerWait() {
nodeTimersPromises.scheduler.wait = FunctionPrototypeBind(
this.#realTimersPromisifiedSchedulerWait,
this,
);
}
#restoreOriginalSetTimeout() {
ObjectDefineProperty(
globalThis,
'setTimeout',
this.#realSetTimeout,
);
ObjectDefineProperty(
globalThis,
'clearTimeout',
this.#realClearTimeout,
);
ObjectDefineProperty(
nodeTimers,
'setTimeout',
this.#realTimersSetTimeout,
);
ObjectDefineProperty(
nodeTimers,
'clearTimeout',
this.#realTimersClearTimeout,
);
ObjectDefineProperty(
nodeTimersPromises,
'setTimeout',
this.#realPromisifiedSetTimeout,
);
}
#storeOriginalSetImmediate() {
this.#realSetImmediate = ObjectGetOwnPropertyDescriptor(
globalThis,
'setImmediate',
);
this.#realClearImmediate = ObjectGetOwnPropertyDescriptor(
globalThis,
'clearImmediate',
);
this.#realTimersSetImmediate = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'setImmediate',
);
this.#realTimersClearImmediate = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'clearImmediate',
);
this.#realPromisifiedSetImmediate = ObjectGetOwnPropertyDescriptor(
nodeTimersPromises,
'setImmediate',
);
}
#storeOriginalSetInterval() {
this.#realSetInterval = ObjectGetOwnPropertyDescriptor(
globalThis,
'setInterval',
);
this.#realClearInterval = ObjectGetOwnPropertyDescriptor(
globalThis,
'clearInterval',
);
this.#realTimersSetInterval = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'setInterval',
);
this.#realTimersClearInterval = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'clearInterval',
);
this.#realPromisifiedSetInterval = ObjectGetOwnPropertyDescriptor(
nodeTimersPromises,
'setInterval',
);
}
#storeOriginalSchedulerWait() {
this.#realTimersPromisifiedSchedulerWait = FunctionPrototypeBind(
nodeTimersPromises.scheduler.wait,
this,
);
}
#storeOriginalSetTimeout() {
this.#realSetTimeout = ObjectGetOwnPropertyDescriptor(
globalThis,
'setTimeout',
);
this.#realClearTimeout = ObjectGetOwnPropertyDescriptor(
globalThis,
'clearTimeout',
);
this.#realTimersSetTimeout = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'setTimeout',
);
this.#realTimersClearTimeout = ObjectGetOwnPropertyDescriptor(
nodeTimers,
'clearTimeout',
);
this.#realPromisifiedSetTimeout = ObjectGetOwnPropertyDescriptor(
nodeTimersPromises,
'setTimeout',
);
}
#createTimer(isInterval, callback, delay, ...args) {
if (delay > TIMEOUT_MAX) {
delay = 1;
}
const timerId = this.#currentTimer++;
const opts = {
__proto__: null,
id: timerId,
callback,
runAt: this.#now + delay,
interval: isInterval ? delay : undefined,
args,
};
const timer = new Timeout(opts);
this.#executionQueue.insert(timer);
return timer;
}
#clearTimer(timer) {
if (timer?.priorityQueuePosition !== undefined) {
this.#executionQueue.removeAt(timer.priorityQueuePosition);
timer.priorityQueuePosition = undefined;
}
}
#createDate() {
kMock ??= Symbol('MockTimers');
const NativeDateConstructor = this.#nativeDateDescriptor.value;
if (NativeDateConstructor.isMock) {
throw new ERR_INVALID_STATE('Date is already being mocked!');
}
/**
* Function to mock the Date constructor, treats cases as per ECMA-262
* and returns a Date object with a mocked implementation
* @typedef {Date} MockDate
* @returns {MockDate} a mocked Date object
*/
function MockDate(year, month, date, hours, minutes, seconds, ms) {
const mockTimersSource = MockDate[kMock];
const nativeDate = mockTimersSource.#nativeDateDescriptor.value;
// As of the fake-timers implementation for Sinon
// ref https://github.com/sinonjs/fake-timers/blob/a4c757f80840829e45e0852ea1b17d87a998388e/src/fake-timers-src.js#L456
// This covers the Date constructor called as a function ref.
// ECMA-262 Edition 5.1 section 15.9.2.
// and ECMA-262 Edition 14 Section 21.4.2.1
// replaces 'this instanceof MockDate' with a more reliable check
// from ECMA-262 Edition 14 Section 13.3.12.1 NewTarget
if (!new.target) {
return DatePrototypeToString(new nativeDate(mockTimersSource.#now));
}
// Cases where Date is called as a constructor
// This is intended as a defensive implementation to avoid
// having unexpected returns
switch (arguments.length) {
case 0:
return new nativeDate(MockDate[kMock].#now);
case 1:
return new nativeDate(year);
case 2:
return new nativeDate(year, month);
case 3:
return new nativeDate(year, month, date);
case 4:
return new nativeDate(year, month, date, hours);
case 5:
return new nativeDate(year, month, date, hours, minutes);
case 6:
return new nativeDate(year, month, date, hours, minutes, seconds);
default:
return new nativeDate(year, month, date, hours, minutes, seconds, ms);
}
}
// Prototype is read-only, and non assignable through Object.defineProperties
// eslint-disable-next-line no-unused-vars -- used to get the prototype out of the object
const { prototype, ...dateProps } = ObjectGetOwnPropertyDescriptors(NativeDateConstructor);
// Binds all the properties of Date to the MockDate function
ObjectDefineProperties(
MockDate,
dateProps,
);
MockDate.now = function now() {
return MockDate[kMock].#now;
};
// This is just to print the function { native code } in the console
// when the user prints the function and not the internal code
MockDate.toString = function toString() {
return FunctionPrototypeToString(MockDate[kMock].#nativeDateDescriptor.value);
};
// We need to pollute the prototype of this
ObjectDefineProperties(MockDate, {
__proto__: null,
[kMock]: {
__proto__: null,
enumerable: false,
configurable: false,
writable: false,
value: this,
},
isMock: {
__proto__: null,
enumerable: true,
configurable: false,
writable: false,
value: true,
},
});
MockDate.prototype = NativeDateConstructor.prototype;
MockDate.parse = NativeDateConstructor.parse;
MockDate.UTC = NativeDateConstructor.UTC;
MockDate.prototype.toUTCString = NativeDateConstructor.prototype.toUTCString;
return MockDate;
}
async * #setIntervalPromisified(interval, result, options) {
const context = this;
const emitter = new EventEmitter();
if (options?.signal) {
validateAbortSignal(options.signal, 'options.signal');
if (options.signal.aborted) {
throw abortIt(options.signal);
}
const onAbort = (reason) => {
emitter.emit('data', { __proto__: null, aborted: true, reason });
};
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
options.signal.addEventListener('abort', onAbort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
}
const eventIt = EventEmitter.on(emitter, 'data');
const callback = () => {
emitter.emit('data', result);
};
const timer = this.#createTimer(true, callback, interval, options);
const clearListeners = () => {
emitter.removeAllListeners();
context.#clearTimer(timer);
};
const iterator = {
__proto__: null,
[SymbolAsyncIterator]() {
return this;
},
async next() {
const result = await eventIt.next();
const value = ArrayPrototypeAt(result.value, 0);
if (value?.aborted) {
iterator.return();
throw abortIt(options.signal);
}
return {
__proto__: null,
done: result.done,
value,
};
},
async return() {
clearListeners();
return eventIt.return();
},
};
yield* iterator;
}
#setImmediate(callback, ...args) {
return this.#createTimer(
false,
callback,
TIMERS_DEFAULT_INTERVAL.setImmediate,
...args,
);
}
#promisifyTimer({ timerFn, clearFn, ms, result, options }) {
return new Promise((resolve, reject) => {
if (options?.signal) {
try {
validateAbortSignal(options.signal, 'options.signal');
} catch (err) {
return reject(err);
}
if (options.signal.aborted) {
return reject(abortIt(options.signal));
}
}
const onabort = () => {
clearFn(timer);
return reject(abortIt(options.signal));
};
const timer = timerFn(() => {
return resolve(result);
}, ms);
if (options?.signal) {
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
options.signal.addEventListener('abort', onabort, {
__proto__: null,
once: true,
[kResistStopPropagation]: true,
});
}
});
}
#setImmediatePromisified(result, options) {
return this.#promisifyTimer({
__proto__: null,
timerFn: FunctionPrototypeBind(this.#setImmediate, this),
clearFn: FunctionPrototypeBind(this.#clearImmediate, this),
ms: TIMERS_DEFAULT_INTERVAL.setImmediate,
result,
options,
});
}
#setTimeoutPromisified(ms, result, options) {
return this.#promisifyTimer({
__proto__: null,
timerFn: FunctionPrototypeBind(this.#setTimeout, this),
clearFn: FunctionPrototypeBind(this.#clearTimeout, this),
ms,
result,
options,
});
}
#assertTimersAreEnabled() {
if (!this.#isEnabled) {
throw new ERR_INVALID_STATE(
'You should enable MockTimers first by calling the .enable function',
);
}
}
#assertTimeArg(time) {
if (time < 0) {
throw new ERR_INVALID_ARG_VALUE('time', 'positive integer', time);
}
}
#isValidDateWithGetTime(maybeDate) {
// Validation inspired on https://github.com/inspect-js/is-date-object/blob/main/index.js#L3-L11
try {
DatePrototypeGetTime(maybeDate);
return true;
} catch {
return false;
}
}
#toggleEnableTimers(activate) {
const options = {
__proto__: null,
toFake: {
'__proto__': null,
'scheduler.wait': () => {
this.#storeOriginalSchedulerWait();
nodeTimersPromises.scheduler.wait = (delay, options) =>
this.#setTimeoutPromisified(delay, undefined, options);
},
'setTimeout': () => {
this.#storeOriginalSetTimeout();
globalThis.setTimeout = this.#setTimeout;
globalThis.clearTimeout = this.#clearTimeout;
nodeTimers.setTimeout = this.#setTimeout;
nodeTimers.clearTimeout = this.#clearTimeout;
nodeTimersPromises.setTimeout = FunctionPrototypeBind(
this.#setTimeoutPromisified,
this,
);
},
'setInterval': () => {
this.#storeOriginalSetInterval();
globalThis.setInterval = this.#setInterval;
globalThis.clearInterval = this.#clearInterval;
nodeTimers.setInterval = this.#setInterval;
nodeTimers.clearInterval = this.#clearInterval;
nodeTimersPromises.setInterval = FunctionPrototypeBind(
this.#setIntervalPromisified,
this,
);
},
'setImmediate': () => {
this.#storeOriginalSetImmediate();
// setImmediate functions needs to bind MockTimers
// otherwise it will throw an error when called
// "Receiver must be an instance of MockTimers"
// because #setImmediate is the only function here
// that calls #createTimer and it's not bound to MockTimers
globalThis.setImmediate = FunctionPrototypeBind(
this.#setImmediate,
this,
);
globalThis.clearImmediate = this.#clearImmediate;
nodeTimers.setImmediate = FunctionPrototypeBind(
this.#setImmediate,
this,
);
nodeTimers.clearImmediate = this.#clearImmediate;
nodeTimersPromises.setImmediate = FunctionPrototypeBind(
this.#setImmediatePromisified,
this,
);
},
'Date': () => {
this.#nativeDateDescriptor = ObjectGetOwnPropertyDescriptor(globalThis, 'Date');
globalThis.Date = this.#createDate();
},
},
toReal: {
'__proto__': null,
'scheduler.wait': () => {
this.#restoreOriginalSchedulerWait();
},
'setTimeout': () => {
this.#restoreOriginalSetTimeout();
},
'setInterval': () => {
this.#restoreOriginalSetInterval();
},
'setImmediate': () => {
this.#restoreSetImmediate();
},
'Date': () => {
ObjectDefineProperty(globalThis, 'Date', this.#nativeDateDescriptor);
},
},
};
const target = activate ? options.toFake : options.toReal;
ArrayPrototypeForEach(this.#timersInContext, (timer) => target[timer]());
this.#isEnabled = activate;
}
/**
* Advances the virtual time of MockTimers by the specified duration (in milliseconds).
* This method simulates the passage of time and triggers any scheduled timers that are due.
* @param {number} [time=1] - The amount of time (in milliseconds) to advance the virtual time.
* @throws {ERR_INVALID_STATE} If MockTimers are not enabled.
* @throws {ERR_INVALID_ARG_VALUE} If a negative time value is provided.
*/
tick(time = 1) {
this.#assertTimersAreEnabled();
this.#assertTimeArg(time);
this.#now += time;
let timer = this.#executionQueue.peek();
while (timer) {
if (timer.runAt > this.#now) break;
FunctionPrototypeApply(timer.callback, undefined, timer.args);
// Check if the timeout was cleared by calling clearTimeout inside its own callback
const afterCallback = this.#executionQueue.peek();
if (afterCallback?.id === timer.id) {
this.#executionQueue.shift();
timer.priorityQueuePosition = undefined;
}
if (timer.interval !== undefined) {
timer.runAt += timer.interval;
this.#executionQueue.insert(timer);
}
timer = this.#executionQueue.peek();
}
}
/**
* @typedef {{apis: SUPPORTED_APIS;now: number | Date;}} EnableOptions Options to enable the timers
* @property {SUPPORTED_APIS} apis List of timers to enable, defaults to all
* @property {number | Date} now The epoch to which the timers should be set to, defaults to 0
*/
/**
* Enables the MockTimers replacing the native timers with the fake ones.
* @param {EnableOptions} [options]
*/
enable(options = { __proto__: null, apis: SUPPORTED_APIS, now: 0 }) {
const internalOptions = { __proto__: null, ...options };
if (this.#isEnabled) {
throw new ERR_INVALID_STATE('MockTimers is already enabled!');
}
if (NumberIsNaN(internalOptions.now)) {
throw new ERR_INVALID_ARG_VALUE('now', internalOptions.now, `epoch must be a positive integer received ${internalOptions.now}`);
}
internalOptions.now ||= 0;
internalOptions.apis ||= SUPPORTED_APIS;
validateStringArray(internalOptions.apis, 'options.apis');
// Check that the timers passed are supported
ArrayPrototypeForEach(internalOptions.apis, (timer) => {
if (!ArrayPrototypeIncludes(SUPPORTED_APIS, timer)) {
throw new ERR_INVALID_ARG_VALUE(
'options.apis',
timer,
`option ${timer} is not supported`,
);
}
});
this.#timersInContext = internalOptions.apis;
// Checks if the second argument is the initial time
if (this.#isValidDateWithGetTime(internalOptions.now)) {
this.#now = DatePrototypeGetTime(internalOptions.now);
} else if (validateNumber(internalOptions.now, 'initialTime') === undefined) {
this.#assertTimeArg(internalOptions.now);
this.#now = internalOptions.now;
}
this.#toggleEnableTimers(true);
}
/**
* Sets the current time to the given epoch.
* @param {number} time The epoch to set the current time to.
*/
setTime(time = kInitialEpoch) {
validateNumber(time, 'time');
this.#assertTimeArg(time);
this.#assertTimersAreEnabled();
this.#now = time;
}
/**
* An alias for `this.reset()`, allowing the disposal of the `MockTimers` instance.
*/
[SymbolDispose]() {
this.reset();
}
/**
* Resets MockTimers, disabling any enabled timers and clearing the execution queue.
* Does nothing if MockTimers are not enabled.
*/
reset() {
// Ignore if not enabled
if (!this.#isEnabled) return;
this.#toggleEnableTimers(false);
this.#timersInContext = [];
this.#now = kInitialEpoch;
let timer = this.#executionQueue.peek();
while (timer) {
this.#executionQueue.shift();
timer = this.#executionQueue.peek();
}
}
/**
* Runs all scheduled timers until there are no more pending timers.
* @throws {ERR_INVALID_STATE} If MockTimers are not enabled.
*/
runAll() {
this.#assertTimersAreEnabled();
const longestTimer = this.#executionQueue.peekBottom();
if (!longestTimer) return;
this.tick(longestTimer.runAt - this.#now);
}
}
module.exports = { MockTimers };