2024-01-01 21:11:32 +00:00
|
|
|
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
|
2022-04-14 06:29:17 +00:00
|
|
|
|
2022-08-11 11:51:20 +00:00
|
|
|
/**
|
|
|
|
* Utilities for mocking time while testing.
|
|
|
|
*
|
2024-06-17 02:31:31 +00:00
|
|
|
* ```ts
|
|
|
|
* import {
|
|
|
|
* assertSpyCalls,
|
|
|
|
* spy,
|
|
|
|
* } from "@std/testing/mock";
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
|
|
|
*
|
|
|
|
* function secondInterval(cb: () => void): number {
|
|
|
|
* return setInterval(cb, 1000);
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
|
|
|
|
* using time = new FakeTime();
|
|
|
|
*
|
|
|
|
* const cb = spy();
|
|
|
|
* const intervalId = secondInterval(cb);
|
|
|
|
* assertSpyCalls(cb, 0);
|
|
|
|
* time.tick(500);
|
|
|
|
* assertSpyCalls(cb, 0);
|
|
|
|
* time.tick(500);
|
|
|
|
* assertSpyCalls(cb, 1);
|
|
|
|
* time.tick(3500);
|
|
|
|
* assertSpyCalls(cb, 4);
|
|
|
|
*
|
|
|
|
* clearInterval(intervalId);
|
|
|
|
* time.tick(1000);
|
|
|
|
* assertSpyCalls(cb, 4);
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
2022-08-11 11:51:20 +00:00
|
|
|
* @module
|
|
|
|
*/
|
2022-04-14 06:29:17 +00:00
|
|
|
|
2024-04-29 02:57:30 +00:00
|
|
|
import { RedBlackTree } from "@std/data-structures/red-black-tree";
|
|
|
|
import { ascend } from "@std/data-structures/comparators";
|
|
|
|
import type { DelayOptions } from "@std/async/delay";
|
2022-04-14 06:29:17 +00:00
|
|
|
import { _internals } from "./_time.ts";
|
|
|
|
|
2024-07-04 04:47:53 +00:00
|
|
|
export type { DelayOptions };
|
|
|
|
|
2024-06-17 02:31:31 +00:00
|
|
|
/**
|
2024-07-29 04:48:44 +00:00
|
|
|
* Represents an error when trying to execute an invalid operation on fake time,
|
|
|
|
* given the state fake time is in.
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime, TimeError } from "@std/testing/time";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assertThrows } from "@std/assert";
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* assertThrows(() => {
|
2024-07-29 04:48:44 +00:00
|
|
|
* const time = new FakeTime();
|
|
|
|
* time.restore();
|
|
|
|
* time.restore();
|
2024-06-17 02:31:31 +00:00
|
|
|
* }, TimeError);
|
|
|
|
* ```
|
|
|
|
*/
|
2022-04-14 06:29:17 +00:00
|
|
|
export class TimeError extends Error {
|
2024-06-17 02:31:31 +00:00
|
|
|
/** Construct TimeError.
|
|
|
|
*
|
|
|
|
* @param message The error message
|
|
|
|
*/
|
2022-04-14 06:29:17 +00:00
|
|
|
constructor(message: string) {
|
|
|
|
super(message);
|
|
|
|
this.name = "TimeError";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-10 10:56:34 +00:00
|
|
|
function FakeTimeNow() {
|
|
|
|
return time?.now ?? _internals.Date.now();
|
2022-04-14 06:29:17 +00:00
|
|
|
}
|
|
|
|
|
2023-06-10 10:56:34 +00:00
|
|
|
const FakeDate = new Proxy(Date, {
|
|
|
|
construct(_target, args) {
|
|
|
|
if (args.length === 0) args.push(FakeDate.now());
|
|
|
|
// @ts-expect-error this is a passthrough
|
|
|
|
return new _internals.Date(...args);
|
|
|
|
},
|
2024-06-24 23:35:53 +00:00
|
|
|
apply(_target, _thisArg, _args) {
|
|
|
|
return new _internals.Date(FakeTimeNow()).toString();
|
2023-06-10 10:56:34 +00:00
|
|
|
},
|
|
|
|
get(target, prop, receiver) {
|
|
|
|
if (prop === "now") {
|
|
|
|
return FakeTimeNow;
|
|
|
|
}
|
|
|
|
return Reflect.get(target, prop, receiver);
|
|
|
|
},
|
2022-04-14 06:29:17 +00:00
|
|
|
});
|
|
|
|
|
|
|
|
interface Timer {
|
|
|
|
id: number;
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
callback: (...args: any[]) => void;
|
|
|
|
delay: number;
|
|
|
|
args: unknown[];
|
|
|
|
due: number;
|
|
|
|
repeat: boolean;
|
|
|
|
}
|
|
|
|
|
2024-06-17 02:31:31 +00:00
|
|
|
/** The option for {@linkcode FakeTime} */
|
2022-04-14 06:29:17 +00:00
|
|
|
export interface FakeTimeOptions {
|
|
|
|
/**
|
|
|
|
* The rate relative to real time at which fake time is updated.
|
|
|
|
* By default time only moves forward through calling tick or setting now.
|
2023-06-21 16:27:37 +00:00
|
|
|
* Set to 1 to have the fake time automatically tick forward at the same rate in milliseconds as real time.
|
2024-07-11 09:21:37 +00:00
|
|
|
*
|
|
|
|
* @default {0}
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
|
|
|
advanceRate: number;
|
|
|
|
/**
|
|
|
|
* The frequency in milliseconds at which fake time is updated.
|
|
|
|
* If advanceRate is set, we will update the time every 10 milliseconds by default.
|
2024-07-11 09:21:37 +00:00
|
|
|
*
|
|
|
|
* @default {10}
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
|
|
|
advanceFrequency?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
interface DueNode {
|
|
|
|
due: number;
|
|
|
|
timers: Timer[];
|
|
|
|
}
|
|
|
|
|
|
|
|
let time: FakeTime | undefined = undefined;
|
|
|
|
|
|
|
|
function fakeSetTimeout(
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
callback: (...args: any[]) => void,
|
|
|
|
delay = 0,
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
...args: any[]
|
|
|
|
): number {
|
2024-08-26 05:22:16 +00:00
|
|
|
if (!time) throw new TimeError("Cannot set timeout: time is not faked");
|
2022-04-14 06:29:17 +00:00
|
|
|
return setTimer(callback, delay, args, false);
|
|
|
|
}
|
|
|
|
|
2024-04-22 22:37:50 +00:00
|
|
|
function fakeClearTimeout(id?: unknown) {
|
2024-08-26 05:22:16 +00:00
|
|
|
if (!time) throw new TimeError("Cannot clear timeout: time is not faked");
|
2022-04-14 06:29:17 +00:00
|
|
|
if (typeof id === "number" && dueNodes.has(id)) {
|
|
|
|
dueNodes.delete(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function fakeSetInterval(
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
callback: (...args: any[]) => unknown,
|
|
|
|
delay = 0,
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
...args: any[]
|
|
|
|
): number {
|
2024-08-26 05:22:16 +00:00
|
|
|
if (!time) throw new TimeError("Cannot set interval: time is not faked");
|
2022-04-14 06:29:17 +00:00
|
|
|
return setTimer(callback, delay, args, true);
|
|
|
|
}
|
|
|
|
|
2024-04-22 22:37:50 +00:00
|
|
|
function fakeClearInterval(id?: unknown) {
|
2024-08-26 05:22:16 +00:00
|
|
|
if (!time) throw new TimeError("Cannot clear interval: time is not faked");
|
2022-04-14 06:29:17 +00:00
|
|
|
if (typeof id === "number" && dueNodes.has(id)) {
|
|
|
|
dueNodes.delete(id);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function setTimer(
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
callback: (...args: any[]) => void,
|
|
|
|
delay = 0,
|
|
|
|
args: unknown[],
|
|
|
|
repeat = false,
|
|
|
|
): number {
|
|
|
|
const id: number = timerId.next().value;
|
|
|
|
delay = Math.max(repeat ? 1 : 0, Math.floor(delay));
|
|
|
|
const due: number = now + delay;
|
|
|
|
let dueNode: DueNode | null = dueTree.find({ due } as DueNode);
|
|
|
|
if (dueNode === null) {
|
|
|
|
dueNode = { due, timers: [] };
|
|
|
|
dueTree.insert(dueNode);
|
|
|
|
}
|
|
|
|
dueNode.timers.push({
|
|
|
|
id,
|
|
|
|
callback,
|
|
|
|
args,
|
|
|
|
delay,
|
|
|
|
due,
|
|
|
|
repeat,
|
|
|
|
});
|
|
|
|
dueNodes.set(id, dueNode);
|
|
|
|
return id;
|
|
|
|
}
|
|
|
|
|
2024-07-22 04:07:48 +00:00
|
|
|
function fakeAbortSignalTimeout(delay: number): AbortSignal {
|
|
|
|
const aborter = new AbortController();
|
|
|
|
fakeSetTimeout(() => {
|
|
|
|
aborter.abort(new DOMException("Signal timed out.", "TimeoutError"));
|
|
|
|
}, delay);
|
|
|
|
return aborter.signal;
|
|
|
|
}
|
|
|
|
|
2022-08-24 01:21:57 +00:00
|
|
|
function overrideGlobals() {
|
2023-06-10 10:56:34 +00:00
|
|
|
globalThis.Date = FakeDate;
|
2022-04-14 06:29:17 +00:00
|
|
|
globalThis.setTimeout = fakeSetTimeout;
|
|
|
|
globalThis.clearTimeout = fakeClearTimeout;
|
|
|
|
globalThis.setInterval = fakeSetInterval;
|
|
|
|
globalThis.clearInterval = fakeClearInterval;
|
2024-07-22 04:07:48 +00:00
|
|
|
AbortSignal.timeout = fakeAbortSignalTimeout;
|
2022-04-14 06:29:17 +00:00
|
|
|
}
|
|
|
|
|
2022-08-24 01:21:57 +00:00
|
|
|
function restoreGlobals() {
|
2022-04-14 06:29:17 +00:00
|
|
|
globalThis.Date = _internals.Date;
|
|
|
|
globalThis.setTimeout = _internals.setTimeout;
|
|
|
|
globalThis.clearTimeout = _internals.clearTimeout;
|
|
|
|
globalThis.setInterval = _internals.setInterval;
|
|
|
|
globalThis.clearInterval = _internals.clearInterval;
|
2024-07-22 04:07:48 +00:00
|
|
|
AbortSignal.timeout = _internals.AbortSignalTimeout;
|
2022-04-14 06:29:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function* timerIdGen() {
|
|
|
|
let i = 1;
|
|
|
|
while (true) yield i++;
|
|
|
|
}
|
|
|
|
|
2023-09-14 10:16:59 +00:00
|
|
|
function nextDueNode(): DueNode | null {
|
|
|
|
for (;;) {
|
|
|
|
const dueNode = dueTree.min();
|
|
|
|
if (!dueNode) return null;
|
|
|
|
const hasTimer = dueNode.timers.some((timer) => dueNodes.has(timer.id));
|
|
|
|
if (hasTimer) return dueNode;
|
|
|
|
dueTree.remove(dueNode);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-14 06:29:17 +00:00
|
|
|
let startedAt: number;
|
|
|
|
let now: number;
|
|
|
|
let initializedAt: number;
|
|
|
|
let advanceRate: number;
|
|
|
|
let advanceFrequency: number;
|
|
|
|
let advanceIntervalId: number | undefined;
|
|
|
|
let timerId: Generator<number>;
|
|
|
|
let dueNodes: Map<number, DueNode>;
|
2022-09-29 15:24:28 +00:00
|
|
|
let dueTree: RedBlackTree<DueNode>;
|
2022-04-14 06:29:17 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Overrides the real Date object and timer functions with fake ones that can be
|
|
|
|
* controlled through the fake time instance.
|
|
|
|
*
|
2024-06-18 04:36:51 +00:00
|
|
|
* Note: there is no setter for the `start` property, as it cannot be changed
|
|
|
|
* after initialization.
|
|
|
|
*
|
2024-06-17 02:31:31 +00:00
|
|
|
* @example Usage
|
2022-04-14 06:29:17 +00:00
|
|
|
* ```ts
|
|
|
|
* import {
|
|
|
|
* assertSpyCalls,
|
|
|
|
* spy,
|
2024-04-29 02:57:30 +00:00
|
|
|
* } from "@std/testing/mock";
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
2023-11-22 07:39:15 +00:00
|
|
|
*
|
|
|
|
* function secondInterval(cb: () => void): number {
|
|
|
|
* return setInterval(cb, 1000);
|
|
|
|
* }
|
2022-04-14 06:29:17 +00:00
|
|
|
*
|
|
|
|
* Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
|
2023-12-14 20:47:50 +00:00
|
|
|
* using time = new FakeTime();
|
2022-04-14 06:29:17 +00:00
|
|
|
*
|
2023-12-14 20:47:50 +00:00
|
|
|
* const cb = spy();
|
|
|
|
* const intervalId = secondInterval(cb);
|
|
|
|
* assertSpyCalls(cb, 0);
|
|
|
|
* time.tick(500);
|
|
|
|
* assertSpyCalls(cb, 0);
|
|
|
|
* time.tick(500);
|
|
|
|
* assertSpyCalls(cb, 1);
|
|
|
|
* time.tick(3500);
|
|
|
|
* assertSpyCalls(cb, 4);
|
2022-04-14 06:29:17 +00:00
|
|
|
*
|
2023-12-14 20:47:50 +00:00
|
|
|
* clearInterval(intervalId);
|
|
|
|
* time.tick(1000);
|
|
|
|
* assertSpyCalls(cb, 4);
|
2022-04-14 06:29:17 +00:00
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*/
|
|
|
|
export class FakeTime {
|
2024-06-17 02:31:31 +00:00
|
|
|
/**
|
|
|
|
* Construct a FakeTime object. This overrides the real Date object and timer functions with fake ones that can be
|
|
|
|
* controlled through the fake time instance.
|
|
|
|
*
|
2024-07-29 04:48:44 +00:00
|
|
|
* @param start The time to simulate. The default is the current time.
|
2024-06-17 02:31:31 +00:00
|
|
|
* @param options The options
|
2024-07-29 04:48:44 +00:00
|
|
|
*
|
|
|
|
* @throws {TimeError} If time is already faked
|
|
|
|
* @throws {TypeError} If the start is invalid
|
2024-06-17 02:31:31 +00:00
|
|
|
*/
|
2022-04-14 06:29:17 +00:00
|
|
|
constructor(
|
|
|
|
start?: number | string | Date | null,
|
|
|
|
options?: FakeTimeOptions,
|
|
|
|
) {
|
2024-08-26 05:22:16 +00:00
|
|
|
if (time) {
|
|
|
|
throw new TimeError("Cannot construct FakeTime: time is already faked");
|
|
|
|
}
|
2022-04-14 06:29:17 +00:00
|
|
|
initializedAt = _internals.Date.now();
|
|
|
|
startedAt = start instanceof Date
|
|
|
|
? start.valueOf()
|
|
|
|
: typeof start === "number"
|
|
|
|
? Math.floor(start)
|
|
|
|
: typeof start === "string"
|
|
|
|
? (new Date(start)).valueOf()
|
|
|
|
: initializedAt;
|
2024-08-26 05:22:16 +00:00
|
|
|
if (Number.isNaN(startedAt)) {
|
|
|
|
throw new TypeError(
|
|
|
|
`Cannot construct FakeTime: invalid start time ${startedAt}`,
|
|
|
|
);
|
|
|
|
}
|
2022-04-14 06:29:17 +00:00
|
|
|
now = startedAt;
|
|
|
|
|
|
|
|
timerId = timerIdGen();
|
|
|
|
dueNodes = new Map();
|
2022-09-29 15:24:28 +00:00
|
|
|
dueTree = new RedBlackTree(
|
2022-04-14 06:29:17 +00:00
|
|
|
(a: DueNode, b: DueNode) => ascend(a.due, b.due),
|
|
|
|
);
|
|
|
|
|
|
|
|
overrideGlobals();
|
|
|
|
time = this;
|
|
|
|
|
|
|
|
advanceRate = Math.max(
|
|
|
|
0,
|
2024-07-11 09:21:37 +00:00
|
|
|
options?.advanceRate ?? 0,
|
2022-04-14 06:29:17 +00:00
|
|
|
);
|
|
|
|
advanceFrequency = Math.max(
|
|
|
|
0,
|
2024-07-11 09:21:37 +00:00
|
|
|
options?.advanceFrequency ?? 10,
|
2022-04-14 06:29:17 +00:00
|
|
|
);
|
|
|
|
advanceIntervalId = advanceRate > 0
|
|
|
|
? _internals.setInterval.call(null, () => {
|
|
|
|
this.tick(advanceRate * advanceFrequency);
|
|
|
|
}, advanceFrequency)
|
|
|
|
: undefined;
|
|
|
|
}
|
|
|
|
|
2024-06-17 02:31:31 +00:00
|
|
|
/**
|
|
|
|
* Restores real time.
|
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
|
|
|
* import { assertEquals, assertNotEquals } from "@std/assert";
|
|
|
|
*
|
|
|
|
* const setTimeout = globalThis.setTimeout;
|
|
|
|
*
|
|
|
|
* {
|
|
|
|
* using fakeTime = new FakeTime();
|
|
|
|
*
|
|
|
|
* assertNotEquals(globalThis.setTimeout, setTimeout);
|
|
|
|
*
|
|
|
|
* // test timer related things.
|
|
|
|
*
|
|
|
|
* // You don't need to call fakeTime.restore() explicitly
|
|
|
|
* // as it's implicitly called via the [Symbol.dispose] method
|
|
|
|
* // when declared with `using`.
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* assertEquals(globalThis.setTimeout, setTimeout);
|
|
|
|
* ```
|
|
|
|
*/
|
2023-12-14 20:47:50 +00:00
|
|
|
[Symbol.dispose]() {
|
|
|
|
this.restore();
|
|
|
|
}
|
|
|
|
|
2024-06-17 02:31:31 +00:00
|
|
|
/**
|
|
|
|
* Restores real time.
|
|
|
|
*
|
2024-07-29 04:48:44 +00:00
|
|
|
* @throws {TimeError} If time is already restored
|
|
|
|
*
|
2024-06-17 02:31:31 +00:00
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
|
|
|
* import { assertEquals, assertNotEquals } from "@std/assert"
|
|
|
|
*
|
|
|
|
* const setTimeout = globalThis.setTimeout;
|
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime();
|
|
|
|
*
|
|
|
|
* assertNotEquals(globalThis.setTimeout, setTimeout);
|
|
|
|
*
|
|
|
|
* FakeTime.restore();
|
|
|
|
*
|
|
|
|
* assertEquals(globalThis.setTimeout, setTimeout);
|
|
|
|
* ```
|
|
|
|
*/
|
2022-08-24 01:21:57 +00:00
|
|
|
static restore() {
|
2024-08-26 05:22:16 +00:00
|
|
|
if (!time) {
|
|
|
|
throw new TimeError("Cannot restore time: time is already restored");
|
|
|
|
}
|
2022-04-14 06:29:17 +00:00
|
|
|
time.restore();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Restores real time temporarily until callback returns and resolves.
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
2024-07-29 04:48:44 +00:00
|
|
|
* @throws {TimeError} If time is not faked
|
|
|
|
*
|
2024-06-17 02:31:31 +00:00
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
|
|
|
* import { assertEquals, assertNotEquals } from "@std/assert"
|
|
|
|
*
|
|
|
|
* const setTimeout = globalThis.setTimeout;
|
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime();
|
|
|
|
*
|
|
|
|
* assertNotEquals(globalThis.setTimeout, setTimeout);
|
|
|
|
*
|
|
|
|
* FakeTime.restoreFor(() => {
|
|
|
|
* assertEquals(globalThis.setTimeout, setTimeout);
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @typeParam T The returned value type of the callback
|
|
|
|
* @param callback The callback to be called while FakeTime being restored
|
|
|
|
* @param args The arguments to pass to the callback
|
|
|
|
* @returns The returned value from the callback
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
2023-08-14 09:33:35 +00:00
|
|
|
static restoreFor<T>(
|
2022-04-14 06:29:17 +00:00
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
callback: (...args: any[]) => Promise<T> | T,
|
|
|
|
// deno-lint-ignore no-explicit-any
|
|
|
|
...args: any[]
|
|
|
|
): Promise<T> {
|
2024-08-26 05:22:16 +00:00
|
|
|
if (!time) {
|
|
|
|
return Promise.reject(
|
|
|
|
new TimeError("Cannot restore time: time is not faked"),
|
|
|
|
);
|
|
|
|
}
|
2022-04-14 06:29:17 +00:00
|
|
|
restoreGlobals();
|
|
|
|
try {
|
2023-08-14 09:33:35 +00:00
|
|
|
const result = callback.apply(null, args);
|
|
|
|
if (result instanceof Promise) {
|
|
|
|
return result.finally(() => overrideGlobals());
|
|
|
|
} else {
|
|
|
|
overrideGlobals();
|
|
|
|
return Promise.resolve(result);
|
|
|
|
}
|
|
|
|
} catch (e) {
|
2022-04-14 06:29:17 +00:00
|
|
|
overrideGlobals();
|
2023-08-14 09:33:35 +00:00
|
|
|
return Promise.reject(e);
|
2022-04-14 06:29:17 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2024-06-17 02:31:31 +00:00
|
|
|
* The number of milliseconds elapsed since the epoch (January 1, 1970 00:00:00 UTC) for the fake time.
|
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assertEquals } from "@std/assert";
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* assertEquals(fakeTime.now, 15_000);
|
|
|
|
*
|
|
|
|
* fakeTime.tick(5_000);
|
|
|
|
*
|
|
|
|
* assertEquals(fakeTime.now, 20_000);
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @returns The number of milliseconds elapsed since the epoch (January 1, 1970 00:00:00 UTC) for the fake time
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
|
|
|
get now(): number {
|
|
|
|
return now;
|
|
|
|
}
|
2024-06-17 02:31:31 +00:00
|
|
|
/**
|
|
|
|
* Set the current time. It will call any functions waiting to be called between the current and new fake time.
|
|
|
|
* If the timer callback throws, time will stop advancing forward beyond that timer.
|
|
|
|
*
|
2024-07-29 04:48:44 +00:00
|
|
|
* @throws {RangeError} If the time goes backwards
|
|
|
|
*
|
2024-06-17 02:31:31 +00:00
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assertEquals } from "@std/assert";
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* assertEquals(fakeTime.now, 15_000);
|
|
|
|
*
|
|
|
|
* fakeTime.now = 35_000;
|
|
|
|
*
|
|
|
|
* assertEquals(fakeTime.now, 35_000);
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param value The current time (in milliseconds)
|
|
|
|
*/
|
2022-04-14 06:29:17 +00:00
|
|
|
set now(value: number) {
|
2024-08-26 05:22:16 +00:00
|
|
|
if (value < now) {
|
|
|
|
throw new RangeError(
|
|
|
|
`Cannot set current time in the past, time must be >= ${now}: received ${value}`,
|
|
|
|
);
|
|
|
|
}
|
2022-04-14 06:29:17 +00:00
|
|
|
let dueNode: DueNode | null = dueTree.min();
|
|
|
|
while (dueNode && dueNode.due <= value) {
|
|
|
|
const timer: Timer | undefined = dueNode.timers.shift();
|
|
|
|
if (timer && dueNodes.has(timer.id)) {
|
|
|
|
now = timer.due;
|
|
|
|
if (timer.repeat) {
|
|
|
|
const due: number = timer.due + timer.delay;
|
|
|
|
let dueNode: DueNode | null = dueTree.find({ due } as DueNode);
|
|
|
|
if (dueNode === null) {
|
|
|
|
dueNode = { due, timers: [] };
|
|
|
|
dueTree.insert(dueNode);
|
|
|
|
}
|
|
|
|
dueNode.timers.push({ ...timer, due });
|
|
|
|
dueNodes.set(timer.id, dueNode);
|
|
|
|
} else {
|
|
|
|
dueNodes.delete(timer.id);
|
|
|
|
}
|
|
|
|
timer.callback.apply(null, timer.args);
|
|
|
|
} else if (!timer) {
|
|
|
|
dueTree.remove(dueNode);
|
|
|
|
dueNode = dueTree.min();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
now = value;
|
|
|
|
}
|
|
|
|
|
2024-06-17 02:31:31 +00:00
|
|
|
/**
|
|
|
|
* The initial number of milliseconds elapsed since the epoch (January 1, 1970 00:00:00 UTC) for the fake time.
|
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assertEquals } from "@std/assert";
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* assertEquals(fakeTime.start, 15_000);
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @returns The initial number of milliseconds elapsed since the epoch (January 1, 1970 00:00:00 UTC) for the fake time.
|
|
|
|
*/
|
2022-04-14 06:29:17 +00:00
|
|
|
get start(): number {
|
|
|
|
return startedAt;
|
|
|
|
}
|
|
|
|
|
2024-06-17 02:31:31 +00:00
|
|
|
/**
|
|
|
|
* Resolves after the given number of milliseconds using real time.
|
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assertEquals } from "@std/assert";
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* await fakeTime.delay(500); // wait 500 ms in real time.
|
|
|
|
*
|
|
|
|
* assertEquals(fakeTime.now, 15_000); // The simulated time doesn't advance.
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param ms The milliseconds to delay
|
|
|
|
* @param options The options
|
|
|
|
*/
|
2022-04-14 06:29:17 +00:00
|
|
|
async delay(ms: number, options: DelayOptions = {}): Promise<void> {
|
|
|
|
const { signal } = options;
|
|
|
|
if (signal?.aborted) {
|
|
|
|
return Promise.reject(
|
|
|
|
new DOMException("Delay was aborted.", "AbortError"),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
|
|
let timer: number | null = null;
|
|
|
|
const abort = () =>
|
|
|
|
FakeTime
|
|
|
|
.restoreFor(() => {
|
|
|
|
if (timer) clearTimeout(timer);
|
|
|
|
})
|
|
|
|
.then(() =>
|
|
|
|
reject(new DOMException("Delay was aborted.", "AbortError"))
|
|
|
|
);
|
|
|
|
const done = () => {
|
|
|
|
signal?.removeEventListener("abort", abort);
|
|
|
|
resolve();
|
|
|
|
};
|
|
|
|
FakeTime.restoreFor(() => setTimeout(done, ms))
|
|
|
|
.then((id) => timer = id);
|
|
|
|
signal?.addEventListener("abort", abort, { once: true });
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-06-17 02:31:31 +00:00
|
|
|
/**
|
|
|
|
* Runs all pending microtasks.
|
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assert } from "@std/assert";
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* let called = false;
|
|
|
|
*
|
|
|
|
* Promise.resolve().then(() => { called = true });
|
|
|
|
*
|
|
|
|
* await fakeTime.runMicrotasks();
|
|
|
|
*
|
|
|
|
* assert(called);
|
|
|
|
* ```
|
|
|
|
*/
|
2022-08-24 01:21:57 +00:00
|
|
|
async runMicrotasks() {
|
2022-04-14 06:29:17 +00:00
|
|
|
await this.delay(0);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds the specified number of milliseconds to the fake time.
|
|
|
|
* This will call any functions waiting to be called between the current and new fake time.
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import {
|
|
|
|
* assertSpyCalls,
|
|
|
|
* spy,
|
|
|
|
* } from "@std/testing/mock";
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
|
|
|
*
|
|
|
|
* function secondInterval(cb: () => void): number {
|
|
|
|
* return setInterval(cb, 1000);
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* Deno.test("secondInterval calls callback every second and stops after being cleared", () => {
|
|
|
|
* using time = new FakeTime();
|
|
|
|
*
|
|
|
|
* const cb = spy();
|
|
|
|
* const intervalId = secondInterval(cb);
|
|
|
|
* assertSpyCalls(cb, 0);
|
|
|
|
* time.tick(500);
|
|
|
|
* assertSpyCalls(cb, 0);
|
|
|
|
* time.tick(500);
|
|
|
|
* assertSpyCalls(cb, 1);
|
|
|
|
* time.tick(3500);
|
|
|
|
* assertSpyCalls(cb, 4);
|
|
|
|
*
|
|
|
|
* clearInterval(intervalId);
|
|
|
|
* time.tick(1000);
|
|
|
|
* assertSpyCalls(cb, 4);
|
|
|
|
* });
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param ms The milliseconds to advance
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
2022-08-24 01:21:57 +00:00
|
|
|
tick(ms = 0) {
|
2022-04-14 06:29:17 +00:00
|
|
|
this.now += ms;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Runs all pending microtasks then adds the specified number of milliseconds to the fake time.
|
|
|
|
* This will call any functions waiting to be called between the current and new fake time.
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
|
|
|
* import { assert, assertEquals } from "@std/assert";
|
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* let called = false;
|
|
|
|
*
|
|
|
|
* Promise.resolve().then(() => { called = true });
|
|
|
|
*
|
|
|
|
* await fakeTime.tickAsync(5_000);
|
|
|
|
*
|
|
|
|
* assert(called);
|
|
|
|
* assertEquals(fakeTime.now, 20_000);
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @param ms The milliseconds to advance
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
2022-08-24 01:21:57 +00:00
|
|
|
async tickAsync(ms = 0) {
|
2022-04-14 06:29:17 +00:00
|
|
|
await this.runMicrotasks();
|
|
|
|
this.now += ms;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Advances time to when the next scheduled timer is due.
|
|
|
|
* If there are no pending timers, time will not be changed.
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
|
|
|
* import { assert, assertEquals } from "@std/assert";
|
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* let called = false;
|
|
|
|
*
|
|
|
|
* setTimeout(() => { called = true }, 5000);
|
|
|
|
*
|
|
|
|
* fakeTime.next();
|
|
|
|
*
|
|
|
|
* assert(called);
|
|
|
|
* assertEquals(fakeTime.now, 20_000);
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @returns `true` when there is a scheduled timer and `false` when there is not.
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
|
|
|
next(): boolean {
|
2023-09-14 10:16:59 +00:00
|
|
|
const next = nextDueNode();
|
2022-04-14 06:29:17 +00:00
|
|
|
if (next) this.now = next.due;
|
|
|
|
return !!next;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Runs all pending microtasks then advances time to when the next scheduled timer is due.
|
|
|
|
* If there are no pending timers, time will not be changed.
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
|
|
|
* import { assert, assertEquals } from "@std/assert";
|
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* let called0 = false;
|
|
|
|
* let called1 = false;
|
|
|
|
*
|
|
|
|
* setTimeout(() => { called0 = true }, 5000);
|
|
|
|
* Promise.resolve().then(() => { called1 = true });
|
|
|
|
*
|
|
|
|
* await fakeTime.nextAsync();
|
|
|
|
*
|
|
|
|
* assert(called0);
|
|
|
|
* assert(called1);
|
|
|
|
* assertEquals(fakeTime.now, 20_000);
|
|
|
|
* ```
|
|
|
|
*
|
|
|
|
* @returns `true` if the pending timers existed and the time advanced, `false` if there was no pending timer and the time didn't advance.
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
|
|
|
async nextAsync(): Promise<boolean> {
|
|
|
|
await this.runMicrotasks();
|
|
|
|
return this.next();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Advances time forward to the next due timer until there are no pending timers remaining.
|
|
|
|
* If the timers create additional timers, they will be run too. If there is an interval,
|
|
|
|
* time will keep advancing forward until the interval is cleared.
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assertEquals } from "@std/assert";
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* let count = 0;
|
|
|
|
*
|
|
|
|
* setTimeout(() => { count++ }, 5_000);
|
|
|
|
* setTimeout(() => { count++ }, 15_000);
|
|
|
|
* setTimeout(() => { count++ }, 35_000);
|
|
|
|
*
|
|
|
|
* fakeTime.runAll();
|
|
|
|
*
|
|
|
|
* assertEquals(count, 3);
|
|
|
|
* assertEquals(fakeTime.now, 50_000);
|
|
|
|
* ```
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
2022-08-24 01:21:57 +00:00
|
|
|
runAll() {
|
2022-04-14 06:29:17 +00:00
|
|
|
while (!dueTree.isEmpty()) {
|
|
|
|
this.next();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Advances time forward to the next due timer until there are no pending timers remaining.
|
|
|
|
* If the timers create additional timers, they will be run too. If there is an interval,
|
|
|
|
* time will keep advancing forward until the interval is cleared.
|
|
|
|
* Runs all pending microtasks before each timer.
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
|
|
|
* import { assertEquals } from "@std/assert";
|
2024-06-17 02:31:31 +00:00
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(15_000);
|
|
|
|
*
|
|
|
|
* let count = 0;
|
|
|
|
*
|
|
|
|
* setTimeout(() => { count++ }, 5_000);
|
|
|
|
* setTimeout(() => { count++ }, 15_000);
|
|
|
|
* setTimeout(() => { count++ }, 35_000);
|
|
|
|
* Promise.resolve().then(() => { count++ });
|
|
|
|
*
|
|
|
|
* await fakeTime.runAllAsync();
|
|
|
|
*
|
|
|
|
* assertEquals(count, 4);
|
|
|
|
* assertEquals(fakeTime.now, 50_000);
|
|
|
|
* ```
|
2022-04-14 06:29:17 +00:00
|
|
|
*/
|
2022-08-24 01:21:57 +00:00
|
|
|
async runAllAsync() {
|
2022-04-14 06:29:17 +00:00
|
|
|
while (!dueTree.isEmpty()) {
|
|
|
|
await this.nextAsync();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-06-17 02:31:31 +00:00
|
|
|
/**
|
|
|
|
* Restores time related global functions to their original state.
|
|
|
|
*
|
|
|
|
* @example Usage
|
|
|
|
* ```ts
|
|
|
|
* import { FakeTime } from "@std/testing/time";
|
|
|
|
* import { assertEquals, assertNotEquals } from "@std/assert";
|
|
|
|
*
|
|
|
|
* const setTimeout = globalThis.setTimeout;
|
|
|
|
*
|
|
|
|
* const fakeTime = new FakeTime(); // global timers are now faked
|
|
|
|
*
|
|
|
|
* assertNotEquals(globalThis.setTimeout, setTimeout);
|
|
|
|
*
|
|
|
|
* fakeTime.restore(); // timers are restored
|
|
|
|
*
|
|
|
|
* assertEquals(globalThis.setTimeout, setTimeout);
|
|
|
|
* ```
|
|
|
|
*/
|
2022-08-24 01:21:57 +00:00
|
|
|
restore() {
|
2024-08-26 05:22:16 +00:00
|
|
|
if (!time) {
|
|
|
|
throw new TimeError("Cannot restore time: time is already restored");
|
|
|
|
}
|
2022-04-14 06:29:17 +00:00
|
|
|
time = undefined;
|
|
|
|
restoreGlobals();
|
|
|
|
if (advanceIntervalId) clearInterval(advanceIntervalId);
|
|
|
|
}
|
|
|
|
}
|