1import { afterAll, assert, describe, it } from "@effect/vitest"
2import { Effect, Layer } from "effect"
3
4const pendingReleases: Array<Promise<void>> = []
5
6// Drain the real timers even when a broken adapter abandons a finalizer.
7afterAll(() => Promise.all(pendingReleases))
8
9// The release takes real time: the TestClock is not advanced once a test has timed out.
10const resource = (events: Array<string>, name: string) =>
11 Effect.acquireRelease(
12 Effect.sync(() => {
13 events.push(`${name}:acquired`)
14 }),
15 () =>
16 Effect.promise(() => {
17 events.push(`${name}:release:start`)
18 const pending = new Promise<void>((resolve) =>
19 setTimeout(() => {
20 events.push(`${name}:release:end`)
21 resolve()
22 }, 250)
23 )
24 pendingReleases.push(pending)
25 return pending
26 })
27 )
28
29for (const mode of ["effect", "live"] as const) {
30 describe(`it.${mode} timeout finalizers`, { concurrent: false }, () => {
31 const events: Array<string> = []
32 let signal: AbortSignal | undefined
33
34 it[mode].fails("times out while holding a resource", (ctx) =>
35 Effect.gen(function*() {
36 signal = ctx.signal
37 yield* resource(events, "test")
38 return yield* Effect.never
39 }), { timeout: 75 })
40
41 it("awaits the finalizer before starting the next test", () => {
42 events.push("next:start")
43 assert.isTrue(signal?.aborted)
44 assert.deepStrictEqual(events, ["test:acquired", "test:release:start", "test:release:end", "next:start"])
45 })
46 })
47}
48
49describe("it.effect timeout retry finalizers", { concurrent: false }, () => {
50 const events: Array<string> = []
51 const abortedAtStart: Array<boolean> = []
52 let attempts = 0
53
54 it.effect.fails("retries after timing out while holding a resource", (ctx) =>
55 Effect.gen(function*() {
56 const attempt = ++attempts
57 abortedAtStart.push(ctx.signal.aborted)
58 yield* resource(events, `attempt${attempt}`)
59 return yield* Effect.never
60 }), { timeout: 75, retry: 1 })
61
62 it("awaits the retry's finalizer before starting the next test", () => {
63 events.push("next:start")
64 assert.strictEqual(attempts, 2)
65 // Vitest reuses the signal aborted by the first attempt's timeout.
66 assert.deepStrictEqual(abortedAtStart, [false, true])
67 assert.deepStrictEqual(events, [
68 "attempt1:acquired",
69 "attempt1:release:start",
70 "attempt1:release:end",
71 "attempt2:acquired",
72 "attempt2:release:start",
73 "attempt2:release:end",
74 "next:start"
75 ])
76 })
77})
78
79describe("it.layer timeout finalizers", { concurrent: false }, () => {
80 const events: Array<string> = []
81 let signal: AbortSignal | undefined
82
83 it.layer(Layer.effectDiscard(resource(events, "layer")))((it) => {
84 it.effect.fails("times out in the last test using the layer", (ctx) =>
85 Effect.gen(function*() {
86 signal = ctx.signal
87 yield* resource(events, "test")
88 return yield* Effect.never
89 }), { timeout: 75 })
90 })
91
92 it("awaits both test and layer finalizers before starting the next test", () => {
93 events.push("next:start")
94 assert.isTrue(signal?.aborted)
95 assert.deepStrictEqual(events, [
96 "layer:acquired",
97 "test:acquired",
98 "test:release:start",
99 "test:release:end",
100 "layer:release:start",
101 "layer:release:end",
102 "next:start"
103 ])
104 })
105})