Skip to content

Commit 57c3009

Browse files
Renegade334richardlau
authored andcommitted
test_runner: clean up promisified interval generation
* yield from loop instead of setting up custom iterator * cancel abort listener on exit * do not call <Array>.at(0) PR-URL: #58824 Reviewed-By: Chemi Atlow <[email protected]> Reviewed-By: James M Snell <[email protected]> Reviewed-By: Moshe Atlow <[email protected]>
1 parent c923cfe commit 57c3009

File tree

2 files changed

+78
-76
lines changed

2 files changed

+78
-76
lines changed

lib/internal/test_runner/mock/mock_timers.js

Lines changed: 42 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
'use strict';
22

33
const {
4-
ArrayPrototypeAt,
54
ArrayPrototypeForEach,
65
ArrayPrototypeIncludes,
76
DatePrototypeGetTime,
@@ -14,9 +13,8 @@ const {
1413
ObjectDefineProperty,
1514
ObjectGetOwnPropertyDescriptor,
1615
ObjectGetOwnPropertyDescriptors,
17-
Promise,
16+
PromiseWithResolvers,
1817
Symbol,
19-
SymbolAsyncIterator,
2018
globalThis,
2119
} = primordials;
2220

@@ -38,14 +36,15 @@ const {
3836
},
3937
} = require('internal/errors');
4038

39+
const { addAbortListener } = require('internal/events/abort_listener');
40+
4141
const { TIMEOUT_MAX } = require('internal/timers');
4242

4343
const PriorityQueue = require('internal/priority_queue');
4444
const nodeTimers = require('timers');
4545
const nodeTimersPromises = require('timers/promises');
4646
const EventEmitter = require('events');
4747

48-
let kResistStopPropagation;
4948
// Internal reference to the MockTimers class inside MockDate
5049
let kMock;
5150
// Initial epoch to which #now should be set to
@@ -430,62 +429,36 @@ class MockTimers {
430429
}
431430

432431
async * #setIntervalPromisified(interval, result, options) {
433-
const context = this;
434432
const emitter = new EventEmitter();
433+
434+
let abortListener;
435435
if (options?.signal) {
436436
validateAbortSignal(options.signal, 'options.signal');
437437

438438
if (options.signal.aborted) {
439439
throw abortIt(options.signal);
440440
}
441441

442-
const onAbort = (reason) => {
443-
emitter.emit('data', { __proto__: null, aborted: true, reason });
444-
};
445-
446-
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
447-
options.signal.addEventListener('abort', onAbort, {
448-
__proto__: null,
449-
once: true,
450-
[kResistStopPropagation]: true,
442+
abortListener = addAbortListener(options.signal, () => {
443+
emitter.emit('error', abortIt(options.signal));
451444
});
452445
}
453446

454447
const eventIt = EventEmitter.on(emitter, 'data');
455-
const callback = () => {
456-
emitter.emit('data', result);
457-
};
448+
const timer = this.#createTimer(true,
449+
() => emitter.emit('data'),
450+
interval,
451+
options);
458452

459-
const timer = this.#createTimer(true, callback, interval, options);
460-
const clearListeners = () => {
461-
emitter.removeAllListeners();
462-
context.#clearTimer(timer);
463-
};
464-
const iterator = {
465-
__proto__: null,
466-
[SymbolAsyncIterator]() {
467-
return this;
468-
},
469-
async next() {
470-
const result = await eventIt.next();
471-
const value = ArrayPrototypeAt(result.value, 0);
472-
if (value?.aborted) {
473-
iterator.return();
474-
throw abortIt(options.signal);
475-
}
476-
477-
return {
478-
__proto__: null,
479-
done: result.done,
480-
value,
481-
};
482-
},
483-
async return() {
484-
clearListeners();
485-
return eventIt.return();
486-
},
487-
};
488-
yield* iterator;
453+
try {
454+
// eslint-disable-next-line no-unused-vars
455+
for await (const event of eventIt) {
456+
yield result;
457+
}
458+
} finally {
459+
abortListener?.[SymbolDispose]();
460+
this.#clearInterval(timer);
461+
}
489462
}
490463

491464
#setImmediate(callback, ...args) {
@@ -497,38 +470,31 @@ class MockTimers {
497470
);
498471
}
499472

500-
#promisifyTimer({ timerFn, clearFn, ms, result, options }) {
501-
return new Promise((resolve, reject) => {
502-
if (options?.signal) {
503-
try {
504-
validateAbortSignal(options.signal, 'options.signal');
505-
} catch (err) {
506-
return reject(err);
507-
}
473+
async #promisifyTimer({ timerFn, clearFn, ms, result, options }) {
474+
const { promise, resolve, reject } = PromiseWithResolvers();
508475

509-
if (options.signal.aborted) {
510-
return reject(abortIt(options.signal));
511-
}
512-
}
476+
let abortListener;
477+
if (options?.signal) {
478+
validateAbortSignal(options.signal, 'options.signal');
513479

514-
const onabort = () => {
515-
clearFn(timer);
516-
return reject(abortIt(options.signal));
517-
};
518-
519-
const timer = timerFn(() => {
520-
return resolve(result);
521-
}, ms);
522-
523-
if (options?.signal) {
524-
kResistStopPropagation ??= require('internal/event_target').kResistStopPropagation;
525-
options.signal.addEventListener('abort', onabort, {
526-
__proto__: null,
527-
once: true,
528-
[kResistStopPropagation]: true,
529-
});
480+
if (options.signal.aborted) {
481+
throw abortIt(options.signal);
530482
}
531-
});
483+
484+
abortListener = addAbortListener(options.signal, () => {
485+
reject(abortIt(options.signal));
486+
});
487+
}
488+
489+
const timer = timerFn(resolve, ms);
490+
491+
try {
492+
await promise;
493+
return result;
494+
} finally {
495+
abortListener?.[SymbolDispose]();
496+
clearFn(timer);
497+
}
532498
}
533499

534500
#setImmediatePromisified(result, options) {

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ process.env.NODE_TEST_KNOWN_GLOBALS = 0;
44
const common = require('../common');
55

66
const assert = require('node:assert');
7+
const { getEventListeners } = require('node:events');
78
const { it, mock, describe } = require('node:test');
89
const nodeTimers = require('node:timers');
910
const nodeTimersPromises = require('node:timers/promises');
@@ -422,6 +423,8 @@ describe('Mock Timers Test Suite', () => {
422423
});
423424

424425
describe('timers/promises', () => {
426+
const hasAbortListener = (signal) => !!getEventListeners(signal, 'abort').length;
427+
425428
describe('setTimeout Suite', () => {
426429
it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => {
427430
t.mock.timers.enable({ apis: ['setTimeout'] });
@@ -515,6 +518,22 @@ describe('Mock Timers Test Suite', () => {
515518
});
516519
});
517520

521+
it('should clear the abort listener when the timer resolves', async (t) => {
522+
t.mock.timers.enable({ apis: ['setTimeout'] });
523+
const expectedResult = 'result';
524+
const controller = new AbortController();
525+
const p = nodeTimersPromises.setTimeout(500, expectedResult, {
526+
ref: true,
527+
signal: controller.signal,
528+
});
529+
530+
assert(hasAbortListener(controller.signal));
531+
532+
t.mock.timers.tick(500);
533+
await p;
534+
assert(!hasAbortListener(controller.signal));
535+
});
536+
518537
it('should reject given an an invalid signal instance', async (t) => {
519538
t.mock.timers.enable({ apis: ['setTimeout'] });
520539
const expectedResult = 'result';
@@ -728,6 +747,23 @@ describe('Mock Timers Test Suite', () => {
728747
});
729748
});
730749

750+
it('should clear the abort listener when the interval returns', async (t) => {
751+
t.mock.timers.enable({ apis: ['setInterval'] });
752+
753+
const abortController = new AbortController();
754+
const intervalIterator = nodeTimersPromises.setInterval(1, Date.now(), {
755+
signal: abortController.signal,
756+
});
757+
758+
const first = intervalIterator.next();
759+
t.mock.timers.tick();
760+
761+
await first;
762+
assert(hasAbortListener(abortController.signal));
763+
await intervalIterator.return();
764+
assert(!hasAbortListener(abortController.signal));
765+
});
766+
731767
it('should abort operation given an abort controller signal on a real use case', async (t) => {
732768
t.mock.timers.enable({ apis: ['setInterval'] });
733769
const controller = new AbortController();

0 commit comments

Comments
 (0)