From 231f650421176b5c722578d94b8568c054cf87e0 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Sat, 12 Oct 2024 14:52:28 -0300 Subject: [PATCH] test_runner: add support for scheduler.wait on mock timers This adds support for nodetimers.promises.scheduler.wait on Mocktimers Refs: https://github.com/nodejs/node/pull/55244 --- lib/internal/test_runner/mock/mock_timers.js | 49 ++++++-- test/parallel/test-runner-mock-timers.js | 120 +++++++++++++++++++ 2 files changed, 157 insertions(+), 12 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index fa91c9759ecc15..a6f6b69cd9659f 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -62,9 +62,9 @@ function abortIt(signal) { } /** - * @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date')[]} Supported timers + * @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date', 'scheduler.wait')[]} Supported timers */ -const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date']; +const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date', 'scheduler.wait']; const TIMERS_DEFAULT_INTERVAL = { __proto__: null, setImmediate: -1, @@ -106,6 +106,7 @@ class MockTimers { #realPromisifiedSetTimeout; #realPromisifiedSetInterval; + #realTimersPromisifiedSchedulerWait; #realTimersSetTimeout; #realTimersClearTimeout; @@ -190,6 +191,13 @@ class MockTimers { ); } + #restoreOriginalSchedulerWait() { + nodeTimersPromises.scheduler.wait = FunctionPrototypeBind( + this.#realTimersPromisifiedSchedulerWait, + this, + ); + } + #restoreOriginalSetTimeout() { ObjectDefineProperty( globalThis, @@ -264,6 +272,14 @@ class MockTimers { ); } + #storeOriginalSchedulerWait() { + + this.#realTimersPromisifiedSchedulerWait = FunctionPrototypeBind( + nodeTimersPromises.scheduler.wait, + this, + ); + } + #storeOriginalSetTimeout() { this.#realSetTimeout = ObjectGetOwnPropertyDescriptor( globalThis, @@ -556,8 +572,14 @@ class MockTimers { const options = { __proto__: null, toFake: { - __proto__: null, - setTimeout: () => { + '__proto__': null, + 'scheduler.wait': () => { + this.#storeOriginalSchedulerWait(); + + nodeTimersPromises.scheduler.wait = (delay, options) => + this.#setTimeoutPromisified(delay, undefined, options); + }, + 'setTimeout': () => { this.#storeOriginalSetTimeout(); globalThis.setTimeout = this.#setTimeout; @@ -571,7 +593,7 @@ class MockTimers { this, ); }, - setInterval: () => { + 'setInterval': () => { this.#storeOriginalSetInterval(); globalThis.setInterval = this.#setInterval; @@ -585,7 +607,7 @@ class MockTimers { this, ); }, - setImmediate: () => { + 'setImmediate': () => { this.#storeOriginalSetImmediate(); // setImmediate functions needs to bind MockTimers @@ -609,23 +631,26 @@ class MockTimers { this, ); }, - Date: () => { + 'Date': () => { this.#nativeDateDescriptor = ObjectGetOwnPropertyDescriptor(globalThis, 'Date'); globalThis.Date = this.#createDate(); }, }, toReal: { - __proto__: null, - setTimeout: () => { + '__proto__': null, + 'scheduler.wait': () => { + this.#restoreOriginalSchedulerWait(); + }, + 'setTimeout': () => { this.#restoreOriginalSetTimeout(); }, - setInterval: () => { + 'setInterval': () => { this.#restoreOriginalSetInterval(); }, - setImmediate: () => { + 'setImmediate': () => { this.#restoreSetImmediate(); }, - Date: () => { + 'Date': () => { ObjectDefineProperty(globalThis, 'Date', this.#nativeDateDescriptor); }, }, diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index e2a86a5263636a..d569ca52ccf1cb 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -775,6 +775,126 @@ describe('Mock Timers Test Suite', () => { }); }); + describe('scheduler Suite', () => { + describe('scheduler.wait', () => { + it('should advance in time and trigger timers when calling the .tick function', (t) => { + t.mock.timers.enable({ apis: ['scheduler.wait'] }); + + const now = Date.now(); + const durationAtMost = 100; + + const p = nodeTimersPromises.scheduler.wait(4000); + t.mock.timers.tick(4000); + + return p.then(common.mustCall((result) => { + assert.strictEqual(result, undefined); + assert.ok( + Date.now() - now < durationAtMost, + `time should be advanced less than the ${durationAtMost}ms` + ); + })); + }); + + it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => { + t.mock.timers.enable({ apis: ['scheduler.wait'] }); + + const fn = t.mock.fn(); + + nodeTimersPromises.scheduler.wait(9999).then(fn); + + t.mock.timers.tick(8999); + assert.strictEqual(fn.mock.callCount(), 0); + t.mock.timers.tick(500); + + await nodeTimersPromises.setImmediate(); + + assert.strictEqual(fn.mock.callCount(), 0); + t.mock.timers.tick(500); + + await nodeTimersPromises.setImmediate(); + assert.strictEqual(fn.mock.callCount(), 1); + }); + + it('should work with the same params as the original timers/promises/scheduler.wait', async (t) => { + t.mock.timers.enable({ apis: ['scheduler.wait'] }); + const controller = new AbortController(); + const p = nodeTimersPromises.scheduler.wait(2000, { + ref: true, + signal: controller.signal, + }); + + t.mock.timers.tick(1000); + t.mock.timers.tick(500); + t.mock.timers.tick(500); + t.mock.timers.tick(500); + + const result = await p; + assert.strictEqual(result, undefined); + }); + + it('should abort operation if timers/promises/scheduler.wait received an aborted signal', async (t) => { + t.mock.timers.enable({ apis: ['scheduler.wait'] }); + const controller = new AbortController(); + const p = nodeTimersPromises.scheduler.wait(2000, { + ref: true, + signal: controller.signal, + }); + + t.mock.timers.tick(1000); + controller.abort(); + t.mock.timers.tick(500); + t.mock.timers.tick(500); + t.mock.timers.tick(500); + + await assert.rejects(() => p, { + name: 'AbortError', + }); + }); + it('should abort operation even if the .tick was not called', async (t) => { + t.mock.timers.enable({ apis: ['scheduler.wait'] }); + const controller = new AbortController(); + const p = nodeTimersPromises.scheduler.wait(2000, { + ref: true, + signal: controller.signal, + }); + + controller.abort(); + + await assert.rejects(() => p, { + name: 'AbortError', + }); + }); + + it('should abort operation when .abort is called before calling setInterval', async (t) => { + t.mock.timers.enable({ apis: ['scheduler.wait'] }); + const controller = new AbortController(); + controller.abort(); + const p = nodeTimersPromises.scheduler.wait(2000, { + ref: true, + signal: controller.signal, + }); + + await assert.rejects(() => p, { + name: 'AbortError', + }); + }); + + it('should reject given an an invalid signal instance', async (t) => { + t.mock.timers.enable({ apis: ['scheduler.wait'] }); + const p = nodeTimersPromises.scheduler.wait(2000, { + ref: true, + signal: {}, + }); + + await assert.rejects(() => p, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + + }); + }); + describe('Date Suite', () => { it('should return the initial UNIX epoch if not specified', (t) => { t.mock.timers.enable({ apis: ['Date'] });