Skip to content
Source
Sign inSign up

no-real-time-wait-in-tests.test.ts

268 lines7.1 KB

1import { assert, describe } from "@effect/vitest";
2​
3import { createOxlintRuleHarness } from "../test/utils.ts";
4​
5const rule = createOxlintRuleHarness("t3code/no-real-time-wait-in-tests", {
6 filename: "fixture.test.ts",
7});
8​
9const REAL_CLOCK = /Real-clock wait in a test/;
10const UNDRIVEN = /never fire without `TestClock.adjust`/;
11const MIXED = /they never meet/;
12​
13describe("t3code/no-real-time-wait-in-tests", () => {
14 rule.valid(
15 "allows TestClock-driven sleeps and timeout safety nets",
16 `
17 import { it } from "@effect/vitest";
18 import * as Effect from "effect/Effect";
19 import * as TestClock from "effect/testing/TestClock";
20​
21 it.effect("fires after a minute", () =>
22 Effect.gen(function* () {
23 const fiber = yield* Effect.sleep("1 minute").pipe(Effect.forkChild);
24 yield* TestClock.adjust("1 minute");
25 yield* fiber.await;
26 }),
27 );
28​
29 it.live("spawns a fixture", () => Effect.void.pipe(Effect.timeout("5 seconds")));
30 `,
31 );
32​
33 rule.valid(
34 "ignores sleeps inside strings and child-process script templates",
35 `
36 import { it } from "@effect/vitest";
37 import * as Effect from "effect/Effect";
38​
39 it.live("runs a script", () =>
40 Effect.sync(() => [
41 "setTimeout(() => process.exit(0), 50)",
42 \`await new Promise((resolve) => setTimeout(resolve, 10)); Effect.sleep(1)\`,
43 ]),
44 );
45 `,
46 );
47​
48 rule.valid(
49 "ignores shared helpers in files that drive the TestClock",
50 `
51 import { it } from "@effect/vitest";
52 import * as Effect from "effect/Effect";
53 import * as TestClock from "effect/testing/TestClock";
54​
55 const slowStep = (delay: number) =>
56 Effect.gen(function* () {
57 if (delay > 0) yield* Effect.sleep(delay);
58 });
59​
60 it.effect("advances", () => slowStep(5).pipe(Effect.andThen(TestClock.adjust(5))));
61 `,
62 );
63​
64 rule.valid(
65 "ignores bare setTimeout callbacks outside promise executors",
66 `
67 import { it } from "vitest";
68​
69 it("schedules work", () => {
70 const handle = setTimeout(() => {}, 1000);
71 clearTimeout(handle);
72 });
73 `,
74 );
75​
76 rule.invalid(
77 "reports Effect.sleep under it.live",
78 `
79 import { it } from "@effect/vitest";
80 import * as Effect from "effect/Effect";
81​
82 it.live("waits", () => Effect.sleep("50 millis"));
83 `,
84 (output) => {
85 assert.match(output, REAL_CLOCK);
86 },
87 );
88​
89 rule.invalid(
90 "reports retry schedules under curried testers",
91 `
92 import { it } from "@effect/vitest";
93 import * as Effect from "effect/Effect";
94 import * as Schedule from "effect/Schedule";
95​
96 it.live.skipIf(false)("polls", () =>
97 Effect.void.pipe(Effect.retry(Schedule.spaced("10 millis"))),
98 );
99 `,
100 (output) => {
101 assert.match(output, REAL_CLOCK);
102 },
103 );
104​
105 rule.invalid(
106 "reports Effect.delay under plain it",
107 `
108 import { it } from "vitest";
109 import * as Effect from "effect/Effect";
110​
111 it("delays", async () => {
112 await Effect.runPromise(Effect.void.pipe(Effect.delay("10 millis")));
113 });
114 `,
115 (output) => {
116 assert.match(output, REAL_CLOCK);
117 },
118 );
119​
120 rule.invalid(
121 "reports it.effect inside an excludeTestServices layer as a real-clock wait",
122 `
123 import { it } from "@effect/vitest";
124 import * as Effect from "effect/Effect";
125 import * as Layer from "effect/Layer";
126 import * as TestClock from "effect/testing/TestClock";
127​
128 it.layer(Layer.empty, { excludeTestServices: true })("live services", (it) => {
129 it.effect("waits", () => Effect.sleep("10 millis"));
130 });
131​
132 it.effect("advances", () => TestClock.adjust(1));
133 `,
134 (output) => {
135 assert.match(output, REAL_CLOCK);
136 },
137 );
138​
139 rule.invalid(
140 "reports it.effect sleeps in files that never adjust the TestClock",
141 `
142 import { it } from "@effect/vitest";
143 import * as Effect from "effect/Effect";
144​
145 it.effect("hangs", () => Effect.sleep("200 millis"));
146 `,
147 (output) => {
148 assert.match(output, UNDRIVEN);
149 },
150 );
151​
152 rule.invalid(
153 "reports polling helpers even when the file drives the TestClock",
154 `
155 import { it } from "@effect/vitest";
156 import * as Effect from "effect/Effect";
157 import * as TestClock from "effect/testing/TestClock";
158​
159 let ready = false;
160 const waitUntilReady = Effect.gen(function* () {
161 while (!ready) {
162 yield* Effect.sleep("10 millis");
163 }
164 });
165​
166 it.effect("advances", () => TestClock.adjust(1));
167 it.live("waits", () => waitUntilReady);
168 `,
169 (output) => {
170 assert.match(output, REAL_CLOCK);
171 },
172 );
173​
174 rule.invalid(
175 "reports promise-wrapped setTimeout in vitest tests",
176 `
177 import { it } from "vitest";
178​
179 it.each([1])("waits %s", async () => {
180 await new Promise((resolve) => setTimeout(resolve, 10));
181 });
182 `,
183 (output) => {
184 assert.match(output, REAL_CLOCK);
185 assert.notMatch(output, MIXED);
186 },
187 );
188​
189 rule.invalid(
190 "reports promise-wrapped setTimeout under it.effect as a mixed clock",
191 `
192 import { it } from "@effect/vitest";
193 import * as Effect from "effect/Effect";
194​
195 it.effect("waits", () =>
196 Effect.promise(() => new Promise<void>((resolve) => setTimeout(resolve, 10))),
197 );
198 `,
199 (output) => {
200 assert.match(output, REAL_CLOCK);
201 assert.match(output, MIXED);
202 },
203 );
204​
205 rule.invalid(
206 "reports TestClock sleeps when TestClock.adjust appears only in a comment",
207 `
208 import { it } from "@effect/vitest";
209 import * as Effect from "effect/Effect";
210​
211 // Callers would normally TestClock.adjust("1 minute") here.
212 const note = "TestClock.setTime";
213 it.effect("hangs", () => Effect.sleep("1 minute"));
214 `,
215 (output) => {
216 assert.match(output, UNDRIVEN);
217 },
218 );
219​
220 rule.invalid(
221 "reports timers hidden in module-level helpers",
222 `
223 import { setTimeout as delay } from "node:timers/promises";
224 import { it } from "vitest";
225​
226 const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
227 const pause = () => delay(10);
228​
229 it("waits", async () => {
230 await sleep(10);
231 await pause();
232 });
233 `,
234 (output) => {
235 assert.equal(output.match(/Real-clock wait in a test/g)?.length, 2);
236 assert.notMatch(output, MIXED);
237 },
238 );
239​
240 rule.invalid(
241 "reports awaited timers/promises sleeps",
242 `
243 import * as NodeTimers from "node:timers/promises";
244 import { setTimeout as sleep } from "node:timers/promises";
245 import { it } from "vitest";
246​
247 it("waits", async () => {
248 await NodeTimers.setTimeout(10);
249 await sleep(10);
250 });
251 `,
252 (output) => {
253 assert.equal(output.match(/Real-clock wait in a test/g)?.length, 2);
254 },
255 );
256});
257​
258const productionRule = createOxlintRuleHarness("t3code/no-real-time-wait-in-tests");
259​
260productionRule.valid(
261 "ignores non-test files",
262 `
263 import * as Effect from "effect/Effect";
264​
265 const it = { live: (_name: string, run: () => unknown) => run() };
266 it.live("waits", () => Effect.sleep("1 second"));
267 `,
268);