Skip to content

Commit c240668

Browse files
committed
initial work
Signed-off-by: Erick Wendel <[email protected]>
1 parent cebf21d commit c240668

File tree

2 files changed

+223
-2
lines changed

2 files changed

+223
-2
lines changed

lib/internal/test_runner/mock/mock_timers.js

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,15 @@ function abortIt(signal) {
6262
}
6363

6464
/**
65-
* @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date')[]} Supported timers
65+
* @enum {('setTimeout'|'setInterval'|'setImmediate'|'Date'|'scheduler.wait')[]} Supported timers
6666
*/
67-
const SUPPORTED_APIS = ['setTimeout', 'setInterval', 'setImmediate', 'Date'];
67+
const SUPPORTED_APIS = [
68+
'setTimeout',
69+
'setInterval',
70+
'setImmediate',
71+
'Date',
72+
'scheduler.wait'
73+
];
6874
const TIMERS_DEFAULT_INTERVAL = {
6975
__proto__: null,
7076
setImmediate: -1,
@@ -115,6 +121,8 @@ class MockTimers {
115121
#realTimersClearImmediate;
116122
#realPromisifiedSetImmediate;
117123

124+
#realPromisifiedSchedulerWait;
125+
118126
#nativeDateDescriptor;
119127

120128
#timersInContext = [];
@@ -130,6 +138,7 @@ class MockTimers {
130138
#clearInterval = FunctionPrototypeBind(this.#clearTimer, this);
131139
#clearImmediate = FunctionPrototypeBind(this.#clearTimer, this);
132140

141+
133142
constructor() {
134143
emitExperimentalWarning('The MockTimers API');
135144
}
@@ -218,6 +227,15 @@ class MockTimers {
218227
);
219228
}
220229

230+
#restoreOriginalSchedulerWait() {
231+
232+
ObjectDefineProperty(
233+
nodeTimersPromises.scheduler,
234+
'wait',
235+
this.#realPromisifiedSchedulerWait,
236+
);
237+
}
238+
221239
#storeOriginalSetImmediate() {
222240
this.#realSetImmediate = ObjectGetOwnPropertyDescriptor(
223241
globalThis,
@@ -287,6 +305,13 @@ class MockTimers {
287305
);
288306
}
289307

308+
#storeOriginalSchedulerWait() {
309+
this.#realPromisifiedSchedulerWait = ObjectGetOwnPropertyDescriptor(
310+
nodeTimersPromises.scheduler,
311+
'wait',
312+
);
313+
}
314+
290315
#createTimer(isInterval, callback, delay, ...args) {
291316
const timerId = this.#currentTimer++;
292317
const opts = {
@@ -472,6 +497,13 @@ class MockTimers {
472497
);
473498
}
474499

500+
#schedulerWait(delay, options) {
501+
// Calling timersPromises.scheduler.wait(delay, options)
502+
// is equivalent to calling
503+
// timersPromises.setTimeout(delay, undefined, options).
504+
return this.#setTimeoutPromisified(delay, undefined, options)
505+
}
506+
475507
#promisifyTimer({ timerFn, clearFn, ms, result, options }) {
476508
return new Promise((resolve, reject) => {
477509
if (options?.signal) {
@@ -613,6 +645,13 @@ class MockTimers {
613645
this.#nativeDateDescriptor = ObjectGetOwnPropertyDescriptor(globalThis, 'Date');
614646
globalThis.Date = this.#createDate();
615647
},
648+
'scheduler.wait': () => {
649+
this.#storeOriginalSchedulerWait();
650+
nodeTimersPromises.scheduler.wait = FunctionPrototypeBind(
651+
this.#schedulerWait,
652+
this
653+
)
654+
},
616655
},
617656
toReal: {
618657
__proto__: null,
@@ -628,6 +667,9 @@ class MockTimers {
628667
Date: () => {
629668
ObjectDefineProperty(globalThis, 'Date', this.#nativeDateDescriptor);
630669
},
670+
'scheduler.wait': () => {
671+
this.#restoreOriginalSchedulerWait()
672+
}
631673
},
632674
};
633675

test/parallel/test-runner-mock-timers.js

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,185 @@ describe('Mock Timers Test Suite', () => {
594594
});
595595
});
596596

597+
describe('scheduler.wait Suite', () => {
598+
it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => {
599+
t.mock.timers.enable({ apis: ['scheduler.wait'] });
600+
const p = nodeTimersPromises.scheduler.wait(2000);
601+
602+
t.mock.timers.tick(1000);
603+
t.mock.timers.tick(500);
604+
t.mock.timers.tick(500);
605+
t.mock.timers.tick(500);
606+
607+
p.then(common.mustCall((result) => {
608+
assert.strictEqual(result, undefined);
609+
}));
610+
});
611+
612+
it('should work with the same params as the original timers/promises/scheduler/wait', async (t) => {
613+
t.mock.timers.enable({ apis: ['scheduler.wait'] });
614+
const expectedResult = 'result';
615+
const controller = new AbortController();
616+
const p = nodeTimersPromises.scheduler.wait(2000, expectedResult, {
617+
ref: true,
618+
signal: controller.signal,
619+
});
620+
621+
t.mock.timers.tick(1000);
622+
t.mock.timers.tick(500);
623+
t.mock.timers.tick(500);
624+
t.mock.timers.tick(500);
625+
626+
const result = await p;
627+
assert.strictEqual(result, expectedResult);
628+
});
629+
630+
it('should abort operation if timers/promises/scheduler.wait received an aborted signal', async (t) => {
631+
t.mock.timers.enable({ apis: ['scheduler.wait'] });
632+
const expectedResult = 'result';
633+
const controller = new AbortController();
634+
const p = nodeTimersPromises.scheduler.wait(2000, {
635+
ref: true,
636+
signal: controller.signal,
637+
});
638+
639+
t.mock.timers.tick(1000);
640+
controller.abort();
641+
t.mock.timers.tick(500);
642+
t.mock.timers.tick(500);
643+
t.mock.timers.tick(500);
644+
await assert.rejects(() => p, {
645+
name: 'AbortError',
646+
});
647+
});
648+
649+
650+
it('should abort operation even if the .tick was not called', async (t) => {
651+
t.mock.timers.enable({ apis: ['scheduler.wait'] });
652+
const controller = new AbortController();
653+
const p = nodeTimersPromises.scheduler.wait(2000, {
654+
ref: true,
655+
signal: controller.signal,
656+
});
657+
658+
controller.abort();
659+
660+
await assert.rejects(() => p, {
661+
name: 'AbortError',
662+
});
663+
});
664+
665+
it('should abort operation when .abort is called before calling setInterval', async (t) => {
666+
t.mock.timers.enable({ apis: ['setTimeout'] });
667+
const expectedResult = 'result';
668+
const controller = new AbortController();
669+
controller.abort();
670+
const p = nodeTimersPromises.setTimeout(2000, expectedResult, {
671+
ref: true,
672+
signal: controller.signal,
673+
});
674+
675+
await assert.rejects(() => p, {
676+
name: 'AbortError',
677+
});
678+
});
679+
680+
it('should reject given an an invalid signal instance', async (t) => {
681+
t.mock.timers.enable({ apis: ['scheduler.wait'] });
682+
const p = nodeTimersPromises.scheduler.wait(2000, {
683+
ref: true,
684+
signal: {},
685+
});
686+
687+
await assert.rejects(() => p, {
688+
name: 'TypeError',
689+
code: 'ERR_INVALID_ARG_TYPE',
690+
});
691+
});
692+
693+
// Test for https://github.com/nodejs/node/issues/50365
694+
it('should not affect other timers when aborting', async (t) => {
695+
const f1 = t.mock.fn();
696+
const f2 = t.mock.fn();
697+
t.mock.timers.enable({ apis: ['scheduler.wait'] });
698+
const ac = new AbortController();
699+
700+
// id 1 & pos 1 in priority queue
701+
nodeTimersPromises.scheduler.wait(100, { signal: ac.signal }).then(f1, f1);
702+
// id 2 & pos 1 in priority queue (id 1 is moved to pos 2)
703+
nodeTimersPromises.scheduler.wait(50).then(f2, f2);
704+
705+
ac.abort(); // BUG: will remove timer at pos 1 not timer with id 1!
706+
707+
t.mock.timers.runAll();
708+
await nodeTimersPromises.setImmediate(); // let promises settle
709+
710+
// First scheduler.wait is aborted
711+
assert.strictEqual(f1.mock.callCount(), 1);
712+
assert.strictEqual(f1.mock.calls[0].arguments[0].code, 'ABORT_ERR');
713+
714+
// Second scheduler.wait should resolve, but never settles, because it was eronously removed by ac.abort()
715+
assert.strictEqual(f2.mock.callCount(), 1);
716+
});
717+
718+
// Test for https://github.com/nodejs/node/issues/50365
719+
// it('should not affect other timers when aborted after triggering', async (t) => {
720+
// const f1 = t.mock.fn();
721+
// const f2 = t.mock.fn();
722+
// t.mock.timers.enable({ apis: ['setTimeout'] });
723+
// const ac = new AbortController();
724+
725+
// // id 1 & pos 1 in priority queue
726+
// nodeTimersPromises.setTimeout(50, true, { signal: ac.signal }).then(f1, f1);
727+
// // id 2 & pos 2 in priority queue
728+
// nodeTimersPromises.setTimeout(100).then(f2, f2);
729+
730+
// // First setTimeout resolves
731+
// t.mock.timers.tick(50);
732+
// await nodeTimersPromises.setImmediate(); // let promises settle
733+
// assert.strictEqual(f1.mock.callCount(), 1);
734+
// assert.strictEqual(f1.mock.calls[0].arguments.length, 1);
735+
// assert.strictEqual(f1.mock.calls[0].arguments[0], true);
736+
737+
// // Now timer with id 2 will be at pos 1 in priority queue
738+
// ac.abort(); // BUG: will remove timer at pos 1 not timer with id 1!
739+
740+
// // Second setTimeout should resolve, but never settles, because it was eronously removed by ac.abort()
741+
// t.mock.timers.runAll();
742+
// await nodeTimersPromises.setImmediate(); // let promises settle
743+
// assert.strictEqual(f2.mock.callCount(), 1);
744+
// });
745+
746+
// it('should not affect other timers when clearing timeout inside own callback', (t) => {
747+
// t.mock.timers.enable({ apis: ['setTimeout'] });
748+
// const f = t.mock.fn();
749+
750+
// const timer = nodeTimers.setTimeout(() => {
751+
// f();
752+
// // Clearing the already-expired timeout should do nothing
753+
// nodeTimers.clearTimeout(timer);
754+
// }, 50);
755+
// nodeTimers.setTimeout(f, 50);
756+
// nodeTimers.setTimeout(f, 50);
757+
758+
// t.mock.timers.runAll();
759+
// assert.strictEqual(f.mock.callCount(), 3);
760+
// });
761+
762+
// it('should allow clearing timeout inside own callback', (t) => {
763+
// t.mock.timers.enable({ apis: ['setTimeout'] });
764+
// const f = t.mock.fn();
765+
766+
// const timer = nodeTimers.setTimeout(() => {
767+
// f();
768+
// nodeTimers.clearTimeout(timer);
769+
// }, 50);
770+
771+
// t.mock.timers.runAll();
772+
// assert.strictEqual(f.mock.callCount(), 1);
773+
// });
774+
});
775+
597776
describe('setInterval Suite', () => {
598777
it('should tick three times using fake setInterval', async (t) => {
599778
t.mock.timers.enable({ apis: ['setInterval'] });

0 commit comments

Comments
 (0)