From 3bcfac2c9a256e4f2760821b44cf0338aeefeb89 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 15 Oct 2024 17:36:09 -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 PR-URL: https://github.com/nodejs/node/pull/55244 Reviewed-By: Benjamin Gruenbaum Reviewed-By: Chemi Atlow Reviewed-By: James M Snell Reviewed-By: Rafael Gonzaga --- 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 ed3108f5539b09..935a419d83d0e4 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -64,9 +64,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, @@ -108,6 +108,7 @@ class MockTimers { #realPromisifiedSetTimeout; #realPromisifiedSetInterval; + #realTimersPromisifiedSchedulerWait; #realTimersSetTimeout; #realTimersClearTimeout; @@ -192,6 +193,13 @@ class MockTimers { ); } + #restoreOriginalSchedulerWait() { + nodeTimersPromises.scheduler.wait = FunctionPrototypeBind( + this.#realTimersPromisifiedSchedulerWait, + this, + ); + } + #restoreOriginalSetTimeout() { ObjectDefineProperty( globalThis, @@ -266,6 +274,14 @@ class MockTimers { ); } + #storeOriginalSchedulerWait() { + + this.#realTimersPromisifiedSchedulerWait = FunctionPrototypeBind( + nodeTimersPromises.scheduler.wait, + this, + ); + } + #storeOriginalSetTimeout() { this.#realSetTimeout = ObjectGetOwnPropertyDescriptor( globalThis, @@ -562,8 +578,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; @@ -577,7 +599,7 @@ class MockTimers { this, ); }, - setInterval: () => { + 'setInterval': () => { this.#storeOriginalSetInterval(); globalThis.setInterval = this.#setInterval; @@ -591,7 +613,7 @@ class MockTimers { this, ); }, - setImmediate: () => { + 'setImmediate': () => { this.#storeOriginalSetImmediate(); // setImmediate functions needs to bind MockTimers @@ -615,23 +637,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 9e1bc7e62cc5b2..e438b2636b832a 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -791,6 +791,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'] });