1import { assert, describe } from "@effect/vitest";
2
3import { createOxlintRuleHarness } from "../test/utils.ts";
4
5const rule = createOxlintRuleHarness("t3code/no-polling-loop-in-tests", {
6 filename: "fixture.test.ts",
7});
8
9describe("t3code/no-polling-loop-in-tests", () => {
10 rule.valid(
11 "allows a fixed-count flush followed by unrelated error handling",
12 `
13 import { it } from "@effect/vitest";
14 import * as Effect from "effect/Effect";
15 it.effect("flushes", () =>
16 Effect.gen(function* () {
17 for (let i = 0; i < 3; i++) yield* Effect.yieldNow;
18 if (!ready()) return yield* Effect.die("not ready");
19 }),
20 );
21 `,
22 );
23
24 rule.valid(
25 "allows a fixed-N flush with a literal bound",
26 `
27 import * as Effect from "effect/Effect";
28
29 export const flush = Effect.gen(function* () {
30 for (let i = 0; i < 3; i++) yield* Effect.yieldNow;
31 });
32 `,
33 );
34
35 rule.valid(
36 "allows a fixed-N while flush with a counter bound",
37 `
38 import * as Effect from "effect/Effect";
39
40 export const flush = Effect.gen(function* () {
41 let i = 0;
42 while (i < 3) {
43 yield* Effect.yieldNow;
44 i++;
45 }
46 });
47 `,
48 );
49
50 rule.valid(
51 "allows a fixed-N do-while flush against a named bound",
52 `
53 import * as Effect from "effect/Effect";
54
55 export const flush = (turns: number) =>
56 Effect.gen(function* () {
57 let i = 0;
58 do {
59 yield* Effect.yieldNow;
60 i += 1;
61 } while (i < turns);
62 });
63 `,
64 );
65
66 rule.valid(
67 "allows a loop that takes from a queue",
68 `
69 import * as Effect from "effect/Effect";
70 import * as Queue from "effect/Queue";
71
72 export const until = (events: Queue.Queue<number>) =>
73 Effect.gen(function* () {
74 while (true) {
75 const event = yield* Queue.take(events);
76 if (event > 2) return event;
77 }
78 });
79 `,
80 );
81
82 rule.valid(
83 "ignores waits inside nested callbacks",
84 `
85 import * as Effect from "effect/Effect";
86
87 export const run = (items: ReadonlyArray<number>, done: () => boolean) =>
88 Effect.gen(function* () {
89 for (let i = 0; i < items.length; i++) {
90 if (done()) return;
91 yield* Effect.forkChild(Effect.gen(function* () {
92 yield* Effect.yieldNow;
93 }));
94 }
95 });
96 `,
97 );
98
99 rule.invalid(
100 "reports a budgeted yieldNow poll that dies after the loop",
101 `
102 import * as Effect from "effect/Effect";
103
104 export const awaitUntil = (predicate: () => boolean) =>
105 Effect.gen(function* () {
106 for (let attempt = 0; attempt < 5000; attempt++) {
107 if (predicate()) return;
108 yield* Effect.yieldNow;
109 }
110 return yield* Effect.die("Timed out");
111 });
112 `,
113 (output) => {
114 assert.match(output, /Await the `Deferred`/);
115 },
116 );
117
118 rule.invalid(
119 "reports a while loop spinning on a predicate",
120 `
121 import * as Effect from "effect/Effect";
122
123 export const wait = (turns: Array<number>) =>
124 Effect.gen(function* () {
125 while (turns.length === 0) yield* Effect.yieldNow;
126 });
127 `,
128 );
129
130 rule.invalid(
131 "reports a while loop bounded by a length predicate",
132 `
133 import * as Effect from "effect/Effect";
134
135 export const wait = (events: Array<number>) =>
136 Effect.gen(function* () {
137 while (events.length < 1) yield* Effect.yieldNow;
138 });
139 `,
140 );
141
142 rule.invalid(
143 "reports a comparison against an identifier the loop never advances",
144 `
145 import * as Effect from "effect/Effect";
146
147 export const wait = (state: { count: number }) =>
148 Effect.gen(function* () {
149 let seen = state.count;
150 while (seen < 3) {
151 yield* Effect.yieldNow;
152 seen = state.count;
153 }
154 });
155 `,
156 );
157
158 rule.invalid(
159 "reports a for loop bounded by a length predicate",
160 `
161 import * as Effect from "effect/Effect";
162
163 export const wait = (events: Array<number>) =>
164 Effect.gen(function* () {
165 for (; events.length < 1; ) yield* Effect.yieldNow;
166 });
167 `,
168 );
169
170 rule.invalid(
171 "reports a sleep poll with a counter bound and an early return",
172 `
173 import * as Effect from "effect/Effect";
174
175 export const waitFor = (done: () => boolean) =>
176 Effect.gen(function* () {
177 for (let attempt = 0; attempt < 240; attempt += 1) {
178 if (done()) return;
179 yield* Effect.sleep("500 millis");
180 }
181 });
182 `,
183 );
184
185 rule.invalid(
186 "reports a microtask poll keyed on the loop condition",
187 `
188 import * as Effect from "effect/Effect";
189
190 export const settle = function* (until: () => boolean) {
191 for (let attempt = 0; attempt < 30 && !until(); attempt++) {
192 yield* Effect.promise(() => Promise.resolve());
193 }
194 };
195 `,
196 );
197
198 rule.invalid(
199 "reports an awaited setTimeout poll",
200 `
201 export const waitFor = async (done: () => boolean) => {
202 do {
203 await new Promise((resolve) => setTimeout(resolve, 10));
204 } while (!done());
205 };
206 `,
207 );
208
209 rule.invalid(
210 "reports an awaited Promise.resolve poll that breaks",
211 `
212 export const waitFor = async (done: () => boolean) => {
213 while (true) {
214 if (done()) break;
215 await Promise.resolve();
216 }
217 };
218 `,
219 );
220});
221
222const productionRule = createOxlintRuleHarness("t3code/no-polling-loop-in-tests");
223
224productionRule.valid(
225 "ignores non-test files",
226 `
227 export const waitFor = async (done: () => boolean) => {
228 while (!done()) await Promise.resolve();
229 };
230 `,
231);