1/**
2 * Controllable `Clock` service for tests.
3 *
4 * Instead of waiting for real time to pass, effects that use `Effect.sleep`,
5 * timeouts, schedules, retries, and other time-based operators can be driven by
6 * advancing the test clock. This makes time-based tests deterministic and fast.
7 * The module also includes helpers for moving test time, temporarily using the
8 * live clock, and warning when a test appears to be waiting on time without
9 * advancing it.
10 *
11 * @since 2.0.0
12 */
13import * as Arr from "../Array.ts"
14import * as Clock from "../Clock.ts"
15import * as Data from "../Data.ts"
16import * as Duration from "../Duration.ts"
17import * as Effect from "../Effect.ts"
18import * as Fiber from "../Fiber.ts"
19import { flow } from "../Function.ts"
20import * as Latch from "../Latch.ts"
21import * as Layer from "../Layer.ts"
22import * as Order from "../Order.ts"
23import * as Semaphore from "../Semaphore.ts"
24
25/**
26 * A `TestClock` simplifies deterministic and efficient testing of effects that
27 * involve the passage of time.
28 *
29 * **Details**
30 *
31 * Instead of waiting for actual time to pass, `sleep` and methods implemented
32 * in terms of it schedule effects to take place at a given clock time. Use
33 * `adjust` and `setTime` to move clock time, and all effects scheduled to take
34 * place on or before that time will automatically run in order.
35 *
36 * **Gotchas**
37 *
38 * Calls to `sleep` and methods derived from it will semantically block until
39 * the time is set to on or after the time they are scheduled to run. Fork the
40 * effect being tested, then adjust the clock time, and finally verify that the
41 * expected effects have been performed.
42 *
43 * **Example** (Testing timeouts deterministically)
44 *
45 * Tests `Effect.timeout` using `TestClock`.
46 *
47 * ```ts import.meta.vitest
48 * import { Effect, Exit, Fiber, pipe } from "effect"
49 * import { TestClock } from "effect/testing"
50 *
51 * const program = Effect.gen(function*() {
52 * const fiber = yield* pipe(
53 * Effect.sleep("5 minutes"),
54 * Effect.timeout("1 minute"),
55 * Effect.forkChild
56 * )
57 * yield* TestClock.adjust("1 minute")
58 * const exit = yield* Fiber.await(fiber)
59 * Exit.isFailure(exit) // => true
60 * })
61 *
62 * await Effect.runPromise(Effect.provide(program, TestClock.layer()))
63 * ```
64 *
65 * **Example** (Advancing time deterministically)
66 *
67 * ```ts import.meta.vitest
68 * import { Effect, Fiber } from "effect"
69 * import { TestClock } from "effect/testing"
70 *
71 * const program = Effect.gen(function*() {
72 * let executed = false
73 *
74 * // Fork an effect that sleeps for 1 hour
75 * const fiber = yield* Effect.gen(function*() {
76 * yield* Effect.sleep("1 hour")
77 * executed = true
78 * }).pipe(Effect.forkChild)
79 *
80 * // Advance the test clock by 1 hour
81 * yield* TestClock.adjust("1 hour")
82 * yield* Fiber.join(fiber)
83 *
84 * // The effect should now be executed
85 * executed // => true
86 * })
87 *
88 * await Effect.runPromise(Effect.provide(program, TestClock.layer()))
89 * ```
90 *
91 * @category models
92 * @since 2.0.0
93 */
94export interface TestClock extends Clock.Clock {
95 /**
96 * Increments the current clock time by the specified duration. Any effects
97 * that were scheduled to occur on or before the new time will be run in
98 * order.
99 */
100 adjust(duration: Duration.Input): Effect.Effect<void>
101 /**
102 * Sets the current clock time to the specified `timestamp`. Any effects that
103 * were scheduled to occur on or before the new time will be run in order.
104 */
105 setTime(timestamp: number): Effect.Effect<void>
106 /**
107 * Executes the specified effect with the live `Clock` instead of the
108 * `TestClock`.
109 */
110 withLive<A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R>
111}
112
113/**
114 * Namespace containing `TestClock` configuration and state types.
115 *
116 * **Example** (Configuring a test clock)
117 *
118 * ```ts import.meta.vitest
119 * import { Effect } from "effect"
120 * import { TestClock } from "effect/testing"
121 *
122 * const program = Effect.gen(function*() {
123 * // Create a TestClock with custom options
124 * const testClock = yield* TestClock.make({
125 * warningDelay: "5 seconds"
126 * })
127 *
128 * // Access the current state
129 * const currentTime = testClock.currentTimeMillisUnsafe()
130 * currentTime // => 0
131 * })
132 *
133 * await Effect.runPromise(Effect.scoped(program))
134 * ```
135 *
136 * @since 2.0.0
137 */
138export declare namespace TestClock {
139 /**
140 * Options used when constructing a `TestClock`. `warningDelay` controls how
141 * long the live clock waits before logging a warning when a test uses time
142 * without advancing the test clock.
143 *
144 * **Example** (Configuring the warning delay)
145 *
146 * ```ts import.meta.vitest
147 * import { Effect } from "effect"
148 * import { TestClock } from "effect/testing"
149 *
150 * const program = Effect.gen(function*() {
151 * // Create a TestClock with custom warning delay
152 * const testClock = yield* TestClock.make({
153 * warningDelay: "30 seconds"
154 * })
155 *
156 * // Use the TestClock in your test
157 * yield* testClock.adjust("1 hour")
158 * testClock.currentTimeMillisUnsafe() // => 3_600_000
159 * })
160 *
161 * await Effect.runPromise(Effect.scoped(program))
162 * ```
163 *
164 * @category options
165 * @since 4.0.0
166 */
167 export interface Options {
168 /**
169 * The amount of time to wait before displaying a warning message when a
170 * test is using time but is not advancing the `TestClock`.
171 */
172 readonly warningDelay?: Duration.Input
173 }
174
175 /**
176 * Represents the state tracked by a `TestClock`, including the current
177 * millisecond timestamp and the sleeps scheduled to resume when the clock
178 * reaches their target time.
179 *
180 * @category models
181 * @since 4.0.0
182 */
183 export interface State {
184 readonly timestamp: number
185 readonly sleeps: ReadonlyArray<[number, Latch.Latch]>
186 }
187}
188
189/**
190 * The warning message that will be displayed if a test is using time but is
191 * not advancing the `TestClock`.
192 */
193const warningMessage = "A test is using time, but is not advancing the test " +
194 "clock, which may result in the test hanging. Use TestClock.adjust to " +
195 "manually advance the time."
196
197const defaultOptions: Required<TestClock.Options> = {
198 warningDelay: "1 second"
199}
200
201const SleepOrder = Order.flip(Order.Struct({
202 timestamp: Order.Number,
203 sequence: Order.Number
204}))
205
206const nanosPerMilli = BigInt(1_000_000)
207
208const millisToNanos = (millis: number): bigint => {
209 const wholeMillis = Math.floor(millis)
210 const fractionalNanos = Math.floor((millis - wholeMillis) * 1_000_000)
211 return BigInt(wholeMillis) * nanosPerMilli + BigInt(fractionalNanos)
212}
213
214/**
215 * Creates a `TestClock` with optional configuration.
216 *
217 * **Example** (Creating a test clock)
218 *
219 * ```ts import.meta.vitest
220 * import { Effect } from "effect"
221 * import { TestClock } from "effect/testing"
222 *
223 * const program = Effect.gen(function*() {
224 * // Create a TestClock with default settings
225 * const testClock = yield* TestClock.make()
226 *
227 * // Create a TestClock with custom warning delay
228 * const customTestClock = yield* TestClock.make({
229 * warningDelay: "10 seconds"
230 * })
231 *
232 * // Use the TestClock to control time in tests
233 * yield* testClock.adjust("1 hour")
234 * const currentTime = testClock.currentTimeMillisUnsafe()
235 * currentTime // => 3_600_000
236 * })
237 *
238 * await Effect.runPromise(Effect.scoped(program))
239 * ```
240 *
241 * @category constructors
242 * @since 4.0.0
243 */
244export const make = Effect.fnUntraced(function*(
245 options?: TestClock.Options
246) {
247 const config = { ...defaultOptions, ...options }
248 let sequence = 0
249 const sleeps: Array<{
250 readonly sequence: number
251 readonly timestamp: number
252 readonly latch: Latch.Latch
253 }> = []
254 const liveClock = yield* Clock.clockWith(Effect.succeed)
255 const warningSemaphore = yield* Semaphore.make(1)
256
257 let currentTimestamp: number = new Date(0).getTime()
258 let currentWallNanos = BigInt(0)
259 let currentMonotonicNanos = BigInt(0)
260 let warningState: WarningState = WarningState.Start()
261
262 function currentTimeMillisUnsafe(): number {
263 return currentTimestamp
264 }
265
266 function currentTimeNanosUnsafe(): bigint {
267 return currentWallNanos
268 }
269
270 function monotonicTimeNanosUnsafe(): bigint {
271 return currentMonotonicNanos
272 }
273
274 const currentTimeMillis = Effect.sync(currentTimeMillisUnsafe)
275 const currentTimeNanos = Effect.sync(currentTimeNanosUnsafe)
276 const monotonicTimeNanos = Effect.sync(monotonicTimeNanosUnsafe)
277
278 function withLive<A, E, R>(effect: Effect.Effect<A, E, R>) {
279 return Effect.provideService(effect, Clock.Clock, liveClock)
280 }
281
282 /**
283 * Forks a fiber that will display a warning message if a test is using time
284 * but is not advancing the `TestClock`.
285 */
286 const warningStart = warningSemaphore.withPermits(1)(
287 Effect.suspend(() => {
288 if (warningState._tag === "Start") {
289 return Effect.logWarning(warningMessage).pipe(
290 Effect.delay(config.warningDelay),
291 withLive,
292 Effect.forkChild,
293 Effect.interruptible,
294 Effect.flatMap((fiber) =>
295 Effect.sync(() => {
296 warningState = WarningState.Pending({ fiber })
297 })
298 )
299 )
300 }
301 return Effect.void
302 })
303 )
304 /**
305 * Cancels the warning message that is displayed if a test is using time but
306 * is not advancing the `TestClock`.
307 */
308 const warningDone = warningSemaphore.withPermits(1)(
309 Effect.suspend(() => {
310 switch (warningState._tag) {
311 case "Pending": {
312 return Fiber.interrupt(warningState.fiber).pipe(
313 Effect.andThen(Effect.sync(() => {
314 warningState = WarningState.Done()
315 }))
316 )
317 }
318 case "Start":
319 case "Done": {
320 warningState = WarningState.Done()
321 return Effect.void
322 }
323 }
324 })
325 )
326
327 const sleep = Effect.fnUntraced(function*(duration: Duration.Duration) {
328 const millis = Duration.toMillis(duration)
329 const end = currentTimestamp + millis
330 if (end <= currentTimestamp) return
331 const latch = Latch.makeUnsafe()
332 sleeps.push({
333 sequence: sequence++,
334 timestamp: end,
335 latch
336 })
337 sleeps.sort(SleepOrder)
338 yield* warningStart
339 yield* latch.await
340 })
341
342 const runSemaphore = yield* Semaphore.make(1)
343 const run = Effect.fnUntraced(function*(
344 step: (currentTimestamp: number) => number,
345 adjustmentNanos?: bigint
346 ) {
347 yield* Fiber.await(yield* Effect.forkChild(Effect.yieldNow))
348 const initialWallNanos = currentWallNanos
349 const initialMonotonicNanos = currentMonotonicNanos
350 const endTimestamp = step(currentTimestamp)
351 const advanceTo = (timestamp: number) => {
352 const deltaMillis = timestamp - currentTimestamp
353 if (deltaMillis > 0 && Number.isFinite(deltaMillis)) {
354 currentMonotonicNanos += BigInt(Math.round(deltaMillis * 1_000_000))
355 }
356 if (Number.isFinite(timestamp)) {
357 currentWallNanos = millisToNanos(timestamp)
358 }
359 currentTimestamp = timestamp
360 }
361 while (Arr.isArrayNonEmpty(sleeps)) {
362 if (Arr.lastNonEmpty(sleeps).timestamp > endTimestamp) break
363 const entry = sleeps.pop()!
364 advanceTo(entry.timestamp)
365 entry.latch.openUnsafe()
366 yield* Effect.yieldNow
367 }
368 advanceTo(endTimestamp)
369 if (adjustmentNanos !== undefined && Number.isFinite(endTimestamp)) {
370 currentWallNanos = initialWallNanos + adjustmentNanos
371 if (adjustmentNanos > BigInt(0)) {
372 currentMonotonicNanos = initialMonotonicNanos + adjustmentNanos
373 }
374 }
375 }, runSemaphore.withPermits(1))
376
377 function adjust(input: Duration.Input) {
378 const duration = Duration.fromInputUnsafe(input)
379 const millis = Duration.toMillis(duration)
380 const nanos = Number.isFinite(millis) ? Duration.toNanosUnsafe(duration) : undefined
381 return warningDone.pipe(Effect.andThen(run((timestamp) => timestamp + millis, nanos)))
382 }
383
384 function setTime(timestamp: number) {
385 return warningDone.pipe(Effect.andThen(run(() => timestamp)))
386 }
387
388 yield* Effect.addFinalizer(() => warningDone)
389
390 return {
391 currentTimeMillisUnsafe,
392 currentTimeNanosUnsafe,
393 monotonicTimeNanosUnsafe,
394 currentTimeMillis,
395 currentTimeNanos,
396 monotonicTimeNanos,
397 adjust,
398 setTime,
399 sleep,
400 withLive
401 }
402})
403
404/**
405 * Creates a `Layer` which constructs a `TestClock`.
406 *
407 * **Example** (Providing a test clock layer)
408 *
409 * ```ts import.meta.vitest
410 * import { Effect } from "effect"
411 * import { TestClock } from "effect/testing"
412 *
413 * // Create a TestClock layer
414 * const testClockLayer = TestClock.layer()
415 *
416 * // Create a TestClock layer with custom options
417 * const customTestClockLayer = TestClock.layer({
418 * warningDelay: "5 seconds"
419 * })
420 *
421 * const program = Effect.gen(function*() {
422 * // Use the layer in your program
423 * yield* TestClock.adjust("1 hour")
424 * return yield* TestClock.testClockWith((testClock) =>
425 * Effect.succeed(testClock.currentTimeMillisUnsafe())
426 * )
427 * })
428 *
429 * await Effect.runPromise(Effect.provide(program, testClockLayer)) // => 3_600_000
430 * await Effect.runPromise(Effect.provide(program, customTestClockLayer)) // => 3_600_000
431 * ```
432 *
433 * @category layers
434 * @since 4.0.0
435 */
436export const layer: (options?: TestClock.Options) => Layer.Layer<TestClock> = flow(
437 make,
438 Layer.effect(Clock.Clock)
439) as any
440
441/**
442 * Retrieves the `TestClock` service for this test and uses it to run the
443 * specified workflow.
444 *
445 * **Example** (Accessing the test clock)
446 *
447 * ```ts import.meta.vitest
448 * import { Effect } from "effect"
449 * import { TestClock } from "effect/testing"
450 *
451 * const program = Effect.gen(function*() {
452 * // Use testClockWith to access the TestClock instance
453 * const currentTime = yield* TestClock.testClockWith((testClock) =>
454 * Effect.succeed(testClock.currentTimeMillisUnsafe())
455 * )
456 *
457 * // Adjust time using the TestClock instance
458 * yield* TestClock.testClockWith((testClock) => testClock.adjust("2 hours"))
459 *
460 * currentTime // => 0
461 * })
462 *
463 * await Effect.runPromise(Effect.provide(program, TestClock.layer()))
464 * ```
465 *
466 * @category testing
467 * @since 2.0.0
468 */
469export const testClockWith = <A, E, R>(
470 f: (testClock: TestClock) => Effect.Effect<A, E, R>
471): Effect.Effect<A, E, R> => Effect.withFiber((fiber) => f(fiber.getRef(Clock.Clock) as TestClock))
472
473/**
474 * Accesses a `TestClock` instance in the context and increments the time
475 * by the specified duration, running any actions scheduled for on or before
476 * the new time in order.
477 *
478 * **Example** (Advancing the test clock)
479 *
480 * ```ts import.meta.vitest
481 * import { Effect, Fiber } from "effect"
482 * import { TestClock } from "effect/testing"
483 *
484 * const program = Effect.gen(function*() {
485 * let executed = false
486 *
487 * // Fork an effect that sleeps for 30 minutes
488 * const fiber = yield* Effect.gen(function*() {
489 * yield* Effect.sleep("30 minutes")
490 * executed = true
491 * }).pipe(Effect.forkChild)
492 *
493 * // Advance the clock by 30 minutes
494 * yield* TestClock.adjust("30 minutes")
495 * yield* Fiber.join(fiber)
496 *
497 * // The effect should now be executed
498 * executed // => true
499 * })
500 *
501 * await Effect.runPromise(Effect.provide(program, TestClock.layer()))
502 * ```
503 *
504 * @category testing
505 * @since 2.0.0
506 */
507export const adjust = (duration: Duration.Input): Effect.Effect<void> =>
508 testClockWith((testClock) => testClock.adjust(duration))
509
510/**
511 * Sets the current clock time to the specified `timestamp`. Any effects that
512 * were scheduled to occur on or before the new time will be run in order.
513 *
514 * **Example** (Setting the test clock time)
515 *
516 * ```ts import.meta.vitest
517 * import { Duration, Effect, Fiber } from "effect"
518 * import { TestClock } from "effect/testing"
519 *
520 * const program = Effect.gen(function*() {
521 * let executed = false
522 *
523 * // Fork an effect that sleeps for 2 hours
524 * const fiber = yield* Effect.gen(function*() {
525 * yield* Effect.sleep("2 hours")
526 * executed = true
527 * }).pipe(Effect.forkChild)
528 *
529 * // Set the clock to a specific timestamp (2 hours from epoch)
530 * const targetTime = Duration.toMillis(Duration.hours(2))
531 * yield* TestClock.setTime(targetTime)
532 * yield* Fiber.join(fiber)
533 *
534 * // The effect should now be executed
535 * executed // => true
536 * })
537 *
538 * await Effect.runPromise(Effect.provide(program, TestClock.layer()))
539 * ```
540 *
541 * @category testing
542 * @since 2.0.0
543 */
544export const setTime = (timestamp: number): Effect.Effect<void> =>
545 testClockWith((testClock) => testClock.setTime(timestamp))
546
547/**
548 * Executes the specified effect with the live `Clock` instead of the
549 * `TestClock`.
550 *
551 * **Example** (Running with the live clock)
552 *
553 * ```ts import.meta.vitest
554 * import { Clock, Effect } from "effect"
555 * import { TestClock } from "effect/testing"
556 *
557 * const program = Effect.gen(function*() {
558 * // Get the current test time (starts at epoch)
559 * const testTime = yield* Clock.currentTimeMillis
560 * testTime // => 0
561 *
562 * // Get the actual system time using withLive
563 * const realTime = yield* TestClock.withLive(Clock.currentTimeMillis)
564 * Number.isFinite(realTime) // => true
565 *
566 * // Advance test time
567 * yield* TestClock.adjust("1 hour")
568 *
569 * // Test time is now 1 hour ahead
570 * const newTestTime = yield* Clock.currentTimeMillis
571 * newTestTime // => 3_600_000
572 * })
573 *
574 * await Effect.runPromise(Effect.provide(program, TestClock.layer()))
575 * ```
576 *
577 * @category testing
578 * @since 4.0.0
579 */
580export const withLive = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
581 testClockWith((testClock) => testClock.withLive(effect))
582
583/**
584 * `WarningState` describes the state of the warning message that is displayed
585 * if a test is using time but is not advancing the `TestClock`.
586 *
587 * The possible states are:
588 * - `Start` if a test has not used time yet
589 * - `Pending` if a test has used time but has not adjusted the `TestClock`
590 * - `Done` if a test has adjusted the `TestClock` or the warning message
591 * has already been displayed.
592 */
593type WarningState = Data.TaggedEnum<{
594 /**
595 * The `WarningState` which indicates that a test has not yet used time.
596 */
597 readonly Start: {}
598 /**
599 * The `WarningState` which indicates that a test has used time but has not
600 * adjusted the `TestClock`.
601 *
602 * The `Pending` state also includes a reference to the fiber that will
603 * display the warning message.
604 */
605 readonly Pending: {
606 readonly fiber: Fiber.Fiber<void, unknown>
607 }
608 /**
609 * The `WarningState` which indicates that a test has used time, or that the
610 * warning message has already been displayed.
611 */
612 readonly Done: {}
613}>
614const WarningState = Data.taggedEnum<WarningState>()