node/lib/internal/test_runner/mock/mock_timers.js
Erick Wendel a61bc9d933
test_runner: fix invalid timer call
Signed-off-by: Erick Wendel <erick.workspace@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/49477
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
Reviewed-By: Moshe Atlow <moshe@atlow.co.il>
2023-09-05 21:17:28 +00:00

570 lines
14 KiB
JavaScript

'use strict';
const {
emitExperimentalWarning,
} = require('internal/util');
const {
ArrayPrototypeAt,
ArrayPrototypeForEach,
ArrayPrototypeIncludes,
DateNow,
FunctionPrototypeApply,
FunctionPrototypeBind,
ObjectDefineProperty,
ObjectGetOwnPropertyDescriptor,
Promise,
SymbolAsyncIterator,
SymbolDispose,
globalThis,
} = primordials;
const {
validateAbortSignal,
validateArray,
} = require('internal/validators');
const {
AbortError,
codes: {
ERR_INVALID_STATE,
ERR_INVALID_ARG_VALUE,
},
} = require('internal/errors');
const PriorityQueue = require('internal/priority_queue');
const nodeTimers = require('timers');
const nodeTimersPromises = require('timers/promises');
const EventEmitter = require('events');
let kResistStopPropagation;
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 });
}
const SUPPORTED_TIMERS = ['setTimeout', 'setInterval', 'setImmediate'];
const TIMERS_DEFAULT_INTERVAL = {
__proto__: null,
setImmediate: -1,
};
class MockTimers {
#realSetTimeout;
#realClearTimeout;
#realSetInterval;
#realClearInterval;
#realSetImmediate;
#realClearImmediate;
#realPromisifiedSetTimeout;
#realPromisifiedSetInterval;
#realTimersSetTimeout;
#realTimersClearTimeout;
#realTimersSetInterval;
#realTimersClearInterval;
#realTimersSetImmediate;
#realTimersClearImmediate;
#realPromisifiedSetImmediate;
#timersInContext = [];
#isEnabled = false;
#currentTimer = 1;
#now = DateNow();
#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);
#setImmediate = (callback, ...args) => {
return this.#createTimer(
false,
callback,
TIMERS_DEFAULT_INTERVAL.setImmediate,
...args,
);
};
#clearImmediate = FunctionPrototypeBind(this.#clearTimer, this);
constructor() {
emitExperimentalWarning('The MockTimers API');
}
#createTimer(isInterval, callback, delay, ...args) {
const timerId = this.#currentTimer++;
this.#executionQueue.insert({
__proto__: null,
id: timerId,
callback,
runAt: this.#now + delay,
interval: isInterval,
args,
});
return timerId;
}
#clearTimer(position) {
this.#executionQueue.removeAt(position);
}
async * #setIntervalPromisified(interval, startTime, 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 = () => {
startTime += interval;
emitter.emit('data', startTime);
};
const timerId = this.#createTimer(true, callback, interval, options);
const clearListeners = () => {
emitter.removeAllListeners();
context.#clearTimer(timerId);
};
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;
}
#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(id);
return reject(abortIt(options.signal));
};
const id = 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,
});
}
#toggleEnableTimers(activate) {
const options = {
__proto__: null,
toFake: {
__proto__: null,
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();
globalThis.setImmediate = this.#setImmediate;
globalThis.clearImmediate = this.#clearImmediate;
nodeTimers.setImmediate = this.#setImmediate;
nodeTimers.clearImmediate = this.#clearImmediate;
nodeTimersPromises.setImmediate = FunctionPrototypeBind(
this.#setImmediatePromisified,
this,
);
},
},
toReal: {
__proto__: null,
setTimeout: () => {
this.#restoreOriginalSetTimeout();
},
setInterval: () => {
this.#restoreOriginalSetInterval();
},
setImmediate: () => {
this.#restoreSetImmediate();
},
},
};
const target = activate ? options.toFake : options.toReal;
ArrayPrototypeForEach(this.#timersInContext, (timer) => target[timer]());
this.#isEnabled = activate;
}
#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,
);
}
#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',
);
}
#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',
);
}
/**
* 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) {
if (!this.#isEnabled) {
throw new ERR_INVALID_STATE(
'You should enable MockTimers first by calling the .enable function',
);
}
if (time < 0) {
throw new ERR_INVALID_ARG_VALUE(
'time',
'positive integer',
time,
);
}
this.#now += time;
let timer = this.#executionQueue.peek();
while (timer) {
if (timer.runAt > this.#now) break;
FunctionPrototypeApply(timer.callback, undefined, timer.args);
this.#executionQueue.shift();
if (timer.interval) {
timer.runAt += timer.interval;
this.#executionQueue.insert(timer);
return;
}
timer = this.#executionQueue.peek();
}
}
/**
* Enables MockTimers for the specified timers.
* @param {string[]} timers - An array of timer types to enable, e.g., ['setTimeout', 'setInterval'].
* @throws {ERR_INVALID_STATE} If MockTimers are already enabled.
* @throws {ERR_INVALID_ARG_VALUE} If an unsupported timer type is specified.
*/
enable(timers = SUPPORTED_TIMERS) {
if (this.#isEnabled) {
throw new ERR_INVALID_STATE(
'MockTimers is already enabled!',
);
}
validateArray(timers, 'timers');
// Check that the timers passed are supported
ArrayPrototypeForEach(timers, (timer) => {
if (!ArrayPrototypeIncludes(SUPPORTED_TIMERS, timer)) {
throw new ERR_INVALID_ARG_VALUE(
'timers',
timer,
`option ${timer} is not supported`,
);
}
});
this.#timersInContext = timers;
this.#now = DateNow();
this.#toggleEnableTimers(true);
}
/**
* 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 = [];
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() {
if (!this.#isEnabled) {
throw new ERR_INVALID_STATE(
'You should enable MockTimers first by calling the .enable function',
);
}
this.tick(Infinity);
}
}
module.exports = { MockTimers };