From 568ad96726504e5bd9bf7f5cf53fa342ca2637c3 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Fri, 28 Apr 2023 19:12:43 -0300 Subject: [PATCH 01/57] test_runner: add initial draft for fakeTimers Signed-off-by: Erick Wendel --- lib/internal/test_runner/fake_timers.js | 110 +++++++++++++++++++++++ lib/internal/test_runner/test.js | 6 ++ lib/test.js | 17 ++++ test/parallel/test-runner-fake-timers.js | 46 ++++++++++ 4 files changed, 179 insertions(+) create mode 100644 lib/internal/test_runner/fake_timers.js create mode 100644 test/parallel/test-runner-fake-timers.js diff --git a/lib/internal/test_runner/fake_timers.js b/lib/internal/test_runner/fake_timers.js new file mode 100644 index 00000000000000..2d7cf32e5b06d3 --- /dev/null +++ b/lib/internal/test_runner/fake_timers.js @@ -0,0 +1,110 @@ +'use strict'; + +const { + DateNow, + SafeMap, + Symbol, + globalThis, +} = primordials; + +class Timers { + constructor() { + this.timers = new SafeMap(); + + this.setTimeout = this.#createTimer.bind(this, false); + this.clearTimeout = this.#clearTimer.bind(this); + this.setInterval = this.#createTimer.bind(this, true); + this.clearInterval = this.#clearTimer.bind(this); + } + + #createTimer(isInterval, callback, delay, ...args) { + const timerId = Symbol('kTimerId'); + const timer = { + id: timerId, + callback, + time: DateNow() + delay, + interval: isInterval, + args, + }; + this.timers.set(timerId, timer); + return timerId; + } + + #clearTimer(timerId) { + this.timers.delete(timerId); + } + +} + +let realSetTimeout; +let realClearTimeout; +let realSetInterval; +let realClearInterval; + +class FakeTimers { + constructor() { + this.fakeTimers = {}; + this.isEnabled = false; + this.now = DateNow(); + } + + tick(time = 0) { + + // if (!this.isEnabled) { + // throw new Error('you should enable fakeTimers first by calling the .enable function'); + // } + + this.now += time; + const timers = this.fakeTimers.timers; + + for (const timer of timers.values()) { + + if (!(this.now >= timer.time)) continue; + + timer.callback(...timer.args); + if (timer.interval) { + timer.time = this.now + (timer.time - this.now) % timer.args[0]; + continue; + } + + timers.delete(timer.id); + } + } + + enable() { + // if (this.isEnabled) { + // throw new Error('fakeTimers is already enabled!'); + // } + this.now = DateNow(); + this.isEnabled = true; + this.fakeTimers = new Timers(); + + realSetTimeout = globalThis.setTimeout; + realClearTimeout = globalThis.clearTimeout; + realSetInterval = globalThis.setInterval; + realClearInterval = globalThis.clearInterval; + + globalThis.setTimeout = this.fakeTimers.setTimeout; + globalThis.clearTimeout = this.fakeTimers.clearTimeout; + globalThis.setInterval = this.fakeTimers.setInterval; + globalThis.clearInterval = this.fakeTimers.clearInterval; + + } + + reset() { + this.isEnabled = false; + this.fakeTimers = {}; + + // Restore the real timer functions + globalThis.setTimeout = realSetTimeout; + globalThis.clearTimeout = realClearTimeout; + globalThis.setInterval = realSetInterval; + globalThis.clearInterval = realClearInterval; + } + + releaseAllTimers() { + + } +} + +module.exports = { FakeTimers }; diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 1f888bc6277e27..52c77b94d92279 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -31,6 +31,7 @@ const { AbortError, } = require('internal/errors'); const { MockTracker } = require('internal/test_runner/mock'); +const { FakeTimers } = require('internal/test_runner/fake_timers'); const { TestsStream } = require('internal/test_runner/tests_stream'); const { createDeferredCallback, @@ -108,6 +109,11 @@ class TestContext { return this.#test.mock; } + get fakeTimers() { + this.#test.fakeTimers ??= new FakeTimers(); + return this.#test.fakeTimers; + } + runOnly(value) { this.#test.runOnlySubtests = !!value; } diff --git a/lib/test.js b/lib/test.js index dc4045622a8284..faa743241da6f7 100644 --- a/lib/test.js +++ b/lib/test.js @@ -31,3 +31,20 @@ ObjectDefineProperty(module.exports, 'mock', { return lazyMock; }, }); + +let lazyFakeTimers; + +ObjectDefineProperty(module.exports, 'fakeTimers', { + __proto__: null, + configurable: true, + enumerable: true, + get() { + if (lazyFakeTimers === undefined) { + const { FakeTimers } = require('internal/test_runner/fake_timers'); + + lazyFakeTimers = new FakeTimers(); + } + + return lazyFakeTimers; + }, +}); diff --git a/test/parallel/test-runner-fake-timers.js b/test/parallel/test-runner-fake-timers.js new file mode 100644 index 00000000000000..f15f83f98197d6 --- /dev/null +++ b/test/parallel/test-runner-fake-timers.js @@ -0,0 +1,46 @@ +'use strict'; +const common = require('../common'); +process.env.NODE_TEST_KNOWN_GLOBALS = 0; + +const assert = require('node:assert'); +const { fakeTimers, it, mock, afterEach, describe } = require('node:test'); +describe('Faketimers Test Suite', () => { + + describe('setTimeout Suite', () => { + afterEach(() => fakeTimers.reset()); + + it('should advance in time and trigger timers when calling the .tick function', (t) => { + fakeTimers.enable(); + + const fn = mock.fn(() => {}); + + global.setTimeout(fn, 4000); + + fakeTimers.tick(4000); + assert.ok(fn.mock.callCount()); + }); + + it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { + fakeTimers.enable(); + const fn = mock.fn(); + + global.setTimeout(fn, 2000); + + fakeTimers.tick(1000); + fakeTimers.tick(1000); + + assert.strictEqual(fn.mock.callCount(), 1); + }); + + it('should keep setTimeout working if fakeTimers are disabled', (t, done) => { + const now = Date.now(); + const timeout = 2; + const expected = () => now - timeout; + global.setTimeout(common.mustCall(() => { + assert.strictEqual(now - timeout, expected()); + done(); + }), timeout); + }); + + }); +}); From ac0fab3215e724f5561721e232bb088b437a0982 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 1 May 2023 13:53:18 -0300 Subject: [PATCH 02/57] test_runner: move fakerTimers to mock module Signed-off-by: Erick Wendel --- .../test_runner/{ => mock}/fake_timers.js | 0 lib/internal/test_runner/{ => mock}/mock.js | 6 ++++++ lib/internal/test_runner/test.js | 8 +------- lib/test.js | 19 +------------------ ....js => test-runner-mocking-fake-timers.js} | 4 +++- 5 files changed, 11 insertions(+), 26 deletions(-) rename lib/internal/test_runner/{ => mock}/fake_timers.js (100%) rename lib/internal/test_runner/{ => mock}/mock.js (97%) rename test/parallel/{test-runner-fake-timers.js => test-runner-mocking-fake-timers.js} (92%) diff --git a/lib/internal/test_runner/fake_timers.js b/lib/internal/test_runner/mock/fake_timers.js similarity index 100% rename from lib/internal/test_runner/fake_timers.js rename to lib/internal/test_runner/mock/fake_timers.js diff --git a/lib/internal/test_runner/mock.js b/lib/internal/test_runner/mock/mock.js similarity index 97% rename from lib/internal/test_runner/mock.js rename to lib/internal/test_runner/mock/mock.js index aa3014a7bc40c6..7268fd6d89ca7d 100644 --- a/lib/internal/test_runner/mock.js +++ b/lib/internal/test_runner/mock/mock.js @@ -26,6 +26,7 @@ const { validateInteger, validateObject, } = require('internal/validators'); +const { FakeTimers } = require('internal/test_runner/mock/fake_timers'); function kDefaultFunction() {} @@ -35,6 +36,7 @@ class MockFunctionContext { #implementation; #restore; #times; + #fakeTimers; constructor(implementation, restore, times) { this.#calls = []; @@ -48,6 +50,10 @@ class MockFunctionContext { return ArrayPrototypeSlice(this.#calls, 0); } + get fakeTimers() { + this.#fakeTimers ??= new FakeTimers(); + return this.#fakeTimers; + } callCount() { return this.#calls.length; } diff --git a/lib/internal/test_runner/test.js b/lib/internal/test_runner/test.js index 52c77b94d92279..6096a0e4f11385 100644 --- a/lib/internal/test_runner/test.js +++ b/lib/internal/test_runner/test.js @@ -30,8 +30,7 @@ const { }, AbortError, } = require('internal/errors'); -const { MockTracker } = require('internal/test_runner/mock'); -const { FakeTimers } = require('internal/test_runner/fake_timers'); +const { MockTracker } = require('internal/test_runner/mock/mock'); const { TestsStream } = require('internal/test_runner/tests_stream'); const { createDeferredCallback, @@ -109,11 +108,6 @@ class TestContext { return this.#test.mock; } - get fakeTimers() { - this.#test.fakeTimers ??= new FakeTimers(); - return this.#test.fakeTimers; - } - runOnly(value) { this.#test.runOnlySubtests = !!value; } diff --git a/lib/test.js b/lib/test.js index faa743241da6f7..d096cae23d5357 100644 --- a/lib/test.js +++ b/lib/test.js @@ -23,7 +23,7 @@ ObjectDefineProperty(module.exports, 'mock', { enumerable: true, get() { if (lazyMock === undefined) { - const { MockTracker } = require('internal/test_runner/mock'); + const { MockTracker } = require('internal/test_runner/mock/mock'); lazyMock = new MockTracker(); } @@ -31,20 +31,3 @@ ObjectDefineProperty(module.exports, 'mock', { return lazyMock; }, }); - -let lazyFakeTimers; - -ObjectDefineProperty(module.exports, 'fakeTimers', { - __proto__: null, - configurable: true, - enumerable: true, - get() { - if (lazyFakeTimers === undefined) { - const { FakeTimers } = require('internal/test_runner/fake_timers'); - - lazyFakeTimers = new FakeTimers(); - } - - return lazyFakeTimers; - }, -}); diff --git a/test/parallel/test-runner-fake-timers.js b/test/parallel/test-runner-mocking-fake-timers.js similarity index 92% rename from test/parallel/test-runner-fake-timers.js rename to test/parallel/test-runner-mocking-fake-timers.js index f15f83f98197d6..b834e174d5777c 100644 --- a/test/parallel/test-runner-fake-timers.js +++ b/test/parallel/test-runner-mocking-fake-timers.js @@ -3,7 +3,9 @@ const common = require('../common'); process.env.NODE_TEST_KNOWN_GLOBALS = 0; const assert = require('node:assert'); -const { fakeTimers, it, mock, afterEach, describe } = require('node:test'); +const { it, mock, afterEach, describe } = require('node:test'); +const { fakeTimers } = mock; + describe('Faketimers Test Suite', () => { describe('setTimeout Suite', () => { From e54a162ef02b85188bb22aee3e5f46a82f536721 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 1 May 2023 14:10:29 -0300 Subject: [PATCH 03/57] test_runner: change fakeTimers prop name Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/fake_timers.js | 6 +++--- lib/internal/test_runner/mock/mock.js | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/internal/test_runner/mock/fake_timers.js b/lib/internal/test_runner/mock/fake_timers.js index 2d7cf32e5b06d3..01b3cd38a12ef4 100644 --- a/lib/internal/test_runner/mock/fake_timers.js +++ b/lib/internal/test_runner/mock/fake_timers.js @@ -22,7 +22,7 @@ class Timers { const timer = { id: timerId, callback, - time: DateNow() + delay, + runAt: DateNow() + delay, interval: isInterval, args, }; @@ -59,11 +59,11 @@ class FakeTimers { for (const timer of timers.values()) { - if (!(this.now >= timer.time)) continue; + if (!(this.now >= timer.runAt)) continue; timer.callback(...timer.args); if (timer.interval) { - timer.time = this.now + (timer.time - this.now) % timer.args[0]; + timer.runAt = this.now + (timer.runAt - this.now) % timer.args[0]; continue; } diff --git a/lib/internal/test_runner/mock/mock.js b/lib/internal/test_runner/mock/mock.js index 7268fd6d89ca7d..63c59a129ebeba 100644 --- a/lib/internal/test_runner/mock/mock.js +++ b/lib/internal/test_runner/mock/mock.js @@ -50,10 +50,6 @@ class MockFunctionContext { return ArrayPrototypeSlice(this.#calls, 0); } - get fakeTimers() { - this.#fakeTimers ??= new FakeTimers(); - return this.#fakeTimers; - } callCount() { return this.#calls.length; } @@ -112,6 +108,12 @@ delete MockFunctionContext.prototype.nextImpl; class MockTracker { #mocks = []; + #fakeTimers; + + get fakeTimers() { + this.#fakeTimers ??= new FakeTimers(); + return this.#fakeTimers; + } fn( original = function() {}, From 71ffd10fe8c88f30346051423fff670eca44cdcf Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 1 May 2023 15:30:20 -0300 Subject: [PATCH 04/57] test_runner: implement PriorityQueue for timers Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/fake_timers.js | 58 ++++++++++++++----- .../test-runner-mocking-fake-timers.js | 8 +-- 2 files changed, 46 insertions(+), 20 deletions(-) diff --git a/lib/internal/test_runner/mock/fake_timers.js b/lib/internal/test_runner/mock/fake_timers.js index 01b3cd38a12ef4..738938c9c6357d 100644 --- a/lib/internal/test_runner/mock/fake_timers.js +++ b/lib/internal/test_runner/mock/fake_timers.js @@ -2,14 +2,30 @@ const { DateNow, - SafeMap, - Symbol, + SafeSet, globalThis, } = primordials; +const PriorityQueue = require('internal/priority_queue'); + +function compareTimersLists(a, b) { + const expiryDiff = a.runAt - b.runAt; + if (expiryDiff === 0) { + if (a.id < b.id) + return -1; + if (a.id > b.id) + return 1; + } + return expiryDiff; +} + +function setPosition(node, pos) { + node.priorityQueuePosition = pos; +} class Timers { + #currentTimer = 1; constructor() { - this.timers = new SafeMap(); + this.timers = new PriorityQueue(compareTimersLists, setPosition); this.setTimeout = this.#createTimer.bind(this, false); this.clearTimeout = this.#clearTimer.bind(this); @@ -18,20 +34,20 @@ class Timers { } #createTimer(isInterval, callback, delay, ...args) { - const timerId = Symbol('kTimerId'); - const timer = { + const timerId = this.#currentTimer++; + this.timers.insert({ id: timerId, callback, runAt: DateNow() + delay, interval: isInterval, args, - }; - this.timers.set(timerId, timer); + }); + return timerId; } - #clearTimer(timerId) { - this.timers.delete(timerId); + #clearTimer(position) { + this.timers.removeAt(position); } } @@ -56,18 +72,27 @@ class FakeTimers { this.now += time; const timers = this.fakeTimers.timers; + const alreadyProcessed = new SafeSet(); + while (true) { + const timer = timers.peek(); - for (const timer of timers.values()) { + if (!timer) { + alreadyProcessed.clear(); + break; + } - if (!(this.now >= timer.runAt)) continue; + if (alreadyProcessed.has(timer)) break; + alreadyProcessed.add(timer); + if (!(this.now >= timer.runAt)) continue; timer.callback(...timer.args); - if (timer.interval) { - timer.runAt = this.now + (timer.runAt - this.now) % timer.args[0]; - continue; - } - timers.delete(timer.id); + // if (timer.interval) { + // timer.runAt = this.now + (timer.runAt - this.now) % timer.args[0]; + // continue; + // } + + timers.removeAt(alreadyProcessed.size - 1); } } @@ -89,6 +114,7 @@ class FakeTimers { globalThis.setInterval = this.fakeTimers.setInterval; globalThis.clearInterval = this.fakeTimers.clearInterval; + // this.#dispatchPendingTimers() } reset() { diff --git a/test/parallel/test-runner-mocking-fake-timers.js b/test/parallel/test-runner-mocking-fake-timers.js index b834e174d5777c..b952359aa62eae 100644 --- a/test/parallel/test-runner-mocking-fake-timers.js +++ b/test/parallel/test-runner-mocking-fake-timers.js @@ -23,13 +23,13 @@ describe('Faketimers Test Suite', () => { }); it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { - fakeTimers.enable(); - const fn = mock.fn(); + t.mock.fakeTimers.enable(); + const fn = t.mock.fn(); global.setTimeout(fn, 2000); - fakeTimers.tick(1000); - fakeTimers.tick(1000); + t.mock.fakeTimers.tick(1000); + t.mock.fakeTimers.tick(1000); assert.strictEqual(fn.mock.callCount(), 1); }); From a0f8afdd7afc3f8ffa6e5e9c56c1edb14f3b0aa4 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 1 May 2023 15:42:09 -0300 Subject: [PATCH 05/57] test_runner: add @ljharb suggestion Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/fake_timers.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/lib/internal/test_runner/mock/fake_timers.js b/lib/internal/test_runner/mock/fake_timers.js index 738938c9c6357d..f6e015452f60c8 100644 --- a/lib/internal/test_runner/mock/fake_timers.js +++ b/lib/internal/test_runner/mock/fake_timers.js @@ -9,14 +9,7 @@ const { const PriorityQueue = require('internal/priority_queue'); function compareTimersLists(a, b) { - const expiryDiff = a.runAt - b.runAt; - if (expiryDiff === 0) { - if (a.id < b.id) - return -1; - if (a.id > b.id) - return 1; - } - return expiryDiff; + return (a.runAt - b.runAt) || (a.id - b.id); } function setPosition(node, pos) { From 0f55f65712bd7e684f1b24f2f3d13e35b3935135 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 1 May 2023 16:01:35 -0300 Subject: [PATCH 06/57] test_runner: add @benjamingr suggestion Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/fake_timers.js | 30 +++++-------------- .../test-runner-mocking-fake-timers.js | 25 +++++++++++++++- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/lib/internal/test_runner/mock/fake_timers.js b/lib/internal/test_runner/mock/fake_timers.js index f6e015452f60c8..579514667c8767 100644 --- a/lib/internal/test_runner/mock/fake_timers.js +++ b/lib/internal/test_runner/mock/fake_timers.js @@ -2,7 +2,6 @@ const { DateNow, - SafeSet, globalThis, } = primordials; @@ -62,31 +61,18 @@ class FakeTimers { // if (!this.isEnabled) { // throw new Error('you should enable fakeTimers first by calling the .enable function'); // } - this.now += time; const timers = this.fakeTimers.timers; - const alreadyProcessed = new SafeSet(); - while (true) { - const timer = timers.peek(); - - if (!timer) { - alreadyProcessed.clear(); - break; - } - if (alreadyProcessed.has(timer)) break; - alreadyProcessed.add(timer); - - if (!(this.now >= timer.runAt)) continue; + for (let timer = timers.peek(); timer?.runAt <= this.now; timer = timers.peek()) { timer.callback(...timer.args); - - // if (timer.interval) { - // timer.runAt = this.now + (timer.runAt - this.now) % timer.args[0]; - // continue; - // } - - timers.removeAt(alreadyProcessed.size - 1); + timers.shift(); + if (timer.interval) { + timer.runAt = timer.runAt + timer.interval; + timers.insert(timer); + } } + } enable() { @@ -106,8 +92,6 @@ class FakeTimers { globalThis.clearTimeout = this.fakeTimers.clearTimeout; globalThis.setInterval = this.fakeTimers.setInterval; globalThis.clearInterval = this.fakeTimers.clearInterval; - - // this.#dispatchPendingTimers() } reset() { diff --git a/test/parallel/test-runner-mocking-fake-timers.js b/test/parallel/test-runner-mocking-fake-timers.js index b952359aa62eae..81929df36cec2a 100644 --- a/test/parallel/test-runner-mocking-fake-timers.js +++ b/test/parallel/test-runner-mocking-fake-timers.js @@ -8,6 +8,28 @@ const { fakeTimers } = mock; describe('Faketimers Test Suite', () => { + // describe('setInterval Suite', () => { + // it('should advance in time and trigger timers when calling the .tick function', (t) => { + // t.mock.fakeTimers.enable(); + + // const fn = mock.fn(() => {}); + + // const id = global.setInterval(fn, 200); + + // t.mock.fakeTimers.tick(200); + // console.log('ae') + // t.mock.fakeTimers.tick(200); + // console.log('ae1') + // t.mock.fakeTimers.tick(200); + // console.log('ae3') + // t.mock.fakeTimers.tick(200); + // console.log('ae4') + // global.clearInterval(id) + + // assert.strictEqual(fn.mock.callCount(), 4); + // }); + // }); + describe('setTimeout Suite', () => { afterEach(() => fakeTimers.reset()); @@ -29,7 +51,8 @@ describe('Faketimers Test Suite', () => { global.setTimeout(fn, 2000); t.mock.fakeTimers.tick(1000); - t.mock.fakeTimers.tick(1000); + t.mock.fakeTimers.tick(500); + t.mock.fakeTimers.tick(500); assert.strictEqual(fn.mock.callCount(), 1); }); From e34a426d280ae546ae3ee8f62fb47cae212be7a8 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 1 May 2023 17:42:37 -0300 Subject: [PATCH 07/57] test_runner: add fake setTimeout initial impl Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/fake_timers.js | 106 ++++++++++-------- .../test-runner-mocking-fake-timers.js | 104 +++++++++-------- 2 files changed, 120 insertions(+), 90 deletions(-) diff --git a/lib/internal/test_runner/mock/fake_timers.js b/lib/internal/test_runner/mock/fake_timers.js index 579514667c8767..bb6815674fa767 100644 --- a/lib/internal/test_runner/mock/fake_timers.js +++ b/lib/internal/test_runner/mock/fake_timers.js @@ -2,10 +2,14 @@ const { DateNow, + FunctionPrototypeBind, globalThis, + Promise, } = primordials; const PriorityQueue = require('internal/priority_queue'); +const nodeTimers = require('timers'); +const nodeTimersPromises = require('timers/promises'); function compareTimersLists(a, b) { return (a.runAt - b.runAt) || (a.id - b.id); @@ -14,20 +18,29 @@ function compareTimersLists(a, b) { function setPosition(node, pos) { node.priorityQueuePosition = pos; } -class Timers { + + +class FakeTimers { + #realSetTimeout; + #realClearTimeout; + #realSetInterval; + #realClearInterval; + + #isEnabled = false; #currentTimer = 1; - constructor() { - this.timers = new PriorityQueue(compareTimersLists, setPosition); + #fakeTimers = {}; + #now = DateNow(); - this.setTimeout = this.#createTimer.bind(this, false); - this.clearTimeout = this.#clearTimer.bind(this); - this.setInterval = this.#createTimer.bind(this, true); - this.clearInterval = this.#clearTimer.bind(this); - } + #timers = new PriorityQueue(compareTimersLists, setPosition); + + #setTimeout = FunctionPrototypeBind(this.#createTimer, this, false); + #clearTimeout = FunctionPrototypeBind(this.#clearTimer, this); + #setInterval = FunctionPrototypeBind(this.#createTimer, this, true); + #clearInterval = FunctionPrototypeBind(this.#clearTimer, this); #createTimer(isInterval, callback, delay, ...args) { const timerId = this.#currentTimer++; - this.timers.insert({ + this.#timers.insert({ id: timerId, callback, runAt: DateNow() + delay, @@ -39,21 +52,7 @@ class Timers { } #clearTimer(position) { - this.timers.removeAt(position); - } - -} - -let realSetTimeout; -let realClearTimeout; -let realSetInterval; -let realClearInterval; - -class FakeTimers { - constructor() { - this.fakeTimers = {}; - this.isEnabled = false; - this.now = DateNow(); + this.#timers.removeAt(position); } tick(time = 0) { @@ -61,10 +60,10 @@ class FakeTimers { // if (!this.isEnabled) { // throw new Error('you should enable fakeTimers first by calling the .enable function'); // } - this.now += time; - const timers = this.fakeTimers.timers; + this.#now += time; + const timers = this.#timers; - for (let timer = timers.peek(); timer?.runAt <= this.now; timer = timers.peek()) { + for (let timer = timers.peek(); timer?.runAt <= this.#now; timer = timers.peek()) { timer.callback(...timer.args); timers.shift(); if (timer.interval) { @@ -79,30 +78,45 @@ class FakeTimers { // if (this.isEnabled) { // throw new Error('fakeTimers is already enabled!'); // } - this.now = DateNow(); - this.isEnabled = true; - this.fakeTimers = new Timers(); - - realSetTimeout = globalThis.setTimeout; - realClearTimeout = globalThis.clearTimeout; - realSetInterval = globalThis.setInterval; - realClearInterval = globalThis.clearInterval; - - globalThis.setTimeout = this.fakeTimers.setTimeout; - globalThis.clearTimeout = this.fakeTimers.clearTimeout; - globalThis.setInterval = this.fakeTimers.setInterval; - globalThis.clearInterval = this.fakeTimers.clearInterval; + this.#now = DateNow(); + this.#isEnabled = true; + + this.#realSetTimeout = globalThis.setTimeout; + this.#realClearTimeout = globalThis.clearTimeout; + this.#realSetInterval = globalThis.setInterval; + this.#realClearInterval = globalThis.clearInterval; + + globalThis.setTimeout = this.#setTimeout; + globalThis.clearTimeout = this.#clearTimeout; + globalThis.setInterval = this.#setInterval; + globalThis.clearInterval = this.#clearInterval; + + + nodeTimers.setTimeout = this.#setTimeout; + nodeTimers.clearTimeout = this.#clearTimeout; + nodeTimers.setInterval = this.#setInterval; + nodeTimers.clearInterval = this.#clearInterval; + + nodeTimersPromises.setTimeout = (ms) => new Promise( + (resolve) => this.#setTimeout(resolve, ms), + ); + } reset() { - this.isEnabled = false; - this.fakeTimers = {}; + this.#isEnabled = false; // Restore the real timer functions - globalThis.setTimeout = realSetTimeout; - globalThis.clearTimeout = realClearTimeout; - globalThis.setInterval = realSetInterval; - globalThis.clearInterval = realClearInterval; + globalThis.setTimeout = this.#realSetTimeout; + globalThis.clearTimeout = this.#realClearTimeout; + globalThis.setInterval = this.#realSetInterval; + globalThis.clearInterval = this.#realClearInterval; + + let timer = this.#timers.peek(); + while (timer) { + this.#timers.shift(); + timer = this.#timers.peek(); + } } releaseAllTimers() { diff --git a/test/parallel/test-runner-mocking-fake-timers.js b/test/parallel/test-runner-mocking-fake-timers.js index 81929df36cec2a..73413c289ee534 100644 --- a/test/parallel/test-runner-mocking-fake-timers.js +++ b/test/parallel/test-runner-mocking-fake-timers.js @@ -1,71 +1,87 @@ 'use strict'; -const common = require('../common'); process.env.NODE_TEST_KNOWN_GLOBALS = 0; +const common = require('../common'); const assert = require('node:assert'); const { it, mock, afterEach, describe } = require('node:test'); +const nodeTimers = require('node:timers'); +const nodeTimersPromises = require('node:timers/promises'); + const { fakeTimers } = mock; describe('Faketimers Test Suite', () => { + describe('globals/timers', () => { + describe('setTimeout Suite', () => { + afterEach(() => fakeTimers.reset()); - // describe('setInterval Suite', () => { - // it('should advance in time and trigger timers when calling the .tick function', (t) => { - // t.mock.fakeTimers.enable(); + it('should advance in time and trigger timers when calling the .tick function', (t) => { + fakeTimers.enable(); - // const fn = mock.fn(() => {}); + const fn = mock.fn(() => {}); - // const id = global.setInterval(fn, 200); + global.setTimeout(fn, 4000); - // t.mock.fakeTimers.tick(200); - // console.log('ae') - // t.mock.fakeTimers.tick(200); - // console.log('ae1') - // t.mock.fakeTimers.tick(200); - // console.log('ae3') - // t.mock.fakeTimers.tick(200); - // console.log('ae4') - // global.clearInterval(id) + fakeTimers.tick(4000); + assert.ok(fn.mock.callCount()); + }); - // assert.strictEqual(fn.mock.callCount(), 4); - // }); - // }); + it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { + t.mock.fakeTimers.enable(); + const fn = t.mock.fn(); - describe('setTimeout Suite', () => { - afterEach(() => fakeTimers.reset()); + global.setTimeout(fn, 2000); - it('should advance in time and trigger timers when calling the .tick function', (t) => { - fakeTimers.enable(); + t.mock.fakeTimers.tick(1000); + t.mock.fakeTimers.tick(500); + t.mock.fakeTimers.tick(500); - const fn = mock.fn(() => {}); + assert.strictEqual(fn.mock.callCount(), 1); + }); - global.setTimeout(fn, 4000); + it('should keep setTimeout working if fakeTimers are disabled', (t, done) => { + const now = Date.now(); + const timeout = 2; + const expected = () => now - timeout; + global.setTimeout(common.mustCall(() => { + assert.strictEqual(now - timeout, expected()); + done(); + }), timeout); + }); - fakeTimers.tick(4000); - assert.ok(fn.mock.callCount()); }); + }); - it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { - t.mock.fakeTimers.enable(); - const fn = t.mock.fn(); - - global.setTimeout(fn, 2000); + describe('timers Suite', () => { + describe('setTimeout Suite', () => { + it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { + t.mock.fakeTimers.enable(); + const fn = t.mock.fn(); + const { setTimeout } = nodeTimers; + setTimeout(fn, 2000); - t.mock.fakeTimers.tick(1000); - t.mock.fakeTimers.tick(500); - t.mock.fakeTimers.tick(500); + t.mock.fakeTimers.tick(1000); + t.mock.fakeTimers.tick(500); + t.mock.fakeTimers.tick(500); - assert.strictEqual(fn.mock.callCount(), 1); + assert.strictEqual(fn.mock.callCount(), 1); + }); }); + }); - it('should keep setTimeout working if fakeTimers are disabled', (t, done) => { - const now = Date.now(); - const timeout = 2; - const expected = () => now - timeout; - global.setTimeout(common.mustCall(() => { - assert.strictEqual(now - timeout, expected()); - done(); - }), timeout); - }); + describe('timers/promises', () => { + describe('setTimeout Suite', () => { + it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => { + t.mock.fakeTimers.enable(); + + const p = nodeTimersPromises.setTimeout(2000); + t.mock.fakeTimers.tick(1000); + t.mock.fakeTimers.tick(500); + t.mock.fakeTimers.tick(500); + t.mock.fakeTimers.tick(500); + + p.finally(common.mustCall(() => assert.ok(p !== 0))); + }); + }); }); }); From df35174179cb5680de4b93883b898cd6cc38d708 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 1 May 2023 17:43:58 -0300 Subject: [PATCH 08/57] test_runner: add @molow suggestion Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/fake_timers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/test_runner/mock/fake_timers.js b/lib/internal/test_runner/mock/fake_timers.js index bb6815674fa767..ccd006593569d0 100644 --- a/lib/internal/test_runner/mock/fake_timers.js +++ b/lib/internal/test_runner/mock/fake_timers.js @@ -55,7 +55,7 @@ class FakeTimers { this.#timers.removeAt(position); } - tick(time = 0) { + tick(time = 1) { // if (!this.isEnabled) { // throw new Error('you should enable fakeTimers first by calling the .enable function'); From e5af52fce5a5b4f43a35c16efcf6032932d30640 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 1 May 2023 17:48:02 -0300 Subject: [PATCH 09/57] test_runner: fix a test Signed-off-by: Erick Wendel --- test/parallel/test-runner-mocking-fake-timers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-runner-mocking-fake-timers.js b/test/parallel/test-runner-mocking-fake-timers.js index 73413c289ee534..a0147f2313be5a 100644 --- a/test/parallel/test-runner-mocking-fake-timers.js +++ b/test/parallel/test-runner-mocking-fake-timers.js @@ -80,7 +80,7 @@ describe('Faketimers Test Suite', () => { t.mock.fakeTimers.tick(500); t.mock.fakeTimers.tick(500); - p.finally(common.mustCall(() => assert.ok(p !== 0))); + p.then(common.mustCall((result) => assert.ok(result))); }); }); }); From a22321fa451a55a52f6da46bdfcd0ae45560ed77 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 3 May 2023 18:31:52 -0300 Subject: [PATCH 10/57] test_runner: rename from fakeTimers to mockTimers Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock.js | 11 +++--- .../mock/{fake_timers.js => mock_timers.js} | 21 ++++++++-- ...e-timers.js => test-runner-mock-timers.js} | 38 +++++++++---------- 3 files changed, 41 insertions(+), 29 deletions(-) rename lib/internal/test_runner/mock/{fake_timers.js => mock_timers.js} (84%) rename test/parallel/{test-runner-mocking-fake-timers.js => test-runner-mock-timers.js} (71%) diff --git a/lib/internal/test_runner/mock/mock.js b/lib/internal/test_runner/mock/mock.js index 63c59a129ebeba..f4064b81f0b319 100644 --- a/lib/internal/test_runner/mock/mock.js +++ b/lib/internal/test_runner/mock/mock.js @@ -26,7 +26,7 @@ const { validateInteger, validateObject, } = require('internal/validators'); -const { FakeTimers } = require('internal/test_runner/mock/fake_timers'); +const { MockTimers } = require('internal/test_runner/mock/mock_timers'); function kDefaultFunction() {} @@ -36,7 +36,6 @@ class MockFunctionContext { #implementation; #restore; #times; - #fakeTimers; constructor(implementation, restore, times) { this.#calls = []; @@ -108,11 +107,11 @@ delete MockFunctionContext.prototype.nextImpl; class MockTracker { #mocks = []; - #fakeTimers; + #timers; - get fakeTimers() { - this.#fakeTimers ??= new FakeTimers(); - return this.#fakeTimers; + get timers() { + this.#timers ??= new MockTimers(); + return this.#timers; } fn( diff --git a/lib/internal/test_runner/mock/fake_timers.js b/lib/internal/test_runner/mock/mock_timers.js similarity index 84% rename from lib/internal/test_runner/mock/fake_timers.js rename to lib/internal/test_runner/mock/mock_timers.js index ccd006593569d0..e8a40e99735ae2 100644 --- a/lib/internal/test_runner/mock/fake_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -20,15 +20,21 @@ function setPosition(node, pos) { } -class FakeTimers { +class MockTimers { #realSetTimeout; #realClearTimeout; #realSetInterval; #realClearInterval; + #realPromisifiedSetTimeout; + + #realTimersSetTimeout; + #realTimersClearTimeout; + #realTimersSetInterval; + #realTimersClearInterval; + #isEnabled = false; #currentTimer = 1; - #fakeTimers = {}; #now = DateNow(); #timers = new PriorityQueue(compareTimersLists, setPosition); @@ -86,12 +92,18 @@ class FakeTimers { this.#realSetInterval = globalThis.setInterval; this.#realClearInterval = globalThis.clearInterval; + this.#realTimersSetTimeout = nodeTimers.setTimeout; + this.#realTimersClearTimeout = nodeTimers.clearTimeout; + this.#realTimersSetInterval = nodeTimers.setInterval; + this.#realTimersClearInterval = nodeTimers.clearInterval; + + this.#realPromisifiedSetTimeout = nodeTimersPromises.setTimeout; + globalThis.setTimeout = this.#setTimeout; globalThis.clearTimeout = this.#clearTimeout; globalThis.setInterval = this.#setInterval; globalThis.clearInterval = this.#clearInterval; - nodeTimers.setTimeout = this.#setTimeout; nodeTimers.clearTimeout = this.#clearTimeout; nodeTimers.setInterval = this.#setInterval; @@ -111,6 +123,7 @@ class FakeTimers { globalThis.clearTimeout = this.#realClearTimeout; globalThis.setInterval = this.#realSetInterval; globalThis.clearInterval = this.#realClearInterval; + nodeTimersPromises.setTimeout = this.#realPromisifiedSetTimeout; let timer = this.#timers.peek(); while (timer) { @@ -124,4 +137,4 @@ class FakeTimers { } } -module.exports = { FakeTimers }; +module.exports = { MockTimers }; diff --git a/test/parallel/test-runner-mocking-fake-timers.js b/test/parallel/test-runner-mock-timers.js similarity index 71% rename from test/parallel/test-runner-mocking-fake-timers.js rename to test/parallel/test-runner-mock-timers.js index a0147f2313be5a..fd644c21c5be7f 100644 --- a/test/parallel/test-runner-mocking-fake-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -7,38 +7,38 @@ const { it, mock, afterEach, describe } = require('node:test'); const nodeTimers = require('node:timers'); const nodeTimersPromises = require('node:timers/promises'); -const { fakeTimers } = mock; +const { timers } = mock; -describe('Faketimers Test Suite', () => { +describe('Timers Test Suite', () => { describe('globals/timers', () => { describe('setTimeout Suite', () => { - afterEach(() => fakeTimers.reset()); + afterEach(() => timers.reset()); it('should advance in time and trigger timers when calling the .tick function', (t) => { - fakeTimers.enable(); + timers.enable(); const fn = mock.fn(() => {}); global.setTimeout(fn, 4000); - fakeTimers.tick(4000); + timers.tick(4000); assert.ok(fn.mock.callCount()); }); it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { - t.mock.fakeTimers.enable(); + t.mock.timers.enable(); const fn = t.mock.fn(); global.setTimeout(fn, 2000); - t.mock.fakeTimers.tick(1000); - t.mock.fakeTimers.tick(500); - t.mock.fakeTimers.tick(500); + t.mock.timers.tick(1000); + t.mock.timers.tick(500); + t.mock.timers.tick(500); assert.strictEqual(fn.mock.callCount(), 1); }); - it('should keep setTimeout working if fakeTimers are disabled', (t, done) => { + it('should keep setTimeout working if timers are disabled', (t, done) => { const now = Date.now(); const timeout = 2; const expected = () => now - timeout; @@ -54,14 +54,14 @@ describe('Faketimers Test Suite', () => { describe('timers Suite', () => { describe('setTimeout Suite', () => { it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { - t.mock.fakeTimers.enable(); + t.mock.timers.enable(); const fn = t.mock.fn(); const { setTimeout } = nodeTimers; setTimeout(fn, 2000); - t.mock.fakeTimers.tick(1000); - t.mock.fakeTimers.tick(500); - t.mock.fakeTimers.tick(500); + t.mock.timers.tick(1000); + t.mock.timers.tick(500); + t.mock.timers.tick(500); assert.strictEqual(fn.mock.callCount(), 1); }); @@ -71,14 +71,14 @@ describe('Faketimers Test Suite', () => { describe('timers/promises', () => { describe('setTimeout Suite', () => { it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => { - t.mock.fakeTimers.enable(); + t.mock.timers.enable(); const p = nodeTimersPromises.setTimeout(2000); - t.mock.fakeTimers.tick(1000); - t.mock.fakeTimers.tick(500); - t.mock.fakeTimers.tick(500); - t.mock.fakeTimers.tick(500); + t.mock.timers.tick(1000); + t.mock.timers.tick(500); + t.mock.timers.tick(500); + t.mock.timers.tick(500); p.then(common.mustCall((result) => assert.ok(result))); }); From 21be0bcfee3b4230fb5b5ff07ea5e5d397233ef2 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 3 May 2023 18:37:43 -0300 Subject: [PATCH 11/57] test_runner: add experimental warning Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index e8a40e99735ae2..f478043dab6b5f 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -1,4 +1,8 @@ 'use strict'; +const { + emitExperimentalWarning, +} = require('internal/util'); + const { DateNow, @@ -44,6 +48,10 @@ class MockTimers { #setInterval = FunctionPrototypeBind(this.#createTimer, this, true); #clearInterval = FunctionPrototypeBind(this.#clearTimer, this); + constructor() { + emitExperimentalWarning('The MockTimers API'); + } + #createTimer(isInterval, callback, delay, ...args) { const timerId = this.#currentTimer++; this.#timers.insert({ From 102a83089fa0c61cfe769ba4e3a26b780403b350 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 3 May 2023 19:30:52 -0300 Subject: [PATCH 12/57] test_runner: fix tests Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 13 +++++++++++-- test/parallel/test-runner-mock-timers.js | 13 ++++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index f478043dab6b5f..64bf4e79bbfcf8 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -1,9 +1,9 @@ 'use strict'; + const { emitExperimentalWarning, } = require('internal/util'); - const { DateNow, FunctionPrototypeBind, @@ -55,6 +55,7 @@ class MockTimers { #createTimer(isInterval, callback, delay, ...args) { const timerId = this.#currentTimer++; this.#timers.insert({ + __proto__: null, id: timerId, callback, runAt: DateNow() + delay, @@ -118,7 +119,9 @@ class MockTimers { nodeTimers.clearInterval = this.#clearInterval; nodeTimersPromises.setTimeout = (ms) => new Promise( - (resolve) => this.#setTimeout(resolve, ms), + (resolve) => { + const id = this.#setTimeout(() => resolve(id), ms); + }, ); } @@ -131,6 +134,12 @@ class MockTimers { globalThis.clearTimeout = this.#realClearTimeout; globalThis.setInterval = this.#realSetInterval; globalThis.clearInterval = this.#realClearInterval; + + nodeTimers.setTimeout = this.#realTimersSetTimeout; + nodeTimers.clearTimeout = this.#realTimersClearTimeout; + nodeTimers.setInterval = this.#realTimersSetInterval; + nodeTimers.clearInterval = this.#realTimersClearInterval; + nodeTimersPromises.setTimeout = this.#realPromisifiedSetTimeout; let timer = this.#timers.peek(); diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index fd644c21c5be7f..2e5e0cced1b493 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -3,16 +3,15 @@ process.env.NODE_TEST_KNOWN_GLOBALS = 0; const common = require('../common'); const assert = require('node:assert'); -const { it, mock, afterEach, describe } = require('node:test'); +const { it, mock, describe } = require('node:test'); const nodeTimers = require('node:timers'); const nodeTimersPromises = require('node:timers/promises'); const { timers } = mock; -describe('Timers Test Suite', () => { +describe('Mock Timers Test Suite', () => { describe('globals/timers', () => { describe('setTimeout Suite', () => { - afterEach(() => timers.reset()); it('should advance in time and trigger timers when calling the .tick function', (t) => { timers.enable(); @@ -22,7 +21,8 @@ describe('Timers Test Suite', () => { global.setTimeout(fn, 4000); timers.tick(4000); - assert.ok(fn.mock.callCount()); + assert.strictEqual(fn.mock.callCount(), 1); + timers.reset(); }); it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { @@ -36,6 +36,7 @@ describe('Timers Test Suite', () => { t.mock.timers.tick(500); assert.strictEqual(fn.mock.callCount(), 1); + t.mock.timers.reset(); }); it('should keep setTimeout working if timers are disabled', (t, done) => { @@ -80,7 +81,9 @@ describe('Timers Test Suite', () => { t.mock.timers.tick(500); t.mock.timers.tick(500); - p.then(common.mustCall((result) => assert.ok(result))); + p.then(common.mustCall((result) => { + assert.ok(result); + })); }); }); }); From 1d30e47b2f9a1872562e3e50ca09db1765804ceb Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 3 May 2023 19:38:02 -0300 Subject: [PATCH 13/57] test_runner: add @ljharb suggestion Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 64bf4e79bbfcf8..986a799387dd3f 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -82,7 +82,7 @@ class MockTimers { timer.callback(...timer.args); timers.shift(); if (timer.interval) { - timer.runAt = timer.runAt + timer.interval; + timer.runAt += timer.interval; timers.insert(timer); } } From 0cb4e7cc90ec2dd0a963eb8a7a6ce8ddbc4a9745 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 3 May 2023 19:45:22 -0300 Subject: [PATCH 14/57] test_runner: fix possible truthy evaluation on tick by @ljharb Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 986a799387dd3f..ec06443e1ccf9b 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -76,14 +76,13 @@ class MockTimers { // throw new Error('you should enable fakeTimers first by calling the .enable function'); // } this.#now += time; - const timers = this.#timers; - for (let timer = timers.peek(); timer?.runAt <= this.#now; timer = timers.peek()) { + for (let timer = this.#timers.peek(); timer && timer.runAt <= this.#now; timer = this.#timers.peek()) { timer.callback(...timer.args); - timers.shift(); + this.#timers.shift(); if (timer.interval) { timer.runAt += timer.interval; - timers.insert(timer); + this.#timers.insert(timer); } } From a53aedce4954a8c0e51f2838e93f823393ac23ff Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 3 May 2023 20:03:06 -0300 Subject: [PATCH 15/57] test_runner: add tests for clearTimeout Signed-off-by: Erick Wendel --- test/parallel/test-runner-mock-timers.js | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 2e5e0cced1b493..7f5ff163172124 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -50,6 +50,21 @@ describe('Mock Timers Test Suite', () => { }); }); + + describe('clearTimeout Suite', () => { + it('should not advance in time if clearTimeout was invoked', (t) => { + t.mock.timers.enable() + + const fn = mock.fn(() => {}); + + const id = global.setTimeout(fn, 4000); + global.clearTimeout(id) + timers.tick(4000); + + assert.strictEqual(fn.mock.callCount(), 0); + t.mock.timers.reset() + }); + }); }); describe('timers Suite', () => { @@ -67,6 +82,21 @@ describe('Mock Timers Test Suite', () => { assert.strictEqual(fn.mock.callCount(), 1); }); }); + + describe('clearTimeout Suite', () => { + it('should not advance in time if clearTimeout was invoked', (t) => { + t.mock.timers.enable() + + const fn = mock.fn(() => {}); + const { setTimeout, clearTimeout } = nodeTimers; + const id = setTimeout(fn, 2000); + clearTimeout(id) + timers.tick(2000); + + assert.strictEqual(fn.mock.callCount(), 0); + t.mock.timers.reset() + }); + }); }); describe('timers/promises', () => { From 1a2cf347153a82462cd8da4ab722fffee46aba8c Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 3 May 2023 20:40:53 -0300 Subject: [PATCH 16/57] test_runner: add tests for setInterval modules Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 21 ++++++-- test/parallel/test-runner-mock-timers.js | 56 ++++++++++++++++---- 2 files changed, 63 insertions(+), 14 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index ec06443e1ccf9b..fe9126c22cb596 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -14,6 +14,7 @@ const { const PriorityQueue = require('internal/priority_queue'); const nodeTimers = require('timers'); const nodeTimersPromises = require('timers/promises'); +const console = require('console') function compareTimersLists(a, b) { return (a.runAt - b.runAt) || (a.id - b.id); @@ -61,7 +62,7 @@ class MockTimers { runAt: DateNow() + delay, interval: isInterval, args, - }); + }); return timerId; } @@ -71,19 +72,31 @@ class MockTimers { } tick(time = 1) { - // if (!this.isEnabled) { // throw new Error('you should enable fakeTimers first by calling the .enable function'); // } - this.#now += time; - for (let timer = this.#timers.peek(); timer && timer.runAt <= this.#now; timer = this.#timers.peek()) { + // Increase the current time by the specified time. + this.#now += time; + + // Execute all timers whose runAt time is less than or equal to the current time. + let timer = this.#timers.peek(); + while (timer && timer.runAt <= this.#now) { + // Execute the timer's callback function with the specified arguments. timer.callback(...timer.args); + + // Remove the timer from the timer queue. this.#timers.shift(); + + // If the timer is an interval timer, update its runAt time and re-insert it into the timer queue. if (timer.interval) { timer.runAt += timer.interval; this.#timers.insert(timer); + return; } + + // Get the next timer in the timer queue. + timer = this.#timers.peek(); } } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 7f5ff163172124..1ec37b0c195eea 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -7,22 +7,20 @@ const { it, mock, describe } = require('node:test'); const nodeTimers = require('node:timers'); const nodeTimersPromises = require('node:timers/promises'); -const { timers } = mock; - describe('Mock Timers Test Suite', () => { describe('globals/timers', () => { describe('setTimeout Suite', () => { it('should advance in time and trigger timers when calling the .tick function', (t) => { - timers.enable(); + mock.timers.enable(); - const fn = mock.fn(() => {}); + const fn = mock.fn(); global.setTimeout(fn, 4000); - timers.tick(4000); + mock.timers.tick(4000); assert.strictEqual(fn.mock.callCount(), 1); - timers.reset(); + mock.timers.reset(); }); it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { @@ -55,16 +53,34 @@ describe('Mock Timers Test Suite', () => { it('should not advance in time if clearTimeout was invoked', (t) => { t.mock.timers.enable() - const fn = mock.fn(() => {}); + const fn = mock.fn(); const id = global.setTimeout(fn, 4000); global.clearTimeout(id) - timers.tick(4000); + t.mock.timers.tick(4000); assert.strictEqual(fn.mock.callCount(), 0); t.mock.timers.reset() }); }); + + describe('setInterval Suite', () => { + it('should tick three times using fake setInterval', (t) => { + t.mock.timers.enable(); + const fn = t.mock.fn(); + + const id = global.setInterval(fn, 200); + + t.mock.timers.tick(200); + t.mock.timers.tick(200); + t.mock.timers.tick(200); + + global.clearInterval(id); + + assert.strictEqual(fn.mock.callCount(), 3); + t.mock.timers.reset(); + }); + }); }); describe('timers Suite', () => { @@ -87,16 +103,35 @@ describe('Mock Timers Test Suite', () => { it('should not advance in time if clearTimeout was invoked', (t) => { t.mock.timers.enable() - const fn = mock.fn(() => {}); + const fn = mock.fn(); const { setTimeout, clearTimeout } = nodeTimers; const id = setTimeout(fn, 2000); clearTimeout(id) - timers.tick(2000); + t.mock.timers.tick(2000); assert.strictEqual(fn.mock.callCount(), 0); t.mock.timers.reset() }); }); + + describe('setInterval Suite', () => { + it('should tick three times using fake setInterval', (t) => { + t.mock.timers.enable(); + const fn = t.mock.fn(); + + const id = nodeTimers.setInterval(fn, 200); + + t.mock.timers.tick(200); + t.mock.timers.tick(200); + t.mock.timers.tick(200); + t.mock.timers.tick(200); + + nodeTimers.clearInterval(id); + + assert.strictEqual(fn.mock.callCount(), 4); + t.mock.timers.reset(); + }); + }); }); describe('timers/promises', () => { @@ -113,6 +148,7 @@ describe('Mock Timers Test Suite', () => { p.then(common.mustCall((result) => { assert.ok(result); + t.mock.timers.reset(); })); }); }); From db2a79f5f46401de6fc984a401b1226e95645c4e Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 3 May 2023 20:47:13 -0300 Subject: [PATCH 17/57] test_runner: add tests for clearInterval modules Signed-off-by: Erick Wendel --- test/parallel/test-runner-mock-timers.js | 40 +++++++++++++++++++++--- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 1ec37b0c195eea..eddbe9bc7412af 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -52,13 +52,13 @@ describe('Mock Timers Test Suite', () => { describe('clearTimeout Suite', () => { it('should not advance in time if clearTimeout was invoked', (t) => { t.mock.timers.enable() - + const fn = mock.fn(); - + const id = global.setTimeout(fn, 4000); global.clearTimeout(id) t.mock.timers.tick(4000); - + assert.strictEqual(fn.mock.callCount(), 0); t.mock.timers.reset() }); @@ -81,6 +81,21 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.reset(); }); }); + + describe('clearInterval Suite', () => { + it('should not advance in time if clearInterval was invoked', (t) => { + t.mock.timers.enable() + + const fn = mock.fn(); + const id = global.setInterval(fn, 200); + global.clearInterval(id) + t.mock.timers.tick(200); + + assert.strictEqual(fn.mock.callCount(), 0); + t.mock.timers.reset() + }); + }); + }); describe('timers Suite', () => { @@ -102,13 +117,13 @@ describe('Mock Timers Test Suite', () => { describe('clearTimeout Suite', () => { it('should not advance in time if clearTimeout was invoked', (t) => { t.mock.timers.enable() - + const fn = mock.fn(); const { setTimeout, clearTimeout } = nodeTimers; const id = setTimeout(fn, 2000); clearTimeout(id) t.mock.timers.tick(2000); - + assert.strictEqual(fn.mock.callCount(), 0); t.mock.timers.reset() }); @@ -132,6 +147,21 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.reset(); }); }); + + describe('clearInterval Suite', () => { + it('should not advance in time if clearInterval was invoked', (t) => { + t.mock.timers.enable() + + const fn = mock.fn(); + const { setInterval, clearInterval } = nodeTimers; + const id = setInterval(fn, 200); + clearInterval(id) + t.mock.timers.tick(200); + + assert.strictEqual(fn.mock.callCount(), 0); + t.mock.timers.reset() + }); + }); }); describe('timers/promises', () => { From f200467ed9e6eacbbee5470d57a3c441c29c5038 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 3 May 2023 22:56:30 -0300 Subject: [PATCH 18/57] test_runner: add impl for timers.promises.setInterval function Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 25 +++++++-- test/parallel/test-runner-mock-timers.js | 55 +++++++++++++++----- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index fe9126c22cb596..552d3e9dea5c06 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -14,7 +14,6 @@ const { const PriorityQueue = require('internal/priority_queue'); const nodeTimers = require('timers'); const nodeTimersPromises = require('timers/promises'); -const console = require('console') function compareTimersLists(a, b) { return (a.runAt - b.runAt) || (a.id - b.id); @@ -32,6 +31,7 @@ class MockTimers { #realClearInterval; #realPromisifiedSetTimeout; + #realPromisifiedSetInterval; #realTimersSetTimeout; #realTimersClearTimeout; @@ -62,7 +62,7 @@ class MockTimers { runAt: DateNow() + delay, interval: isInterval, args, - }); + }); return timerId; } @@ -71,17 +71,29 @@ class MockTimers { this.#timers.removeAt(position); } + async * #setIntervalPromisified(interval, startTime, ...args) { + while (true) { + yield startTime; + await new Promise((resolve) => { + this.#createTimer(false, resolve, interval, ...args); + }); + + startTime += interval; + } + } + tick(time = 1) { // if (!this.isEnabled) { // throw new Error('you should enable fakeTimers first by calling the .enable function'); // } - // Increase the current time by the specified time. - this.#now += time; - + // Increase the current time by the specified time. + this.#now += time; + // Execute all timers whose runAt time is less than or equal to the current time. let timer = this.#timers.peek(); while (timer && timer.runAt <= this.#now) { + // Execute the timer's callback function with the specified arguments. timer.callback(...timer.args); @@ -119,6 +131,7 @@ class MockTimers { this.#realTimersClearInterval = nodeTimers.clearInterval; this.#realPromisifiedSetTimeout = nodeTimersPromises.setTimeout; + this.#realPromisifiedSetInterval = nodeTimersPromises.setInterval; globalThis.setTimeout = this.#setTimeout; globalThis.clearTimeout = this.#clearTimeout; @@ -136,6 +149,7 @@ class MockTimers { }, ); + nodeTimersPromises.setInterval = FunctionPrototypeBind(this.#setIntervalPromisified, this); } reset() { @@ -153,6 +167,7 @@ class MockTimers { nodeTimers.clearInterval = this.#realTimersClearInterval; nodeTimersPromises.setTimeout = this.#realPromisifiedSetTimeout; + nodeTimersPromises.setInterval = this.#realPromisifiedSetInterval; let timer = this.#timers.peek(); while (timer) { diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index eddbe9bc7412af..225ed7368f152a 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -51,16 +51,16 @@ describe('Mock Timers Test Suite', () => { describe('clearTimeout Suite', () => { it('should not advance in time if clearTimeout was invoked', (t) => { - t.mock.timers.enable() + t.mock.timers.enable(); const fn = mock.fn(); const id = global.setTimeout(fn, 4000); - global.clearTimeout(id) + global.clearTimeout(id); t.mock.timers.tick(4000); assert.strictEqual(fn.mock.callCount(), 0); - t.mock.timers.reset() + t.mock.timers.reset(); }); }); @@ -84,15 +84,15 @@ describe('Mock Timers Test Suite', () => { describe('clearInterval Suite', () => { it('should not advance in time if clearInterval was invoked', (t) => { - t.mock.timers.enable() + t.mock.timers.enable(); const fn = mock.fn(); const id = global.setInterval(fn, 200); - global.clearInterval(id) + global.clearInterval(id); t.mock.timers.tick(200); assert.strictEqual(fn.mock.callCount(), 0); - t.mock.timers.reset() + t.mock.timers.reset(); }); }); @@ -116,16 +116,16 @@ describe('Mock Timers Test Suite', () => { describe('clearTimeout Suite', () => { it('should not advance in time if clearTimeout was invoked', (t) => { - t.mock.timers.enable() + t.mock.timers.enable(); const fn = mock.fn(); const { setTimeout, clearTimeout } = nodeTimers; const id = setTimeout(fn, 2000); - clearTimeout(id) + clearTimeout(id); t.mock.timers.tick(2000); assert.strictEqual(fn.mock.callCount(), 0); - t.mock.timers.reset() + t.mock.timers.reset(); }); }); @@ -150,16 +150,16 @@ describe('Mock Timers Test Suite', () => { describe('clearInterval Suite', () => { it('should not advance in time if clearInterval was invoked', (t) => { - t.mock.timers.enable() + t.mock.timers.enable(); const fn = mock.fn(); const { setInterval, clearInterval } = nodeTimers; const id = setInterval(fn, 200); - clearInterval(id) + clearInterval(id); t.mock.timers.tick(200); assert.strictEqual(fn.mock.callCount(), 0); - t.mock.timers.reset() + t.mock.timers.reset(); }); }); }); @@ -182,5 +182,36 @@ describe('Mock Timers Test Suite', () => { })); }); }); + + describe('setInterval Suite', () => { + it('should tick three times using fake setInterval', async (t) => { + t.mock.timers.enable(); + + const results = []; + const interval = 100; + const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now()); + + const first = intervalIterator.next(); + const second = intervalIterator.next(); + const third = intervalIterator.next(); + + t.mock.timers.tick(interval); + results.push(await first); + + t.mock.timers.tick(interval); + results.push(await second); + + t.mock.timers.tick(interval); + results.push(await third); + + results.forEach((result) => { + assert.strictEqual(typeof result.value, 'number'); + assert.strictEqual(result.done, false); + }); + + t.mock.timers.reset(); + }); + }); + }); }); From cc652fe2454b0e4dd12e72798628afb7f22337f5 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Thu, 4 May 2023 16:14:46 -0300 Subject: [PATCH 19/57] test_runner: fix setInterval algorithm Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 13 +++---- test/parallel/test-runner-mock-timers.js | 41 +++++++++++++++++--- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 552d3e9dea5c06..573f34d5e6ea36 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -14,6 +14,7 @@ const { const PriorityQueue = require('internal/priority_queue'); const nodeTimers = require('timers'); const nodeTimersPromises = require('timers/promises'); +const EventEmitter = require('events'); function compareTimersLists(a, b) { return (a.runAt - b.runAt) || (a.id - b.id); @@ -72,13 +73,12 @@ class MockTimers { } async * #setIntervalPromisified(interval, startTime, ...args) { - while (true) { - yield startTime; - await new Promise((resolve) => { - this.#createTimer(false, resolve, interval, ...args); - }); + const intervalEmitter = new EventEmitter(); + const callback = () => intervalEmitter.emit('data', startTime); + this.#createTimer(true, callback, interval, ...args); - startTime += interval; + for await (const events of EventEmitter.on(intervalEmitter, 'data')) { + yield events.at(0); } } @@ -93,7 +93,6 @@ class MockTimers { // Execute all timers whose runAt time is less than or equal to the current time. let timer = this.#timers.peek(); while (timer && timer.runAt <= this.#now) { - // Execute the timer's callback function with the specified arguments. timer.callback(...timer.args); diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 225ed7368f152a..a0817d5db82545 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -187,7 +187,6 @@ describe('Mock Timers Test Suite', () => { it('should tick three times using fake setInterval', async (t) => { t.mock.timers.enable(); - const results = []; const interval = 100; const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now()); @@ -196,13 +195,14 @@ describe('Mock Timers Test Suite', () => { const third = intervalIterator.next(); t.mock.timers.tick(interval); - results.push(await first); - t.mock.timers.tick(interval); - results.push(await second); - t.mock.timers.tick(interval); - results.push(await third); + + const results = await Promise.all([ + first, + second, + third, + ]); results.forEach((result) => { assert.strictEqual(typeof result.value, 'number'); @@ -211,6 +211,35 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.reset(); }); + + it('should tick five times testing a real use case', async (t) => { + + t.mock.timers.enable(); + + const expectedIterations = 5; + const interval = 1000; + + async function run() { + const timers = []; + for await (const startTime of nodeTimersPromises.setInterval(interval, Date.now())) { + timers.push(startTime); + if (timers.length === expectedIterations) break; + + } + return timers; + } + + const r = run(); + t.mock.timers.tick(interval); + t.mock.timers.tick(interval); + t.mock.timers.tick(interval); + t.mock.timers.tick(interval); + t.mock.timers.tick(interval); + + const timersResults = await r; + assert.strictEqual(timersResults.length, expectedIterations); + t.mock.timers.reset(); + }); }); }); From 942f5049ca950e27e41a6dc3d22ffeb19dcaa2d1 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Sun, 14 May 2023 17:30:19 +0200 Subject: [PATCH 20/57] test_runner: add abortController support for promises.setTimeout and more tests Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 36 ++++-- test/parallel/test-runner-mock-timers.js | 112 +++++++++++++++++++ 2 files changed, 141 insertions(+), 7 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 573f34d5e6ea36..c80ffca4fdc099 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -10,7 +10,12 @@ const { globalThis, Promise, } = primordials; - +const { + validateAbortSignal, +} = require('internal/validators'); +const { + AbortError, +} = require('internal/errors'); const PriorityQueue = require('internal/priority_queue'); const nodeTimers = require('timers'); const nodeTimersPromises = require('timers/promises'); @@ -82,6 +87,28 @@ class MockTimers { } } + #setTimeoutPromisified(ms, result, options) { + return new Promise((resolve, reject) => { + const abortIt = (signal) => new AbortError(undefined, { cause: signal.reason }); + if (options?.signal) { + try { + validateAbortSignal(options.signal, 'options.signal'); + } catch (err) { + return reject(err); + } + + if (options.signal?.aborted) { + return reject(abortIt(options.signal)); + } + } + const id = this.#setTimeout(() => resolve(result || id), ms); + options?.signal?.addEventListener('abort', () => { + this.#clearTimeout(id); + return reject(abortIt(options.signal)); + }); + }); + } + tick(time = 1) { // if (!this.isEnabled) { // throw new Error('you should enable fakeTimers first by calling the .enable function'); @@ -142,12 +169,7 @@ class MockTimers { nodeTimers.setInterval = this.#setInterval; nodeTimers.clearInterval = this.#clearInterval; - nodeTimersPromises.setTimeout = (ms) => new Promise( - (resolve) => { - const id = this.#setTimeout(() => resolve(id), ms); - }, - ); - + nodeTimersPromises.setTimeout = FunctionPrototypeBind(this.#setTimeoutPromisified, this); nodeTimersPromises.setInterval = FunctionPrototypeBind(this.#setIntervalPromisified, this); } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index a0817d5db82545..3624919a33166a 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -37,6 +37,21 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.reset(); }); + it('should work with the same params as the original setTimeout', (t) => { + t.mock.timers.enable(); + const fn = t.mock.fn(); + const args = ['a', 'b', 'c']; + global.setTimeout(fn, 2000, ...args); + + t.mock.timers.tick(1000); + t.mock.timers.tick(500); + t.mock.timers.tick(500); + + assert.strictEqual(fn.mock.callCount(), 1); + assert.deepStrictEqual(fn.mock.calls[0].arguments, args); + t.mock.timers.reset(); + }); + it('should keep setTimeout working if timers are disabled', (t, done) => { const now = Date.now(); const timeout = 2; @@ -80,6 +95,26 @@ describe('Mock Timers Test Suite', () => { assert.strictEqual(fn.mock.callCount(), 3); t.mock.timers.reset(); }); + + it('should work with the same params as the original setInterval', (t) => { + t.mock.timers.enable(); + const fn = t.mock.fn(); + const args = ['a', 'b', 'c']; + const id = global.setInterval(fn, 200, ...args); + + t.mock.timers.tick(200); + t.mock.timers.tick(200); + t.mock.timers.tick(200); + + global.clearInterval(id); + + assert.strictEqual(fn.mock.callCount(), 3); + assert.deepStrictEqual(fn.mock.calls[0].arguments, args); + assert.deepStrictEqual(fn.mock.calls[1].arguments, args); + assert.deepStrictEqual(fn.mock.calls[2].arguments, args); + + t.mock.timers.reset(); + }); }); describe('clearInterval Suite', () => { @@ -112,6 +147,21 @@ describe('Mock Timers Test Suite', () => { assert.strictEqual(fn.mock.callCount(), 1); }); + + it('should work with the same params as the original timers.setTimeout', (t) => { + t.mock.timers.enable(); + const fn = t.mock.fn(); + const { setTimeout } = nodeTimers; + const args = ['a', 'b', 'c']; + setTimeout(fn, 2000, ...args); + + t.mock.timers.tick(1000); + t.mock.timers.tick(500); + t.mock.timers.tick(500); + + assert.strictEqual(fn.mock.callCount(), 1); + assert.deepStrictEqual(fn.mock.calls[0].arguments, args); + }); }); describe('clearTimeout Suite', () => { @@ -146,6 +196,28 @@ describe('Mock Timers Test Suite', () => { assert.strictEqual(fn.mock.callCount(), 4); t.mock.timers.reset(); }); + + it('should work with the same params as the original timers.setInterval', (t) => { + t.mock.timers.enable(); + const fn = t.mock.fn(); + const args = ['a', 'b', 'c']; + const id = nodeTimers.setInterval(fn, 200, ...args); + + t.mock.timers.tick(200); + t.mock.timers.tick(200); + t.mock.timers.tick(200); + t.mock.timers.tick(200); + + nodeTimers.clearInterval(id); + + assert.strictEqual(fn.mock.callCount(), 4); + assert.deepStrictEqual(fn.mock.calls[0].arguments, args); + assert.deepStrictEqual(fn.mock.calls[1].arguments, args); + assert.deepStrictEqual(fn.mock.calls[2].arguments, args); + assert.deepStrictEqual(fn.mock.calls[3].arguments, args); + + t.mock.timers.reset(); + }); }); describe('clearInterval Suite', () => { @@ -181,6 +253,46 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.reset(); })); }); + + it('should work with the same params as the original timers/promises/setTimeout', async (t) => { + t.mock.timers.enable(); + const expectedResult = 'result'; + const controller = new AbortController(); + const p = nodeTimersPromises.setTimeout(2000, expectedResult, { + 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, expectedResult); + t.mock.timers.reset(); + }); + + it('should abort operation if timers/promises/setTimeout received an aborted signal', async (t) => { + t.mock.timers.enable(); + const expectedResult = 'result'; + const controller = new AbortController(); + const p = nodeTimersPromises.setTimeout(2000, expectedResult, { + 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', + }); + + t.mock.timers.reset(); + }); }); describe('setInterval Suite', () => { From bb752871196cd2b4ada69d1390084613b2908984 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Sun, 14 May 2023 23:25:35 +0200 Subject: [PATCH 21/57] test_runner: add test cases for abortController and remove listeners after fullfuling results Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 13 ++++++-- test/parallel/test-runner-mock-timers.js | 33 ++++++++++++++++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index c80ffca4fdc099..1d1fe9e4d04528 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -101,11 +101,18 @@ class MockTimers { return reject(abortIt(options.signal)); } } - const id = this.#setTimeout(() => resolve(result || id), ms); - options?.signal?.addEventListener('abort', () => { + + const onabort = () => { this.#clearTimeout(id); return reject(abortIt(options.signal)); - }); + }; + + const id = this.#setTimeout(() => { + options?.signal?.removeEventListener('abort', onabort); + return resolve(result || id); + }, ms); + + options?.signal?.addEventListener('abort', onabort); }); } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 3624919a33166a..7c589b0b318369 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -291,6 +291,39 @@ describe('Mock Timers Test Suite', () => { name: 'AbortError', }); + t.mock.timers.reset(); + }); + it('should abort operation even if the .tick wasn\'t called', async (t) => { + t.mock.timers.enable(); + const expectedResult = 'result'; + const controller = new AbortController(); + const p = nodeTimersPromises.setTimeout(2000, expectedResult, { + ref: true, + signal: controller.signal + }); + + controller.abort(); + + await assert.rejects(() => p, { + name: 'AbortError', + }); + + t.mock.timers.reset(); + }); + + it('should reject given an an invalid signal instance', async (t) => { + t.mock.timers.enable(); + const expectedResult = 'result'; + const p = nodeTimersPromises.setTimeout(2000, expectedResult, { + ref: true, + signal: {} + }); + + await assert.rejects(() => p, { + name: 'TypeError', + code: 'ERR_INVALID_ARG_TYPE' + }); + t.mock.timers.reset(); }); }); From b0baf008b307f416a20d5f9ea77e4bd2e96d1787 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 15 May 2023 18:47:34 -0300 Subject: [PATCH 22/57] test_runner: change import order Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 1d1fe9e4d04528..08bb26ba4c9656 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -7,8 +7,8 @@ const { const { DateNow, FunctionPrototypeBind, - globalThis, Promise, + globalThis, } = primordials; const { validateAbortSignal, From 92bf115ca31dd48f0553944ac359e376978ab3b3 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 15 May 2023 18:50:39 -0300 Subject: [PATCH 23/57] test_runner: add ArrayPrototypeAt instead of [].at Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 08bb26ba4c9656..277d50a04e8589 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -5,6 +5,7 @@ const { } = require('internal/util'); const { + ArrayPrototypeAt, DateNow, FunctionPrototypeBind, Promise, @@ -83,7 +84,7 @@ class MockTimers { this.#createTimer(true, callback, interval, ...args); for await (const events of EventEmitter.on(intervalEmitter, 'data')) { - yield events.at(0); + yield ArrayPrototypeAt(events, 0); } } From 1fe731e0ffa4d3dfda66811c56a01a1476530608 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 15 May 2023 20:18:17 -0300 Subject: [PATCH 24/57] test_runner: return safe asyncIterator Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 34 ++++++++++++++++---- test/parallel/test-runner-mock-timers.js | 4 +++ 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 277d50a04e8589..4ffbb4c6337a9c 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -9,6 +9,7 @@ const { DateNow, FunctionPrototypeBind, Promise, + SymbolAsyncIterator, globalThis, } = primordials; const { @@ -79,13 +80,32 @@ class MockTimers { } async * #setIntervalPromisified(interval, startTime, ...args) { - const intervalEmitter = new EventEmitter(); - const callback = () => intervalEmitter.emit('data', startTime); - this.#createTimer(true, callback, interval, ...args); - - for await (const events of EventEmitter.on(intervalEmitter, 'data')) { - yield ArrayPrototypeAt(events, 0); - } + const context = this; + const emitter = new EventEmitter(); + const eventIt = EventEmitter.on(emitter, 'data'); + const callback = () => emitter.emit('data', startTime); + const timerId = this.#createTimer(true, callback, interval, ...args); + + const iterator = { + [SymbolAsyncIterator]() { + return this; + }, + async next() { + const result = await eventIt.next(); + return { + __proto__: null, + done: result.done, + value: ArrayPrototypeAt(result.value, 0), + }; + }, + async return() { + emitter.removeAllListeners(); + context.#clearTimer(timerId); + return eventIt.return(); + }, + }; + + yield* iterator; } #setTimeoutPromisified(ms, result, options) { diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 7c589b0b318369..70c628ec100cbe 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -342,6 +342,7 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(interval); t.mock.timers.tick(interval); t.mock.timers.tick(interval); + t.mock.timers.tick(interval); const results = await Promise.all([ first, @@ -349,6 +350,9 @@ describe('Mock Timers Test Suite', () => { third, ]); + const finished = await intervalIterator.return(); + assert.deepStrictEqual(finished, { done: true, value: undefined }); + results.forEach((result) => { assert.strictEqual(typeof result.value, 'number'); assert.strictEqual(result.done, false); From 08f27ba015ded9bc1d152798c8e415275224e415 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 15 May 2023 20:31:48 -0300 Subject: [PATCH 25/57] test_runner: make promises.setInterval update time during iterations Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 5 ++++- test/parallel/test-runner-mock-timers.js | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 4ffbb4c6337a9c..59eef328186158 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -83,7 +83,10 @@ class MockTimers { const context = this; const emitter = new EventEmitter(); const eventIt = EventEmitter.on(emitter, 'data'); - const callback = () => emitter.emit('data', startTime); + const callback = () => { + startTime += interval; + emitter.emit('data', startTime); + }; const timerId = this.#createTimer(true, callback, interval, ...args); const iterator = { diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 70c628ec100cbe..76a36f64fdaedc 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -367,15 +367,15 @@ describe('Mock Timers Test Suite', () => { const expectedIterations = 5; const interval = 1000; - + const startedAt = Date.now(); async function run() { - const timers = []; - for await (const startTime of nodeTimersPromises.setInterval(interval, Date.now())) { - timers.push(startTime); - if (timers.length === expectedIterations) break; + const times = []; + for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) { + times.push(time); + if (times.length === expectedIterations) break; } - return timers; + return times; } const r = run(); @@ -385,8 +385,12 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(interval); t.mock.timers.tick(interval); - const timersResults = await r; - assert.strictEqual(timersResults.length, expectedIterations); + const timeResults = await r; + assert.strictEqual(timeResults.length, expectedIterations); + for (let it = 1; it < expectedIterations; it++) { + assert.strictEqual(timeResults[it - 1], startedAt + (interval * it)); + } + console.log('timers', timeResults); t.mock.timers.reset(); }); }); From 3e39782f95ca7937b52a68daa9dc41db2202a893 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Fri, 19 May 2023 15:48:32 -0300 Subject: [PATCH 26/57] test_runner: add __proto__ null while returning the object Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 59eef328186158..d8d5512f6570d5 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -90,6 +90,7 @@ class MockTimers { const timerId = this.#createTimer(true, callback, interval, ...args); const iterator = { + __proto__: null, [SymbolAsyncIterator]() { return this; }, From a563ce948d0baeffc84cb51c48bb5bb9fcdbe49b Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 30 May 2023 18:04:54 +0200 Subject: [PATCH 27/57] test_runner: make promises.setInterval work with abortControllers Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 36 +++++++++-- test/parallel/test-runner-mock-timers.js | 67 +++++++++++++++++++- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index d8d5512f6570d5..257a7c366b8b59 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -9,6 +9,7 @@ const { DateNow, FunctionPrototypeBind, Promise, + PromiseReject, SymbolAsyncIterator, globalThis, } = primordials; @@ -30,7 +31,7 @@ function compareTimersLists(a, b) { function setPosition(node, pos) { node.priorityQueuePosition = pos; } - +const abortIt = (signal) => new AbortError(undefined, { cause: signal.reason }); class MockTimers { #realSetTimeout; @@ -79,15 +80,35 @@ class MockTimers { this.#timers.removeAt(position); } - async * #setIntervalPromisified(interval, startTime, ...args) { + async * #setIntervalPromisified(interval, startTime, options) { const context = this; const emitter = new EventEmitter(); + if (options?.signal) { + try { + validateAbortSignal(options.signal, 'options.signal'); + } catch (err) { + return PromiseReject(err); + } + + if (options.signal?.aborted) { + return PromiseReject(abortIt(options.signal)); + } + + const onabort = (reason) => { + emitter.emit('data', { aborted: true, reason }); + options?.signal?.removeEventListener('abort', onabort); + }; + + options.signal?.addEventListener('abort', onabort); + } + const eventIt = EventEmitter.on(emitter, 'data'); const callback = () => { startTime += interval; emitter.emit('data', startTime); }; - const timerId = this.#createTimer(true, callback, interval, ...args); + + const timerId = this.#createTimer(true, callback, interval, options); const iterator = { __proto__: null, @@ -96,10 +117,16 @@ class MockTimers { }, async next() { const result = await eventIt.next(); + const value = ArrayPrototypeAt(result.value, 0); + if (value?.aborted) { + iterator.return(); + return PromiseReject(abortIt(options.signal)); + } + return { __proto__: null, done: result.done, - value: ArrayPrototypeAt(result.value, 0), + value, }; }, async return() { @@ -114,7 +141,6 @@ class MockTimers { #setTimeoutPromisified(ms, result, options) { return new Promise((resolve, reject) => { - const abortIt = (signal) => new AbortError(undefined, { cause: signal.reason }); if (options?.signal) { try { validateAbortSignal(options.signal, 'options.signal'); diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 76a36f64fdaedc..451d8e315c4e9a 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -360,7 +360,6 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.reset(); }); - it('should tick five times testing a real use case', async (t) => { t.mock.timers.enable(); @@ -390,7 +389,71 @@ describe('Mock Timers Test Suite', () => { for (let it = 1; it < expectedIterations; it++) { assert.strictEqual(timeResults[it - 1], startedAt + (interval * it)); } - console.log('timers', timeResults); + t.mock.timers.reset(); + }); + + it('should abort operation given an abort controller signal', async (t) => { + t.mock.timers.enable(); + + const interval = 100; + const abortController = new AbortController(); + const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now(), { + signal: abortController.signal + }); + + const first = intervalIterator.next(); + const second = intervalIterator.next(); + + t.mock.timers.tick(interval); + abortController.abort(); + t.mock.timers.tick(interval); + + const firstResult = await first; + assert.strictEqual(firstResult.value, Date.now() + interval); + assert.strictEqual(firstResult.done, false); + + await assert.rejects(() => second, { + name: 'AbortError', + }); + + t.mock.timers.reset(); + }); + + it('should abort operation given an abort controller signa on a real use case', async (t) => { + + t.mock.timers.enable(); + const controller = new AbortController(); + const signal = controller.signal; + const interval = 200; + const expectedIterations = 2; + const startedAt = Date.now(); + const timeResults = []; + async function run() { + const it = nodeTimersPromises.setInterval(interval, startedAt, { signal }); + for await (const time of it) { + timeResults.push(time); + if (timeResults.length === 5) break; + } + } + + const r = run(); + t.mock.timers.tick(interval); + t.mock.timers.tick(interval); + controller.abort(); + t.mock.timers.tick(interval); + t.mock.timers.tick(interval); + t.mock.timers.tick(interval); + t.mock.timers.tick(interval); + + await assert.rejects(() => r, { + name: 'AbortError', + }); + assert.strictEqual(timeResults.length, expectedIterations); + + for (let it = 1; it < expectedIterations; it++) { + assert.strictEqual(timeResults[it - 1], startedAt + (interval * it)); + } + t.mock.timers.reset(); }); }); From 17381f1b2109754fd9037357a221934e615611b3 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 30 May 2023 18:08:39 +0200 Subject: [PATCH 28/57] test_runner: rm unsafe array iteration and comments Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 257a7c366b8b59..bfed1c99207f2c 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -7,6 +7,7 @@ const { const { ArrayPrototypeAt, DateNow, + FunctionPrototypeApply, FunctionPrototypeBind, Promise, PromiseReject, @@ -172,26 +173,20 @@ class MockTimers { // throw new Error('you should enable fakeTimers first by calling the .enable function'); // } - // Increase the current time by the specified time. this.#now += time; - // Execute all timers whose runAt time is less than or equal to the current time. let timer = this.#timers.peek(); while (timer && timer.runAt <= this.#now) { - // Execute the timer's callback function with the specified arguments. - timer.callback(...timer.args); + FunctionPrototypeApply(timer.callback, undefined, timer.args); - // Remove the timer from the timer queue. this.#timers.shift(); - // If the timer is an interval timer, update its runAt time and re-insert it into the timer queue. if (timer.interval) { timer.runAt += timer.interval; this.#timers.insert(timer); return; } - // Get the next timer in the timer queue. timer = this.#timers.peek(); } From 36774821822d20100b391423b779d115c46df5e8 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 30 May 2023 18:35:02 +0200 Subject: [PATCH 29/57] test_runner: avoid avoid code repetition Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index bfed1c99207f2c..952fa07806c010 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -174,9 +174,9 @@ class MockTimers { // } this.#now += time; - - let timer = this.#timers.peek(); - while (timer && timer.runAt <= this.#now) { + let timer; + while (timer = this.#timers.peek()) { + if(!(timer.runAt <= this.#now)) break; FunctionPrototypeApply(timer.callback, undefined, timer.args); this.#timers.shift(); @@ -189,7 +189,6 @@ class MockTimers { timer = this.#timers.peek(); } - } enable() { @@ -199,6 +198,7 @@ class MockTimers { this.#now = DateNow(); this.#isEnabled = true; + this.#realSetTimeout = globalThis.setTimeout; this.#realClearTimeout = globalThis.clearTimeout; this.#realSetInterval = globalThis.setInterval; From 1825426feafb0668a08873f3b15a9fb8ecd7b05d Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 30 May 2023 18:40:15 +0200 Subject: [PATCH 30/57] test_runner: (rollback) avoid avoid code repetition Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 952fa07806c010..95ef5a89d4848b 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -174,9 +174,9 @@ class MockTimers { // } this.#now += time; - let timer; - while (timer = this.#timers.peek()) { - if(!(timer.runAt <= this.#now)) break; + let timer = this.#timers.peek(); + while (timer) { + if (!(timer.runAt <= this.#now)) break; FunctionPrototypeApply(timer.callback, undefined, timer.args); this.#timers.shift(); @@ -198,7 +198,7 @@ class MockTimers { this.#now = DateNow(); this.#isEnabled = true; - + this.#realSetTimeout = globalThis.setTimeout; this.#realClearTimeout = globalThis.clearTimeout; this.#realSetInterval = globalThis.setInterval; From abc7c969d41afc40a7b950f22f775b8cee7732a8 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 30 May 2023 18:42:33 +0200 Subject: [PATCH 31/57] test_runner: rm unecessarly PromiseRejection Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 95ef5a89d4848b..2d225853bff159 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -10,7 +10,6 @@ const { FunctionPrototypeApply, FunctionPrototypeBind, Promise, - PromiseReject, SymbolAsyncIterator, globalThis, } = primordials; @@ -85,14 +84,10 @@ class MockTimers { const context = this; const emitter = new EventEmitter(); if (options?.signal) { - try { - validateAbortSignal(options.signal, 'options.signal'); - } catch (err) { - return PromiseReject(err); - } + validateAbortSignal(options.signal, 'options.signal'); if (options.signal?.aborted) { - return PromiseReject(abortIt(options.signal)); + throw abortIt(options.signal); } const onabort = (reason) => { @@ -121,7 +116,7 @@ class MockTimers { const value = ArrayPrototypeAt(result.value, 0); if (value?.aborted) { iterator.return(); - return PromiseReject(abortIt(options.signal)); + throw abortIt(options.signal); } return { From 87b0d3680f20463d586dd4213d0bac9571e8ae71 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 5 Jun 2023 11:36:02 +0100 Subject: [PATCH 32/57] test_runner: add timers.reset on test postRun Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock.js | 1 + test/parallel/test-runner-mock-timers.js | 20 -------------------- 2 files changed, 1 insertion(+), 20 deletions(-) diff --git a/lib/internal/test_runner/mock/mock.js b/lib/internal/test_runner/mock/mock.js index f4064b81f0b319..71363154c88328 100644 --- a/lib/internal/test_runner/mock/mock.js +++ b/lib/internal/test_runner/mock/mock.js @@ -272,6 +272,7 @@ class MockTracker { reset() { this.restoreAll(); + this.#timers?.reset(); this.#mocks = []; } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 451d8e315c4e9a..f8612242f95902 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -10,7 +10,6 @@ const nodeTimersPromises = require('node:timers/promises'); describe('Mock Timers Test Suite', () => { describe('globals/timers', () => { describe('setTimeout Suite', () => { - it('should advance in time and trigger timers when calling the .tick function', (t) => { mock.timers.enable(); @@ -34,7 +33,6 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(500); assert.strictEqual(fn.mock.callCount(), 1); - t.mock.timers.reset(); }); it('should work with the same params as the original setTimeout', (t) => { @@ -49,7 +47,6 @@ describe('Mock Timers Test Suite', () => { assert.strictEqual(fn.mock.callCount(), 1); assert.deepStrictEqual(fn.mock.calls[0].arguments, args); - t.mock.timers.reset(); }); it('should keep setTimeout working if timers are disabled', (t, done) => { @@ -75,7 +72,6 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(4000); assert.strictEqual(fn.mock.callCount(), 0); - t.mock.timers.reset(); }); }); @@ -93,7 +89,6 @@ describe('Mock Timers Test Suite', () => { global.clearInterval(id); assert.strictEqual(fn.mock.callCount(), 3); - t.mock.timers.reset(); }); it('should work with the same params as the original setInterval', (t) => { @@ -113,7 +108,6 @@ describe('Mock Timers Test Suite', () => { assert.deepStrictEqual(fn.mock.calls[1].arguments, args); assert.deepStrictEqual(fn.mock.calls[2].arguments, args); - t.mock.timers.reset(); }); }); @@ -127,7 +121,6 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(200); assert.strictEqual(fn.mock.callCount(), 0); - t.mock.timers.reset(); }); }); @@ -175,7 +168,6 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(2000); assert.strictEqual(fn.mock.callCount(), 0); - t.mock.timers.reset(); }); }); @@ -194,7 +186,6 @@ describe('Mock Timers Test Suite', () => { nodeTimers.clearInterval(id); assert.strictEqual(fn.mock.callCount(), 4); - t.mock.timers.reset(); }); it('should work with the same params as the original timers.setInterval', (t) => { @@ -216,7 +207,6 @@ describe('Mock Timers Test Suite', () => { assert.deepStrictEqual(fn.mock.calls[2].arguments, args); assert.deepStrictEqual(fn.mock.calls[3].arguments, args); - t.mock.timers.reset(); }); }); @@ -231,7 +221,6 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(200); assert.strictEqual(fn.mock.callCount(), 0); - t.mock.timers.reset(); }); }); }); @@ -250,7 +239,6 @@ describe('Mock Timers Test Suite', () => { p.then(common.mustCall((result) => { assert.ok(result); - t.mock.timers.reset(); })); }); @@ -270,7 +258,6 @@ describe('Mock Timers Test Suite', () => { const result = await p; assert.strictEqual(result, expectedResult); - t.mock.timers.reset(); }); it('should abort operation if timers/promises/setTimeout received an aborted signal', async (t) => { @@ -291,7 +278,6 @@ describe('Mock Timers Test Suite', () => { name: 'AbortError', }); - t.mock.timers.reset(); }); it('should abort operation even if the .tick wasn\'t called', async (t) => { t.mock.timers.enable(); @@ -308,7 +294,6 @@ describe('Mock Timers Test Suite', () => { name: 'AbortError', }); - t.mock.timers.reset(); }); it('should reject given an an invalid signal instance', async (t) => { @@ -324,7 +309,6 @@ describe('Mock Timers Test Suite', () => { code: 'ERR_INVALID_ARG_TYPE' }); - t.mock.timers.reset(); }); }); @@ -358,7 +342,6 @@ describe('Mock Timers Test Suite', () => { assert.strictEqual(result.done, false); }); - t.mock.timers.reset(); }); it('should tick five times testing a real use case', async (t) => { @@ -389,7 +372,6 @@ describe('Mock Timers Test Suite', () => { for (let it = 1; it < expectedIterations; it++) { assert.strictEqual(timeResults[it - 1], startedAt + (interval * it)); } - t.mock.timers.reset(); }); it('should abort operation given an abort controller signal', async (t) => { @@ -416,7 +398,6 @@ describe('Mock Timers Test Suite', () => { name: 'AbortError', }); - t.mock.timers.reset(); }); it('should abort operation given an abort controller signa on a real use case', async (t) => { @@ -454,7 +435,6 @@ describe('Mock Timers Test Suite', () => { assert.strictEqual(timeResults[it - 1], startedAt + (interval * it)); } - t.mock.timers.reset(); }); }); From a001e65a44a0502c09adaa50ad662b51ba97e688 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 5 Jun 2023 13:28:11 +0100 Subject: [PATCH 33/57] test_runner: add config for choosing timers to use, error handling and refactoring Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 180 +++++++++++++------ test/parallel/test-runner-mock-timers.js | 76 +++++--- 2 files changed, 175 insertions(+), 81 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 2d225853bff159..8682e1a01e4299 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -6,6 +6,8 @@ const { const { ArrayPrototypeAt, + ArrayPrototypeForEach, + ArrayPrototypeIncludes, DateNow, FunctionPrototypeApply, FunctionPrototypeBind, @@ -15,10 +17,17 @@ const { } = primordials; const { validateAbortSignal, + validateArray, } = require('internal/validators'); + const { AbortError, + codes: { + ERR_INVALID_ARG_VALUE, + ERR_INVALID_STATE, + }, } = require('internal/errors'); + const PriorityQueue = require('internal/priority_queue'); const nodeTimers = require('timers'); const nodeTimersPromises = require('timers/promises'); @@ -31,7 +40,12 @@ function compareTimersLists(a, b) { function setPosition(node, pos) { node.priorityQueuePosition = pos; } -const abortIt = (signal) => new AbortError(undefined, { cause: signal.reason }); + +function abortIt(signal) { + return new AbortError(undefined, { cause: signal.reason }); +} + +const SUPPORTED_TIMERS = ['setTimeout', 'setInterval']; class MockTimers { #realSetTimeout; @@ -47,11 +61,12 @@ class MockTimers { #realTimersSetInterval; #realTimersClearInterval; + #timersInContext = []; #isEnabled = false; #currentTimer = 1; #now = DateNow(); - #timers = new PriorityQueue(compareTimersLists, setPosition); + #executionQueue = new PriorityQueue(compareTimersLists, setPosition); #setTimeout = FunctionPrototypeBind(this.#createTimer, this, false); #clearTimeout = FunctionPrototypeBind(this.#clearTimer, this); @@ -64,7 +79,7 @@ class MockTimers { #createTimer(isInterval, callback, delay, ...args) { const timerId = this.#currentTimer++; - this.#timers.insert({ + this.#executionQueue.insert({ __proto__: null, id: timerId, callback, @@ -77,7 +92,7 @@ class MockTimers { } #clearTimer(position) { - this.#timers.removeAt(position); + this.#executionQueue.removeAt(position); } async * #setIntervalPromisified(interval, startTime, options) { @@ -163,85 +178,134 @@ class MockTimers { }); } + #toggleEnableTimers(activate) { + const options = { + toFake: { + setTimeout: () => { + this.#realSetTimeout = globalThis.setTimeout; + this.#realClearTimeout = globalThis.clearTimeout; + this.#realTimersSetTimeout = nodeTimers.setTimeout; + this.#realTimersClearTimeout = nodeTimers.clearTimeout; + this.#realPromisifiedSetTimeout = nodeTimersPromises.setTimeout; + + globalThis.setTimeout = this.#setTimeout; + globalThis.clearTimeout = this.#clearTimeout; + + nodeTimers.setTimeout = this.#setTimeout; + nodeTimers.clearTimeout = this.#clearTimeout; + + nodeTimersPromises.setTimeout = FunctionPrototypeBind( + this.#setTimeoutPromisified, + this, + ); + }, + setInterval: () => { + this.#realSetInterval = globalThis.setInterval; + this.#realClearInterval = globalThis.clearInterval; + this.#realTimersSetInterval = nodeTimers.setInterval; + this.#realTimersClearInterval = nodeTimers.clearInterval; + this.#realPromisifiedSetInterval = nodeTimersPromises.setInterval; + + globalThis.setInterval = this.#setInterval; + globalThis.clearInterval = this.#clearInterval; + + nodeTimers.setInterval = this.#setInterval; + nodeTimers.clearInterval = this.#clearInterval; + + nodeTimersPromises.setInterval = FunctionPrototypeBind( + this.#setIntervalPromisified, + this, + ); + }, + }, + toReal: { + setTimeout: () => { + globalThis.setTimeout = this.#realSetTimeout; + globalThis.clearTimeout = this.#realClearTimeout; + + nodeTimers.setTimeout = this.#realTimersSetTimeout; + nodeTimers.clearTimeout = this.#realTimersClearTimeout; + + nodeTimersPromises.setTimeout = this.#realPromisifiedSetTimeout; + }, + setInterval: () => { + globalThis.setInterval = this.#realSetInterval; + globalThis.clearInterval = this.#realClearInterval; + + nodeTimers.setInterval = this.#realTimersSetInterval; + nodeTimers.clearInterval = this.#realTimersClearInterval; + + nodeTimersPromises.setInterval = this.#realPromisifiedSetInterval; + }, + }, + }; + + const target = activate ? options.toFake : options.toReal; + ArrayPrototypeForEach(this.#timersInContext, (timer) => target[timer]()); + this.#isEnabled = activate; + } + tick(time = 1) { - // if (!this.isEnabled) { - // throw new Error('you should enable fakeTimers first by calling the .enable function'); - // } + if (!this.#isEnabled) { + throw new ERR_INVALID_STATE( + 'You should enable MockTimers first by calling the .enable function', + ); + } this.#now += time; - let timer = this.#timers.peek(); + let timer = this.#executionQueue.peek(); while (timer) { if (!(timer.runAt <= this.#now)) break; FunctionPrototypeApply(timer.callback, undefined, timer.args); - this.#timers.shift(); + this.#executionQueue.shift(); if (timer.interval) { timer.runAt += timer.interval; - this.#timers.insert(timer); + this.#executionQueue.insert(timer); return; } - timer = this.#timers.peek(); + timer = this.#executionQueue.peek(); } } - enable() { - // if (this.isEnabled) { - // throw new Error('fakeTimers is already enabled!'); - // } - this.#now = DateNow(); - this.#isEnabled = true; - - - this.#realSetTimeout = globalThis.setTimeout; - this.#realClearTimeout = globalThis.clearTimeout; - this.#realSetInterval = globalThis.setInterval; - this.#realClearInterval = globalThis.clearInterval; - - this.#realTimersSetTimeout = nodeTimers.setTimeout; - this.#realTimersClearTimeout = nodeTimers.clearTimeout; - this.#realTimersSetInterval = nodeTimers.setInterval; - this.#realTimersClearInterval = nodeTimers.clearInterval; - - this.#realPromisifiedSetTimeout = nodeTimersPromises.setTimeout; - this.#realPromisifiedSetInterval = nodeTimersPromises.setInterval; + enable(timers = SUPPORTED_TIMERS) { + if (this.#isEnabled) { + throw new ERR_INVALID_STATE( + 'MockTimers is already enabled!', + ); + } - globalThis.setTimeout = this.#setTimeout; - globalThis.clearTimeout = this.#clearTimeout; - globalThis.setInterval = this.#setInterval; - globalThis.clearInterval = this.#clearInterval; + validateArray(timers, 'timers'); - nodeTimers.setTimeout = this.#setTimeout; - nodeTimers.clearTimeout = this.#clearTimeout; - nodeTimers.setInterval = this.#setInterval; - nodeTimers.clearInterval = this.#clearInterval; + // Check that the timers passed are supported + ArrayPrototypeForEach(timers, (timer) => { + if (!ArrayPrototypeIncludes(SUPPORTED_TIMERS, timer)) { + throw new ERR_INVALID_ARG_VALUE( + 'timers', + timer, + `option ${timer} is not supported`, + ); + } + }); - nodeTimersPromises.setTimeout = FunctionPrototypeBind(this.#setTimeoutPromisified, this); - nodeTimersPromises.setInterval = FunctionPrototypeBind(this.#setIntervalPromisified, this); + this.#timersInContext = timers; + this.#now = DateNow(); + this.#toggleEnableTimers(true); } reset() { - this.#isEnabled = false; - - // Restore the real timer functions - globalThis.setTimeout = this.#realSetTimeout; - globalThis.clearTimeout = this.#realClearTimeout; - globalThis.setInterval = this.#realSetInterval; - globalThis.clearInterval = this.#realClearInterval; - - nodeTimers.setTimeout = this.#realTimersSetTimeout; - nodeTimers.clearTimeout = this.#realTimersClearTimeout; - nodeTimers.setInterval = this.#realTimersSetInterval; - nodeTimers.clearInterval = this.#realTimersClearInterval; + // Ignore if not enabled + if (!this.#isEnabled) return; - nodeTimersPromises.setTimeout = this.#realPromisifiedSetTimeout; - nodeTimersPromises.setInterval = this.#realPromisifiedSetInterval; + this.#toggleEnableTimers(false); + this.#timersInContext = []; - let timer = this.#timers.peek(); + let timer = this.#executionQueue.peek(); while (timer) { - this.#timers.shift(); - timer = this.#timers.peek(); + this.#executionQueue.shift(); + timer = this.#executionQueue.peek(); } } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index f8612242f95902..bbb4c63f5ff4ce 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -8,10 +8,41 @@ const nodeTimers = require('node:timers'); const nodeTimersPromises = require('node:timers/promises'); describe('Mock Timers Test Suite', () => { + describe('MockTimers API', () => { + it('should throw an error if trying to enable a timer that is not supported', (t) => { + assert.throws(() => { + t.mock.timers.enable(['DOES_NOT_EXIST']); + }, { + code: 'ERR_INVALID_ARG_VALUE', + }); + }); + + it('should throw an error if trying to enable a timer twice', (t) => { + t.mock.timers.enable(); + assert.throws(() => { + t.mock.timers.enable(); + }, { + code: 'ERR_INVALID_STATE', + }); + }); + + it('should not throw if calling reset without enabling timers', (t) => { + t.mock.timers.reset(); + }); + + it('should throw an error if calling tick without enabling timers', (t) => { + assert.throws(() => { + t.mock.timers.tick(); + }, { + code: 'ERR_INVALID_STATE', + }); + }); + }); + describe('globals/timers', () => { describe('setTimeout Suite', () => { it('should advance in time and trigger timers when calling the .tick function', (t) => { - mock.timers.enable(); + mock.timers.enable(['setTimeout']); const fn = mock.fn(); @@ -23,7 +54,7 @@ describe('Mock Timers Test Suite', () => { }); it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const fn = t.mock.fn(); global.setTimeout(fn, 2000); @@ -36,7 +67,7 @@ describe('Mock Timers Test Suite', () => { }); it('should work with the same params as the original setTimeout', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const fn = t.mock.fn(); const args = ['a', 'b', 'c']; global.setTimeout(fn, 2000, ...args); @@ -63,7 +94,7 @@ describe('Mock Timers Test Suite', () => { describe('clearTimeout Suite', () => { it('should not advance in time if clearTimeout was invoked', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const fn = mock.fn(); @@ -77,7 +108,7 @@ describe('Mock Timers Test Suite', () => { describe('setInterval Suite', () => { it('should tick three times using fake setInterval', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const fn = t.mock.fn(); const id = global.setInterval(fn, 200); @@ -92,7 +123,7 @@ describe('Mock Timers Test Suite', () => { }); it('should work with the same params as the original setInterval', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const fn = t.mock.fn(); const args = ['a', 'b', 'c']; const id = global.setInterval(fn, 200, ...args); @@ -113,7 +144,7 @@ describe('Mock Timers Test Suite', () => { describe('clearInterval Suite', () => { it('should not advance in time if clearInterval was invoked', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const fn = mock.fn(); const id = global.setInterval(fn, 200); @@ -129,7 +160,7 @@ describe('Mock Timers Test Suite', () => { describe('timers Suite', () => { describe('setTimeout Suite', () => { it('should advance in time and trigger timers when calling the .tick function multiple times', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const fn = t.mock.fn(); const { setTimeout } = nodeTimers; setTimeout(fn, 2000); @@ -142,7 +173,7 @@ describe('Mock Timers Test Suite', () => { }); it('should work with the same params as the original timers.setTimeout', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const fn = t.mock.fn(); const { setTimeout } = nodeTimers; const args = ['a', 'b', 'c']; @@ -159,7 +190,7 @@ describe('Mock Timers Test Suite', () => { describe('clearTimeout Suite', () => { it('should not advance in time if clearTimeout was invoked', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const fn = mock.fn(); const { setTimeout, clearTimeout } = nodeTimers; @@ -173,7 +204,7 @@ describe('Mock Timers Test Suite', () => { describe('setInterval Suite', () => { it('should tick three times using fake setInterval', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const fn = t.mock.fn(); const id = nodeTimers.setInterval(fn, 200); @@ -189,7 +220,7 @@ describe('Mock Timers Test Suite', () => { }); it('should work with the same params as the original timers.setInterval', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const fn = t.mock.fn(); const args = ['a', 'b', 'c']; const id = nodeTimers.setInterval(fn, 200, ...args); @@ -212,7 +243,7 @@ describe('Mock Timers Test Suite', () => { describe('clearInterval Suite', () => { it('should not advance in time if clearInterval was invoked', (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const fn = mock.fn(); const { setInterval, clearInterval } = nodeTimers; @@ -228,7 +259,7 @@ describe('Mock Timers Test Suite', () => { describe('timers/promises', () => { describe('setTimeout Suite', () => { it('should advance in time and trigger timers when calling the .tick function multiple times', async (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const p = nodeTimersPromises.setTimeout(2000); @@ -243,7 +274,7 @@ describe('Mock Timers Test Suite', () => { }); it('should work with the same params as the original timers/promises/setTimeout', async (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const expectedResult = 'result'; const controller = new AbortController(); const p = nodeTimersPromises.setTimeout(2000, expectedResult, { @@ -261,7 +292,7 @@ describe('Mock Timers Test Suite', () => { }); it('should abort operation if timers/promises/setTimeout received an aborted signal', async (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const expectedResult = 'result'; const controller = new AbortController(); const p = nodeTimersPromises.setTimeout(2000, expectedResult, { @@ -280,7 +311,7 @@ describe('Mock Timers Test Suite', () => { }); it('should abort operation even if the .tick wasn\'t called', async (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const expectedResult = 'result'; const controller = new AbortController(); const p = nodeTimersPromises.setTimeout(2000, expectedResult, { @@ -297,7 +328,7 @@ describe('Mock Timers Test Suite', () => { }); it('should reject given an an invalid signal instance', async (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setTimeout']); const expectedResult = 'result'; const p = nodeTimersPromises.setTimeout(2000, expectedResult, { ref: true, @@ -314,7 +345,7 @@ describe('Mock Timers Test Suite', () => { describe('setInterval Suite', () => { it('should tick three times using fake setInterval', async (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const interval = 100; const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now()); @@ -345,7 +376,7 @@ describe('Mock Timers Test Suite', () => { }); it('should tick five times testing a real use case', async (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const expectedIterations = 5; const interval = 1000; @@ -375,7 +406,7 @@ describe('Mock Timers Test Suite', () => { }); it('should abort operation given an abort controller signal', async (t) => { - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const interval = 100; const abortController = new AbortController(); @@ -401,8 +432,7 @@ describe('Mock Timers Test Suite', () => { }); it('should abort operation given an abort controller signa on a real use case', async (t) => { - - t.mock.timers.enable(); + t.mock.timers.enable(['setInterval']); const controller = new AbortController(); const signal = controller.signal; const interval = 200; From d168614c89de8195d8bcd1029a04d2128ebaaea1 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 5 Jun 2023 13:46:23 +0100 Subject: [PATCH 34/57] test_runner: implement releaseAllTimers function Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 29 ++++++++++++++++- test/parallel/test-runner-mock-timers.js | 34 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 8682e1a01e4299..7ed0e06012e0cd 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -23,8 +23,9 @@ const { const { AbortError, codes: { - ERR_INVALID_ARG_VALUE, ERR_INVALID_STATE, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, }, } = require('internal/errors'); @@ -252,6 +253,14 @@ class MockTimers { ); } + if (time < 0) { + throw new ERR_INVALID_ARG_TYPE( + 'time', + 'positive integer', + time, + ); + } + this.#now += time; let timer = this.#executionQueue.peek(); while (timer) { @@ -310,7 +319,25 @@ class MockTimers { } releaseAllTimers() { + if (!this.#isEnabled) { + throw new ERR_INVALID_STATE( + 'You should enable MockTimers first by calling the .enable function', + ); + } + let timer = this.#executionQueue.peek(); + while (timer) { + FunctionPrototypeApply(timer.callback, undefined, timer.args); + this.#executionQueue.shift(); + + if (timer.interval) { + timer.runAt += timer.interval; + this.#executionQueue.insert(timer); + return; + } + + timer = this.#executionQueue.peek(); + } } } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index bbb4c63f5ff4ce..6d92cdaf73218d 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -37,6 +37,40 @@ describe('Mock Timers Test Suite', () => { code: 'ERR_INVALID_STATE', }); }); + + it('should throw an error if calling tick with a negative number', (t) => { + t.mock.timers.enable(); + assert.throws(() => { + t.mock.timers.tick(-1); + }, { + code: 'ERR_INVALID_ARG_TYPE', + }); + }); + + describe('releaseAllTimers Suite', () => { + it('should throw an error if calling releaseAllTimers without enabling timers', (t) => { + assert.throws(() => { + t.mock.timers.releaseAllTimers(); + }, { + code: 'ERR_INVALID_STATE', + }); + }); + + it('should trigger all timers when calling .releaseAllTimers function', async (t) => { + const timeoutFn = t.mock.fn(); + const intervalFn = t.mock.fn(); + + t.mock.timers.enable(); + global.setTimeout(timeoutFn, 1111); + const id = global.setInterval(intervalFn, 9999); + t.mock.timers.releaseAllTimers(); + + global.clearInterval(id); + assert.strictEqual(timeoutFn.mock.callCount(), 1); + assert.strictEqual(intervalFn.mock.callCount(), 1); + }); + }); + }); describe('globals/timers', () => { From 1e09a22a432e6e0a91017450b77684fdf8298f5a Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 5 Jun 2023 14:21:50 +0100 Subject: [PATCH 35/57] test_runner: reach 100% code coverage Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 1 - test/parallel/test-runner-mock-timers.js | 60 ++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 7ed0e06012e0cd..00c5b8e1746767 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -147,7 +147,6 @@ class MockTimers { return eventIt.return(); }, }; - yield* iterator; } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 6d92cdaf73218d..d2de259f58d031 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -47,6 +47,32 @@ describe('Mock Timers Test Suite', () => { }); }); + it('should reset all timers when calling .reset function', (t) => { + t.mock.timers.enable(); + const fn = t.mock.fn(); + global.setTimeout(fn, 1000); + t.mock.timers.reset(); + assert.throws(() => { + t.mock.timers.tick(1000); + }, { + code: 'ERR_INVALID_STATE', + }); + + assert.strictEqual(fn.mock.callCount(), 0); + }); + it('should execute in order if timeout is the same', (t) => { + t.mock.timers.enable(); + const order = []; + const fn1 = t.mock.fn(() => order.push('f1')); + const fn2 = t.mock.fn(() => order.push('f2')); + global.setTimeout(fn1, 1000); + global.setTimeout(fn2, 1000); + t.mock.timers.tick(1000); + assert.strictEqual(fn1.mock.callCount(), 1); + assert.strictEqual(fn2.mock.callCount(), 1); + assert.deepStrictEqual(order, ['f1', 'f2']); + }); + describe('releaseAllTimers Suite', () => { it('should throw an error if calling releaseAllTimers without enabling timers', (t) => { assert.throws(() => { @@ -361,6 +387,22 @@ describe('Mock Timers Test Suite', () => { }); + it('should abort operation when .abort is called before calling setInterval', async (t) => { + t.mock.timers.enable(['setTimeout']); + const expectedResult = 'result'; + const controller = new AbortController(); + controller.abort(); + const p = nodeTimersPromises.setTimeout(2000, expectedResult, { + 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(['setTimeout']); const expectedResult = 'result'; @@ -465,6 +507,24 @@ describe('Mock Timers Test Suite', () => { }); + it('should abort operation when .abort is called before calling setInterval', async (t) => { + t.mock.timers.enable(['setInterval']); + + const interval = 100; + const abortController = new AbortController(); + abortController.abort(); + const intervalIterator = nodeTimersPromises.setInterval(interval, Date.now(), { + signal: abortController.signal + }); + + const first = intervalIterator.next(); + t.mock.timers.tick(interval); + + await assert.rejects(() => first, { + name: 'AbortError', + }); + }); + it('should abort operation given an abort controller signa on a real use case', async (t) => { t.mock.timers.enable(['setInterval']); const controller = new AbortController(); From 89b489b2835128aafa178b6e13cf67b4ac0e55a6 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 6 Jun 2023 16:39:32 +0100 Subject: [PATCH 36/57] test_runner: rename function to runAll Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 16 ++-------------- test/parallel/test-runner-mock-timers.js | 10 +++++----- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 00c5b8e1746767..0761ac3e58155a 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -317,26 +317,14 @@ class MockTimers { } } - releaseAllTimers() { + runAll() { if (!this.#isEnabled) { throw new ERR_INVALID_STATE( 'You should enable MockTimers first by calling the .enable function', ); } - let timer = this.#executionQueue.peek(); - while (timer) { - FunctionPrototypeApply(timer.callback, undefined, timer.args); - this.#executionQueue.shift(); - - if (timer.interval) { - timer.runAt += timer.interval; - this.#executionQueue.insert(timer); - return; - } - - timer = this.#executionQueue.peek(); - } + this.tick(Infinity); } } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index d2de259f58d031..aa0289064c6814 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -73,23 +73,23 @@ describe('Mock Timers Test Suite', () => { assert.deepStrictEqual(order, ['f1', 'f2']); }); - describe('releaseAllTimers Suite', () => { - it('should throw an error if calling releaseAllTimers without enabling timers', (t) => { + describe('runAll Suite', () => { + it('should throw an error if calling runAll without enabling timers', (t) => { assert.throws(() => { - t.mock.timers.releaseAllTimers(); + t.mock.timers.runAll(); }, { code: 'ERR_INVALID_STATE', }); }); - it('should trigger all timers when calling .releaseAllTimers function', async (t) => { + it('should trigger all timers when calling .runAll function', async (t) => { const timeoutFn = t.mock.fn(); const intervalFn = t.mock.fn(); t.mock.timers.enable(); global.setTimeout(timeoutFn, 1111); const id = global.setInterval(intervalFn, 9999); - t.mock.timers.releaseAllTimers(); + t.mock.timers.runAll(); global.clearInterval(id); assert.strictEqual(timeoutFn.mock.callCount(), 1); From 2b64a4ad701c3183aa9bc94cd8359873f939758e Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Thu, 8 Jun 2023 21:23:58 +0100 Subject: [PATCH 37/57] fix flaky test Signed-off-by: Erick Wendel --- test/parallel/test-runner-mock-timers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index aa0289064c6814..b59b460833359b 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -498,7 +498,8 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(interval); const firstResult = await first; - assert.strictEqual(firstResult.value, Date.now() + interval); + // interval * 2 because value can be a little bit greater than interval + assert.ok(firstResult.value < Date.now() + interval * 2); assert.strictEqual(firstResult.done, false); await assert.rejects(() => second, { From a9ed28db8279509bda61322437dacc287fdc5b65 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Thu, 8 Jun 2023 22:13:02 +0100 Subject: [PATCH 38/57] add abort once flag Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 20 +++++++++++++------- test/parallel/test-runner-mock-timers.js | 2 ++ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 0761ac3e58155a..0afd5bdc8a61f7 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -108,10 +108,12 @@ class MockTimers { const onabort = (reason) => { emitter.emit('data', { aborted: true, reason }); - options?.signal?.removeEventListener('abort', onabort); }; - options.signal?.addEventListener('abort', onabort); + options.signal?.addEventListener('abort', onabort, { + __proto__: null, + once: true, + }); } const eventIt = EventEmitter.on(emitter, 'data'); @@ -121,7 +123,10 @@ class MockTimers { }; const timerId = this.#createTimer(true, callback, interval, options); - + const clearListeners = () => { + emitter.removeAllListeners(); + context.#clearTimer(timerId); + }; const iterator = { __proto__: null, [SymbolAsyncIterator]() { @@ -142,8 +147,7 @@ class MockTimers { }; }, async return() { - emitter.removeAllListeners(); - context.#clearTimer(timerId); + clearListeners(); return eventIt.return(); }, }; @@ -170,11 +174,13 @@ class MockTimers { }; const id = this.#setTimeout(() => { - options?.signal?.removeEventListener('abort', onabort); return resolve(result || id); }, ms); - options?.signal?.addEventListener('abort', onabort); + options?.signal?.addEventListener('abort', onabort, { + __proto__: null, + once: true, + }); }); } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index b59b460833359b..0339bd0f022e7f 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -60,6 +60,7 @@ describe('Mock Timers Test Suite', () => { assert.strictEqual(fn.mock.callCount(), 0); }); + it('should execute in order if timeout is the same', (t) => { t.mock.timers.enable(); const order = []; @@ -561,6 +562,7 @@ describe('Mock Timers Test Suite', () => { } }); + }); }); From b383a1c7c199d3890fe2462abb652d90d5c6399d Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Thu, 8 Jun 2023 22:17:57 +0100 Subject: [PATCH 39/57] check that timeout will only be called when reaches the specified timeout Signed-off-by: Erick Wendel --- test/parallel/test-runner-mock-timers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 0339bd0f022e7f..bc43369d818854 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -121,9 +121,10 @@ describe('Mock Timers Test Suite', () => { global.setTimeout(fn, 2000); t.mock.timers.tick(1000); + assert.strictEqual(fn.mock.callCount(), 0); t.mock.timers.tick(500); + assert.strictEqual(fn.mock.callCount(), 0); t.mock.timers.tick(500); - assert.strictEqual(fn.mock.callCount(), 1); }); From ba725f40b4c51563dc56d09898189313bc69b366 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Thu, 8 Jun 2023 22:19:03 +0100 Subject: [PATCH 40/57] fix lint problems Signed-off-by: Erick Wendel --- lib/internal/test_runner/mock/mock_timers.js | 4 ++-- test/parallel/test-runner-mock-timers.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 0afd5bdc8a61f7..130320619ce6aa 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -110,9 +110,9 @@ class MockTimers { emitter.emit('data', { aborted: true, reason }); }; - options.signal?.addEventListener('abort', onabort, { + options.signal?.addEventListener('abort', onabort, { __proto__: null, - once: true, + once: true, }); } diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index bc43369d818854..9a4b03d7b66ca8 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -500,7 +500,7 @@ describe('Mock Timers Test Suite', () => { t.mock.timers.tick(interval); const firstResult = await first; - // interval * 2 because value can be a little bit greater than interval + // Interval * 2 because value can be a little bit greater than interval assert.ok(firstResult.value < Date.now() + interval * 2); assert.strictEqual(firstResult.done, false); From 1bcbe65a166d9ccf06486c61acdd488bff7e3af6 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 12 Jun 2023 14:37:55 -0300 Subject: [PATCH 41/57] test_runner: add initial doc Signed-off-by: Erick Wendel --- doc/api/test.md | 113 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/doc/api/test.md b/doc/api/test.md index 61658b886cd656..227a62a95d239a 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -514,7 +514,110 @@ test('spies on an object method', (t) => { assert.strictEqual(call.this, number); }); ``` +## Mocking Timers +Mocking timers is a technique commonly used in software testing to simulate and +control the behavior of timers, such as `setInterval` and `setTimeout`, +without actually waiting for the specified time intervals. + +Look at [`MockTimers`][] class to check out all methods +and supported features from this API. + +This allows developers to write more reliable and +predictable tests for time-dependent functionality. + +The example below shows how to mock `setTimeout`. +Using `.enable(['setTimeout']);` +it'll mock the `setTimeout` from both `node:timers`, +`node:timers/promises` modules, and from the Node.js global context. + +```mjs +import assert from 'node:assert'; +import { mock, test } from 'node:test'; + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', () => { + const fn = mock.fn(); + + // Optionally choose what to mock + mock.timers.enable(['setTimeout']); + setTimeout(() => fn(), 9999); + assert.strictEqual(fn.mock.callCount(), 0); + + // Advance in time + mock.timers.tick(9999); + assert.strictEqual(fn.mock.callCount(), 1); + + // Reset the globally tracked mocks. + mock.timers.reset(); + + // If you call reset mock instance, it'll also reset timers instance + mock.reset(); +}); +``` +```js +const assert = require('node:assert'); +const { mock, test } = require('node:test'); + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', () => { + const fn = mock.fn(); + + // Optionally choose what to mock + mock.timers.enable(['setTimeout']); + setTimeout(() => fn(), 9999); + assert.strictEqual(fn.mock.callCount(), 0); + + // Advance in time + mock.timers.tick(9999); + assert.strictEqual(fn.mock.callCount(), 1); + + // Reset the globally tracked mocks. + mock.timers.reset(); + + // If you call reset mock instance, it'll also reset timers instance + mock.reset(); +}); +``` + +The same mocking functionality is also exposed in the mock object on [`TestContext`][] object +of each test. The benefit of mocking via the test context is +that the test runner will automatically restore all mocked timers +functionality once the test finishes. + +```mjs +import assert from 'node:assert'; +import { test } from 'node:test'; + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + const fn = context.mock.fn(); + + // Optionally choose what to mock + context.mock.timers.enable(['setTimeout']); + setTimeout(() => fn(), 9999); + assert.strictEqual(fn.mock.callCount(), 0); + + // Advance in time + context.mock.timers.tick(9999); + assert.strictEqual(fn.mock.callCount(), 1); +}); +``` + +```js +const assert = require('node:assert'); +const { test } = require('node:test'); + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + const fn = context.mock.fn(); + + // Optionally choose what to mock + context.mock.timers.enable(['setTimeout']); + setTimeout(() => fn(), 9999); + assert.strictEqual(fn.mock.callCount(), 0); + + // Advance in time + context.mock.timers.tick(9999); + assert.strictEqual(fn.mock.callCount(), 1); +}); +``` ## Test reporters + +> Stability: 1 - Experimental + ## Class: `TestsStream` + +Enables timer mocking for the specified timers. + +* `timers` {Array} An optional array containing the timers to mock. +The currently supported timer values are `'setInterval'` and `'setTimeout'`. +If no array is provided, all timers (`'setInterval'`, `'clearInterval'`, `'setTimeout'`, +and `'clearTimeout'`) will be mocked by default. + +**Note:** When you enable mocking for a specific timer, its associated +clear function will also be implicitly mocked. + +Example usage: + +```mjs +import { mock } from 'node:test'; +mock.timers.enable(['setInterval']); +``` +```js +const { mock } = require('node:test'); +mock.timers.enable(['setInterval']); +``` + +The above example enables mocking for the `setInterval` timer and +implicitly mocks the `clearInterval` function. Only the `setInterval` + and `clearInterval` functions from [node:timers](./timers.md), +[node:timers/promises](./timers.md#timers-promises-api), and +`globalThis` will be mocked. + +Alternatively, if you call `mock.timers.enable()` without any parameters: + +All timers (`'setInterval'`, `'clearInterval'`, `'setTimeout'`, and `'clearTimeout'`) +will be mocked. The `setInterval`, `clearInterval`, `setTimeout`, and `clearTimeout` +functions from `node:timers`, `node:timers/promises`, +and `globalThis` will be mocked. + +### `timers.reset()` + + +This function restores the default behavior of all mocks that were previously +created by this `MockTimers` instance and disassociates the mocks +from the `MockTracker` instance. + +**Note:** After each test completes, this function is called on +the test context's `MockTracker`. + +```mjs +import { mock } from 'node:test'; +mock.timers.reset(); +``` +```js +const { mock } = require('node:test'); +mock.timers.reset(); +``` + +### `timers.tick(milliseconds)` + +Advances time for all mocked timers. + +* `milliseconds` {number} The amount of time, in milliseconds, +to advance the timers. + +**Note:** This diverges from how `setTimeout` in Node.js behaves and accepts +only positive numbers. In Node.js, `setTimeout` with negative numbers is +only supported for web compatibility reasons. + +The following example mocks a `setTimeout` function and +by using `.tick` advances in +time triggering all pending timers. + +```mjs +import assert from 'node:assert'; +import { test } from 'node:test'; + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + const fn = context.mock.fn(); + + context.mock.timers.enable(['setTimeout']); + + setTimeout(fn, 9999); + + assert.strictEqual(fn.mock.callCount(), 0); + + // Advance in time + context.mock.timers.tick(9999); + + assert.strictEqual(fn.mock.callCount(), 1); +}); +``` +```js +const assert = require('node:assert'); +const { test } = require('node:test'); + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + const fn = context.mock.fn(); + context.mock.timers.enable(['setTimeout']); + + setTimeout(fn, 9999); + assert.strictEqual(fn.mock.callCount(), 0); + + // Advance in time + context.mock.timers.tick(9999); + + assert.strictEqual(fn.mock.callCount(), 1); +}); +``` +Alternativelly, the `.tick` function can be called many times + +```mjs +import assert from 'node:assert'; +import { test } from 'node:test'; + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + const fn = context.mock.fn(); + context.mock.timers.enable(['setTimeout']); + const nineSecs = 9000; + setTimeout(fn, nineSecs); + + const twoSeconds = 3000; + context.mock.timers.tick(twoSeconds); + context.mock.timers.tick(twoSeconds); + context.mock.timers.tick(twoSeconds); + + assert.strictEqual(fn.mock.callCount(), 1); +}); +``` + +```js +const assert = require('node:assert'); +const { test } = require('node:test'); + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + const fn = context.mock.fn(); + context.mock.timers.enable(['setTimeout']); + const nineSecs = 9000; + setTimeout(fn, nineSecs); + + const twoSeconds = 3000; + context.mock.timers.tick(twoSeconds); + context.mock.timers.tick(twoSeconds); + context.mock.timers.tick(twoSeconds); + + assert.strictEqual(fn.mock.callCount(), 1); +}); +``` + +#### Using Clear functions + +As mentioned, all clear functions from timers (clearTimeout and clearInterval) +are implicity mocked.Take a look at this example using `setTimeout`: + +```mjs +import assert from 'node:assert'; +import { test } from 'node:test'; + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + const fn = context.mock.fn(); + + // Optionally choose what to mock + context.mock.timers.enable(['setTimeout']); + const id = setTimeout(fn, 9999); + + // Implicity mocked as well + clearTimeout(id); + context.mock.timers.tick(9999); + + // As that setTimeout was cleared the mock function will never be called + assert.strictEqual(fn.mock.callCount(), 0); +}); +``` + +```js +const assert = require('node:assert'); +const { test } = require('node:test'); + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => { + const fn = context.mock.fn(); + + // Optionally choose what to mock + context.mock.timers.enable(['setTimeout']); + const id = setTimeout(fn, 9999); + + // Implicity mocked as well + clearTimeout(id); + context.mock.timers.tick(9999); + + // As that setTimeout was cleared the mock function will never be called + assert.strictEqual(fn.mock.callCount(), 0); +}); +``` +#### Working with Node.js timers modules + +Once you enable mocking timers, [node:timers](./timers.md), +[node:timers/promises](./timers.md#timers-promises-api) modules, +and timers from the Node.js global context are enabled: + +```mjs +import assert from 'node:assert'; +import { test } from 'node:test'; +import nodeTimers from 'node:timers'; +import nodeTimersPromises from 'node:timers/promises'; + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', async (context) => { + const globalTimeoutObjectSpy = context.mock.fn(); + const nodeTimerSpy = context.mock.fn(); + const nodeTimerPromiseSpy = context.mock.fn(); + + // Optionally choose what to mock + context.mock.timers.enable(['setTimeout']); + setTimeout(globalTimeoutObjectSpy, 9999); + nodeTimers.setTimeout(nodeTimerSpy, 9999); + + const promise = nodeTimersPromises.setTimeout(9999).then(nodeTimerPromiseSpy); + + // Advance in time + context.mock.timers.tick(9999); + assert.strictEqual(globalTimeoutObjectSpy.mock.callCount(), 1); + assert.strictEqual(nodeTimerSpy.mock.callCount(), 1); + await promise; + assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1); +}); +``` +```js +const assert = require('node:assert'); +const { test } = require('node:test'); +const nodeTimers = require('node:timers'); +const nodeTimersPromises = require('node:timers/promises'); + +test('mocks setTimeout to be executed synchronously without having to actually wait for it', async (context) => { + const globalTimeoutObjectSpy = context.mock.fn(); + const nodeTimerSpy = context.mock.fn(); + const nodeTimerPromiseSpy = context.mock.fn(); + + // Optionally choose what to mock + context.mock.timers.enable(['setTimeout']); + setTimeout(globalTimeoutObjectSpy, 9999); + nodeTimers.setTimeout(nodeTimerSpy, 9999); + + const promise = nodeTimersPromises.setTimeout(9999).then(nodeTimerPromiseSpy); + + // Advance in time + context.mock.timers.tick(9999); + assert.strictEqual(globalTimeoutObjectSpy.mock.callCount(), 1); + assert.strictEqual(nodeTimerSpy.mock.callCount(), 1); + await promise; + assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1); +}); +``` +In Node.js, `setInterval` from [node:timers/promises](./timers.md#timers-promises-api) + is a Async Gerator and is also supported by this API: + +```mjs +import assert from 'node:assert'; +import { test } from 'node:test'; +import nodeTimersPromises from 'node:timers/promises'; +test('should tick five times testing a real use case', async (context) => { + + context.mock.timers.enable(['setInterval']); + + const expectedIterations = 3; + const interval = 1000; + const startedAt = Date.now(); + async function run() { + const times = []; + for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) { + times.push(time); + if (times.length === expectedIterations) break; + } + return times; + } + + const r = run(); + context.mock.timers.tick(interval); + context.mock.timers.tick(interval); + context.mock.timers.tick(interval); + + const timeResults = await r; + assert.strictEqual(timeResults.length, expectedIterations); + for (let it = 1; it < expectedIterations; it++) { + assert.strictEqual(timeResults[it - 1], startedAt + (interval * it)); + } +}); +``` +```js +const assert = require('node:assert'); +const { test } = require('node:test'); +const nodeTimersPromises = require('node:timers/promises'); +test('should tick five times testing a real use case', async (context) => { + + context.mock.timers.enable(['setInterval']); + + const expectedIterations = 3; + const interval = 1000; + const startedAt = Date.now(); + async function run() { + const times = []; + for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) { + times.push(time); + if (times.length === expectedIterations) break; + } + return times; + } + + const r = run(); + context.mock.timers.tick(interval); + context.mock.timers.tick(interval); + context.mock.timers.tick(interval); + + const timeResults = await r; + assert.strictEqual(timeResults.length, expectedIterations); + for (let it = 1; it < expectedIterations; it++) { + assert.strictEqual(timeResults[it - 1], startedAt + (interval * it)); + } +}); +``` + +### `timers.runAll()` + + ## Class: `TestsStream` +Triggers all pending mocked timers immediately. + +The example below triggers all pending timers immediately, +causing them to execute without any delay. +```mjs +import assert from 'node:assert'; +import { test } from 'node:test'; + +test('runAll functions following the given order', (context) => { + + context.mock.timers.enable(['setTimeout']); + const results = []; + setTimeout(() => results.push(1), 9999); + + // Notice that if both timers have the same timeout, + // the order of execution is guaranteed + setTimeout(() => results.push(3), 8888); + setTimeout(() => results.push(2), 8888); + + assert.deepStrictEqual(results, []); + + context.mock.timers.runAll(); + + assert.deepStrictEqual(results, [3, 2, 1]); +}); +``` +```js +const assert = require('node:assert'); +const { test } = require('node:test'); + +test('runAll functions following the given order', (context) => { + + context.mock.timers.enable(['setTimeout']); + const results = []; + setTimeout(() => results.push(1), 9999); + + // Notice that if both timers have the same timeout, + // the order of execution is guaranteed + setTimeout(() => results.push(3), 8888); + setTimeout(() => results.push(2), 8888); + + assert.deepStrictEqual(results, []); + + context.mock.timers.runAll(); + + assert.deepStrictEqual(results, [3, 2, 1]); +}); +``` + +**Note:** The `runAll()` function is specifically designed for +triggering timers in the context of timer mocking. +It does not have any effect on real-time system +clocks or actual timers outside of the mocking environment. ## Class: `TestsStream` From a75ddb41aff6cfee630c32045d931e5b8cfba497 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Mon, 12 Jun 2023 16:30:20 -0300 Subject: [PATCH 47/57] doc rm empty spaces Signed-off-by: Erick Wendel --- doc/api/test.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 4b60d245116d46..09d8c13027bffc 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -1779,7 +1779,6 @@ import assert from 'node:assert'; import { test } from 'node:test'; import nodeTimersPromises from 'node:timers/promises'; test('should tick five times testing a real use case', async (context) => { - context.mock.timers.enable(['setInterval']); const expectedIterations = 3; @@ -1811,7 +1810,6 @@ const assert = require('node:assert'); const { test } = require('node:test'); const nodeTimersPromises = require('node:timers/promises'); test('should tick five times testing a real use case', async (context) => { - context.mock.timers.enable(['setInterval']); const expectedIterations = 3; @@ -1853,7 +1851,6 @@ import assert from 'node:assert'; import { test } from 'node:test'; test('runAll functions following the given order', (context) => { - context.mock.timers.enable(['setTimeout']); const results = []; setTimeout(() => results.push(1), 9999); @@ -1875,7 +1872,6 @@ const assert = require('node:assert'); const { test } = require('node:test'); test('runAll functions following the given order', (context) => { - context.mock.timers.enable(['setTimeout']); const results = []; setTimeout(() => results.push(1), 9999); From 1bee321821d4bca491c810ba4ff0908f57a3f3b0 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 13 Jun 2023 09:59:24 -0300 Subject: [PATCH 48/57] Update test/parallel/test-runner-mock-timers.js Co-authored-by: Moshe Atlow --- test/parallel/test-runner-mock-timers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/parallel/test-runner-mock-timers.js b/test/parallel/test-runner-mock-timers.js index 9a4b03d7b66ca8..a4589d71773653 100644 --- a/test/parallel/test-runner-mock-timers.js +++ b/test/parallel/test-runner-mock-timers.js @@ -528,7 +528,7 @@ describe('Mock Timers Test Suite', () => { }); }); - it('should abort operation given an abort controller signa on a real use case', async (t) => { + it('should abort operation given an abort controller signal on a real use case', async (t) => { t.mock.timers.enable(['setInterval']); const controller = new AbortController(); const signal = controller.signal; From bc8fea7684fd6222a2f81d48dd6abe4c11167d9b Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 13 Jun 2023 09:59:36 -0300 Subject: [PATCH 49/57] Update lib/internal/test_runner/mock/mock_timers.js Co-authored-by: Jordan Harband --- lib/internal/test_runner/mock/mock_timers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 130320619ce6aa..7ac6612cb4174a 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -107,7 +107,7 @@ class MockTimers { } const onabort = (reason) => { - emitter.emit('data', { aborted: true, reason }); + emitter.emit('data', { __proto__: null, aborted: true, reason }); }; options.signal?.addEventListener('abort', onabort, { From 1bfd7b8d6416ad430f6eb292d227496867da1616 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 13 Jun 2023 10:04:41 -0300 Subject: [PATCH 50/57] format test.md Signed-off-by: Erick Wendel --- doc/api/test.md | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 09d8c13027bffc..34c6ee830dd770 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -514,6 +514,7 @@ test('spies on an object method', (t) => { assert.strictEqual(call.this, number); }); ``` + ### Timers Mocking timers is a technique commonly used in software testing to simulate and @@ -555,6 +556,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w mock.reset(); }); ``` + ```js const assert = require('node:assert'); const { mock, test } = require('node:test'); @@ -619,6 +621,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w assert.strictEqual(fn.mock.callCount(), 1); }); ``` + ## Test reporters + Advances time for all mocked timers. * `milliseconds` {number} The amount of time, in milliseconds, -to advance the timers. + to advance the timers. **Note:** This diverges from how `setTimeout` in Node.js behaves and accepts only positive numbers. In Node.js, `setTimeout` with negative numbers is @@ -1612,6 +1621,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w assert.strictEqual(fn.mock.callCount(), 1); }); ``` + ```js const assert = require('node:assert'); const { test } = require('node:test'); @@ -1629,6 +1639,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w assert.strictEqual(fn.mock.callCount(), 1); }); ``` + Alternativelly, the `.tick` function can be called many times ```mjs @@ -1713,6 +1724,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w assert.strictEqual(fn.mock.callCount(), 0); }); ``` + #### Working with Node.js timers modules Once you enable mocking timers, [node:timers](./timers.md), @@ -1745,6 +1757,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1); }); ``` + ```js const assert = require('node:assert'); const { test } = require('node:test'); @@ -1771,8 +1784,9 @@ test('mocks setTimeout to be executed synchronously without having to actually w assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1); }); ``` + In Node.js, `setInterval` from [node:timers/promises](./timers.md#timers-promises-api) - is a Async Gerator and is also supported by this API: +is a Async Gerator and is also supported by this API: ```mjs import assert from 'node:assert'; @@ -1805,6 +1819,7 @@ test('should tick five times testing a real use case', async (context) => { } }); ``` + ```js const assert = require('node:assert'); const { test } = require('node:test'); @@ -1838,14 +1853,17 @@ test('should tick five times testing a real use case', async (context) => { ``` ### `timers.runAll()` + + Triggers all pending mocked timers immediately. The example below triggers all pending timers immediately, causing them to execute without any delay. + ```mjs import assert from 'node:assert'; import { test } from 'node:test'; @@ -1867,6 +1885,7 @@ test('runAll functions following the given order', (context) => { assert.deepStrictEqual(results, [3, 2, 1]); }); ``` + ```js const assert = require('node:assert'); const { test } = require('node:test'); From d2c9a1abe05ae9b6b7866c5fab31af2fcbad52a2 Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 13 Jun 2023 15:49:06 -0300 Subject: [PATCH 51/57] add 1ms to tick to fix flakyness --- lib/internal/test_runner/mock/mock_timers.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 7ac6612cb4174a..e36d76467e315d 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -266,7 +266,8 @@ class MockTimers { ); } - this.#now += time; + // Add 1ms to fix flaky tests + this.#now += (time += 1); let timer = this.#executionQueue.peek(); while (timer) { if (!(timer.runAt <= this.#now)) break; From 93950f5b5ef9cf3e8648444136cfdcf52ec692cf Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 13 Jun 2023 16:16:24 -0300 Subject: [PATCH 52/57] test_runner: add 50ms to .tick so setInterval will not be flaky Signed-off-by: Erick Wendel --- doc/api/test.md | 5 ++++- lib/internal/test_runner/mock/mock_timers.js | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 34c6ee830dd770..26e08752c49abb 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -1594,7 +1594,10 @@ Advances time for all mocked timers. * `milliseconds` {number} The amount of time, in milliseconds, to advance the timers. -**Note:** This diverges from how `setTimeout` in Node.js behaves and accepts +**Note:** Everytime `.tick` is called by default it advances +50ms in the future plus the time you've specified to prevent flaky tests. + +This diverges from how `setTimeout` in Node.js behaves and accepts only positive numbers. In Node.js, `setTimeout` with negative numbers is only supported for web compatibility reasons. diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index e36d76467e315d..9ce80c8e2dfeb6 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -266,8 +266,8 @@ class MockTimers { ); } - // Add 1ms to fix flaky tests - this.#now += (time += 1); + // Add 50ms to fix flaky tests + this.#now += time + 50; let timer = this.#executionQueue.peek(); while (timer) { if (!(timer.runAt <= this.#now)) break; From 49e8eba439e36ab47b027fbb289a11b48b504d3f Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Tue, 13 Jun 2023 16:19:11 -0300 Subject: [PATCH 53/57] test_runner: revert last commit Signed-off-by: Erick Wendel --- doc/api/test.md | 5 +---- lib/internal/test_runner/mock/mock_timers.js | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 26e08752c49abb..34c6ee830dd770 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -1594,10 +1594,7 @@ Advances time for all mocked timers. * `milliseconds` {number} The amount of time, in milliseconds, to advance the timers. -**Note:** Everytime `.tick` is called by default it advances -50ms in the future plus the time you've specified to prevent flaky tests. - -This diverges from how `setTimeout` in Node.js behaves and accepts +**Note:** This diverges from how `setTimeout` in Node.js behaves and accepts only positive numbers. In Node.js, `setTimeout` with negative numbers is only supported for web compatibility reasons. diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index 9ce80c8e2dfeb6..e36d76467e315d 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -266,8 +266,8 @@ class MockTimers { ); } - // Add 50ms to fix flaky tests - this.#now += time + 50; + // Add 1ms to fix flaky tests + this.#now += (time += 1); let timer = this.#executionQueue.peek(); while (timer) { if (!(timer.runAt <= this.#now)) break; From 4d3a45cd1c54eed95c7cc145a9bc95484da5cf07 Mon Sep 17 00:00:00 2001 From: Moshe Atlow Date: Tue, 13 Jun 2023 22:50:52 +0300 Subject: [PATCH 54/57] =?UTF-8?q?founde=20the=20bug=20=F0=9F=90=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/internal/test_runner/mock/mock_timers.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/internal/test_runner/mock/mock_timers.js b/lib/internal/test_runner/mock/mock_timers.js index e36d76467e315d..3488a8259d1ae4 100644 --- a/lib/internal/test_runner/mock/mock_timers.js +++ b/lib/internal/test_runner/mock/mock_timers.js @@ -84,7 +84,7 @@ class MockTimers { __proto__: null, id: timerId, callback, - runAt: DateNow() + delay, + runAt: this.#now + delay, interval: isInterval, args, }); @@ -266,8 +266,7 @@ class MockTimers { ); } - // Add 1ms to fix flaky tests - this.#now += (time += 1); + this.#now += time; let timer = this.#executionQueue.peek(); while (timer) { if (!(timer.runAt <= this.#now)) break; From 12772d141e26d05c43914c467b5bad9a839d920f Mon Sep 17 00:00:00 2001 From: Erick Wendel Date: Wed, 14 Jun 2023 14:21:51 -0300 Subject: [PATCH 55/57] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tobias Nießen Co-authored-by: Colin Ihrig --- doc/api/test.md | 25 ++++++++++---------- lib/internal/test_runner/mock/mock_timers.js | 2 +- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/doc/api/test.md b/doc/api/test.md index 34c6ee830dd770..242998bce94908 100644 --- a/doc/api/test.md +++ b/doc/api/test.md @@ -521,17 +521,16 @@ Mocking timers is a technique commonly used in software testing to simulate and control the behavior of timers, such as `setInterval` and `setTimeout`, without actually waiting for the specified time intervals. -Look at [`MockTimers`][] class to check out all methods -and supported features from this API. +Refer to the [`MockTimers`][] class for a full list of methods and features. This allows developers to write more reliable and predictable tests for time-dependent functionality. The example below shows how to mock `setTimeout`. Using `.enable(['setTimeout']);` -it'll mock the `setTimeout` from both [node:timers](./timers.md), +it will mock the `setTimeout` functions in the [node:timers](./timers.md) and [node:timers/promises](./timers.md#timers-promises-api) modules, -and from the Node.js global context. +as well as from the Node.js global context. ```mjs import assert from 'node:assert'; @@ -552,7 +551,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w // Reset the globally tracked mocks. mock.timers.reset(); - // If you call reset mock instance, it'll also reset timers instance + // If you call reset mock instance, it will also reset timers instance mock.reset(); }); ``` @@ -581,7 +580,7 @@ test('mocks setTimeout to be executed synchronously without having to actually w }); ``` -The same mocking functionality is also exposed in the mock object on [`TestContext`][] object +The same mocking functionality is also exposed in the mock property on the [`TestContext`][] object of each test. The benefit of mocking via the test context is that the test runner will automatically restore all mocked timers functionality once the test finishes. @@ -1513,10 +1512,10 @@ Mocking timers is a technique commonly used in software testing to simulate and control the behavior of timers, such as `setInterval` and `setTimeout`, without actually waiting for the specified time intervals. -The [`MockTracker`][] provides a top level `timers` export -which is a MockTimers instance +The [`MockTracker`][] provides a top-level `timers` export +which is a `MockTimers` instance. -### `timers.enable([,timers])` +### `timers.enable([timers])`