From 69ee56aacd68a7e0c1940894d834cb5bd9450919 Mon Sep 17 00:00:00 2001
From: Erick Wendel <erick.workspace@gmail.com>
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 <benjamingr@gmail.com>
Reviewed-By: Chemi Atlow <chemi@atlow.co.il>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Rafael Gonzaga <rafael.nunu@hotmail.com>
---
 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'] });