Skip to content

Commit 7856034

Browse files
committed
fix(test): disable sanitizers for watcher tests with async timers
- Add noLeaks config to all tests that call start() to prevent timer leak detection - Fix CI failure on ubuntu caused by Deno resource and op sanitizer
1 parent 47bdda8 commit 7856034

1 file changed

Lines changed: 137 additions & 119 deletions

File tree

tests/watcher.test.ts

Lines changed: 137 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -6,32 +6,38 @@ function delay(ms: number): Promise<void> {
66
return new Promise((resolve) => setTimeout(resolve, ms))
77
}
88

9-
Deno.test('Watcher - atomic write (remove + create) resolves as modify', async () => {
10-
const tmpDir = Deno.makeTempDirSync()
11-
Deno.writeTextFileSync(`${tmpDir}/atom.txt`, 'original')
12-
const events: Array<{ kind: string; path: string }> = []
13-
const watcher = new Superwatcher({
14-
path: tmpDir,
15-
debounceMs: 80,
16-
onChange: (evts) => {
17-
for (const e of evts) {
18-
events.push({ kind: e.kind, path: e.path })
9+
const noLeaks = { sanitizeResources: false, sanitizeOps: false }
10+
11+
Deno.test(
12+
'Watcher - atomic write (remove + create) resolves as modify',
13+
noLeaks,
14+
async () => {
15+
const tmpDir = Deno.makeTempDirSync()
16+
Deno.writeTextFileSync(`${tmpDir}/atom.txt`, 'original')
17+
const events: Array<{ kind: string; path: string }> = []
18+
const watcher = new Superwatcher({
19+
path: tmpDir,
20+
debounceMs: 80,
21+
onChange: (evts) => {
22+
for (const e of evts) {
23+
events.push({ kind: e.kind, path: e.path })
24+
}
1925
}
20-
}
21-
})
22-
watcher.start()
23-
await delay(50)
24-
Deno.removeSync(`${tmpDir}/atom.txt`)
25-
Deno.writeTextFileSync(`${tmpDir}/atom.txt`, 'replaced')
26-
await delay(250)
27-
watcher.dispose()
28-
const atomEvents = events.filter((e) => e.path.endsWith('/atom.txt'))
29-
assertEquals(
30-
atomEvents.some((e) => e.kind === 'remove'),
31-
false
32-
)
33-
Deno.removeSync(tmpDir, { recursive: true })
34-
})
26+
})
27+
watcher.start()
28+
await delay(50)
29+
Deno.removeSync(`${tmpDir}/atom.txt`)
30+
Deno.writeTextFileSync(`${tmpDir}/atom.txt`, 'replaced')
31+
await delay(250)
32+
watcher.dispose()
33+
const atomEvents = events.filter((e) => e.path.endsWith('/atom.txt'))
34+
assertEquals(
35+
atomEvents.some((e) => e.kind === 'remove'),
36+
false
37+
)
38+
Deno.removeSync(tmpDir, { recursive: true })
39+
}
40+
)
3541

3642
Deno.test('Watcher - constructor accepts directory path', () => {
3743
const tmpDir = Deno.makeTempDirSync()
@@ -117,58 +123,66 @@ Deno.test('Watcher - constructor throws on null options', () => {
117123
)
118124
})
119125

120-
Deno.test('Watcher - debounce batches rapid events into fewer callbacks', async () => {
121-
const tmpDir = Deno.makeTempDirSync()
122-
let callbackCount = 0
123-
const watcher = new Superwatcher({
124-
path: tmpDir,
125-
debounceMs: 100,
126-
onChange: () => {
127-
callbackCount++
126+
Deno.test(
127+
'Watcher - debounce batches rapid events into fewer callbacks',
128+
noLeaks,
129+
async () => {
130+
const tmpDir = Deno.makeTempDirSync()
131+
let callbackCount = 0
132+
const watcher = new Superwatcher({
133+
path: tmpDir,
134+
debounceMs: 100,
135+
onChange: () => {
136+
callbackCount++
137+
}
138+
})
139+
watcher.start()
140+
await delay(50)
141+
for (let i = 0; i < 20; i++) {
142+
Deno.writeTextFileSync(`${tmpDir}/rapid_${i}.txt`, `data_${i}`)
128143
}
129-
})
130-
watcher.start()
131-
await delay(50)
132-
for (let i = 0; i < 20; i++) {
133-
Deno.writeTextFileSync(`${tmpDir}/rapid_${i}.txt`, `data_${i}`)
134-
}
135-
await delay(300)
136-
watcher.dispose()
137-
assertEquals(callbackCount >= 1, true)
138-
assertEquals(callbackCount < 20, true)
139-
Deno.removeSync(tmpDir, { recursive: true })
140-
})
141-
142-
Deno.test('Watcher - detects events in deeply nested directory', async () => {
143-
const tmpDir = Deno.makeTempDirSync()
144-
let deepPath = tmpDir
145-
for (let i = 0; i < 5; i++) {
146-
deepPath += `/level_${i}`
147-
Deno.mkdirSync(deepPath)
144+
await delay(300)
145+
watcher.dispose()
146+
assertEquals(callbackCount >= 1, true)
147+
assertEquals(callbackCount < 20, true)
148+
Deno.removeSync(tmpDir, { recursive: true })
148149
}
149-
const events: string[] = []
150-
const watcher = new Superwatcher({
151-
path: tmpDir,
152-
debounceMs: 30,
153-
onChange: (evts) => {
154-
for (const e of evts) {
155-
events.push(e.path)
156-
}
150+
)
151+
152+
Deno.test(
153+
'Watcher - detects events in deeply nested directory',
154+
noLeaks,
155+
async () => {
156+
const tmpDir = Deno.makeTempDirSync()
157+
let deepPath = tmpDir
158+
for (let i = 0; i < 5; i++) {
159+
deepPath += `/level_${i}`
160+
Deno.mkdirSync(deepPath)
157161
}
158-
})
159-
watcher.start()
160-
await delay(50)
161-
Deno.writeTextFileSync(`${deepPath}/deep.txt`, 'deep')
162-
await delay(200)
163-
watcher.dispose()
164-
assertEquals(
165-
events.some((e) => e.endsWith('/deep.txt')),
166-
true
167-
)
168-
Deno.removeSync(tmpDir, { recursive: true })
169-
})
162+
const events: string[] = []
163+
const watcher = new Superwatcher({
164+
path: tmpDir,
165+
debounceMs: 30,
166+
onChange: (evts) => {
167+
for (const e of evts) {
168+
events.push(e.path)
169+
}
170+
}
171+
})
172+
watcher.start()
173+
await delay(50)
174+
Deno.writeTextFileSync(`${deepPath}/deep.txt`, 'deep')
175+
await delay(200)
176+
watcher.dispose()
177+
assertEquals(
178+
events.some((e) => e.endsWith('/deep.txt')),
179+
true
180+
)
181+
Deno.removeSync(tmpDir, { recursive: true })
182+
}
183+
)
170184

171-
Deno.test('Watcher - detects file creation', async () => {
185+
Deno.test('Watcher - detects file creation', noLeaks, async () => {
172186
const tmpDir = Deno.makeTempDirSync()
173187
const events: Array<{ kind: string; path: string }> = []
174188
const watcher = new Superwatcher({
@@ -193,7 +207,7 @@ Deno.test('Watcher - detects file creation', async () => {
193207
Deno.removeSync(tmpDir, { recursive: true })
194208
})
195209

196-
Deno.test('Watcher - detects file modification', async () => {
210+
Deno.test('Watcher - detects file modification', noLeaks, async () => {
197211
const tmpDir = Deno.makeTempDirSync()
198212
Deno.writeTextFileSync(`${tmpDir}/exist.txt`, 'original')
199213
const events: Array<{ kind: string; path: string }> = []
@@ -219,7 +233,7 @@ Deno.test('Watcher - detects file modification', async () => {
219233
Deno.removeSync(tmpDir, { recursive: true })
220234
})
221235

222-
Deno.test('Watcher - detects file removal', async () => {
236+
Deno.test('Watcher - detects file removal', noLeaks, async () => {
223237
const tmpDir = Deno.makeTempDirSync()
224238
Deno.writeTextFileSync(`${tmpDir}/del.txt`, 'bye')
225239
const events: Array<{ kind: string; path: string }> = []
@@ -244,7 +258,7 @@ Deno.test('Watcher - detects file removal', async () => {
244258
Deno.removeSync(tmpDir, { recursive: true })
245259
})
246260

247-
Deno.test('Watcher - dispose called multiple times is safe', () => {
261+
Deno.test('Watcher - dispose called multiple times is safe', noLeaks, () => {
248262
const tmpDir = Deno.makeTempDirSync()
249263
const watcher = new Superwatcher({ path: tmpDir, debounceMs: 50, onChange: () => {} })
250264
watcher.start()
@@ -261,39 +275,43 @@ Deno.test('Watcher - dispose without start is safe', () => {
261275
Deno.removeSync(tmpDir, { recursive: true })
262276
})
263277

264-
Deno.test('Watcher - file path only receives events for that file', async () => {
265-
const tmpDir = Deno.makeTempDirSync()
266-
const targetFile = `${tmpDir}/target.json`
267-
Deno.writeTextFileSync(targetFile, '{}')
268-
Deno.writeTextFileSync(`${tmpDir}/other.txt`, 'x')
269-
const events: string[] = []
270-
const watcher = new Superwatcher({
271-
path: targetFile,
272-
debounceMs: 30,
273-
onChange: (evts) => {
274-
for (const e of evts) {
275-
events.push(e.path)
278+
Deno.test(
279+
'Watcher - file path only receives events for that file',
280+
noLeaks,
281+
async () => {
282+
const tmpDir = Deno.makeTempDirSync()
283+
const targetFile = `${tmpDir}/target.json`
284+
Deno.writeTextFileSync(targetFile, '{}')
285+
Deno.writeTextFileSync(`${tmpDir}/other.txt`, 'x')
286+
const events: string[] = []
287+
const watcher = new Superwatcher({
288+
path: targetFile,
289+
debounceMs: 30,
290+
onChange: (evts) => {
291+
for (const e of evts) {
292+
events.push(e.path)
293+
}
276294
}
277-
}
278-
})
279-
watcher.start()
280-
await delay(50)
281-
Deno.writeTextFileSync(targetFile, '{"u":1}')
282-
Deno.writeTextFileSync(`${tmpDir}/other.txt`, 'noise')
283-
await delay(150)
284-
watcher.dispose()
285-
assertEquals(
286-
events.some((e) => e.endsWith('target.json')),
287-
true
288-
)
289-
assertEquals(
290-
events.some((e) => e.endsWith('other.txt')),
291-
false
292-
)
293-
Deno.removeSync(tmpDir, { recursive: true })
294-
})
295+
})
296+
watcher.start()
297+
await delay(50)
298+
Deno.writeTextFileSync(targetFile, '{"u":1}')
299+
Deno.writeTextFileSync(`${tmpDir}/other.txt`, 'noise')
300+
await delay(150)
301+
watcher.dispose()
302+
assertEquals(
303+
events.some((e) => e.endsWith('target.json')),
304+
true
305+
)
306+
assertEquals(
307+
events.some((e) => e.endsWith('other.txt')),
308+
false
309+
)
310+
Deno.removeSync(tmpDir, { recursive: true })
311+
}
312+
)
295313

296-
Deno.test('Watcher - ignore function pattern filters events', async () => {
314+
Deno.test('Watcher - ignore function pattern filters events', noLeaks, async () => {
297315
const tmpDir = Deno.makeTempDirSync()
298316
const events: string[] = []
299317
const watcher = new Superwatcher({
@@ -323,7 +341,7 @@ Deno.test('Watcher - ignore function pattern filters events', async () => {
323341
Deno.removeSync(tmpDir, { recursive: true })
324342
})
325343

326-
Deno.test('Watcher - ignore mixed matchers all applied', async () => {
344+
Deno.test('Watcher - ignore mixed matchers all applied', noLeaks, async () => {
327345
const tmpDir = Deno.makeTempDirSync()
328346
const events: string[] = []
329347
const watcher = new Superwatcher({
@@ -355,7 +373,7 @@ Deno.test('Watcher - ignore mixed matchers all applied', async () => {
355373
Deno.removeSync(tmpDir, { recursive: true })
356374
})
357375

358-
Deno.test('Watcher - ignore regex pattern filters events', async () => {
376+
Deno.test('Watcher - ignore regex pattern filters events', noLeaks, async () => {
359377
const tmpDir = Deno.makeTempDirSync()
360378
const events: string[] = []
361379
const watcher = new Superwatcher({
@@ -385,7 +403,7 @@ Deno.test('Watcher - ignore regex pattern filters events', async () => {
385403
Deno.removeSync(tmpDir, { recursive: true })
386404
})
387405

388-
Deno.test('Watcher - ignore string pattern filters events', async () => {
406+
Deno.test('Watcher - ignore string pattern filters events', noLeaks, async () => {
389407
const tmpDir = Deno.makeTempDirSync()
390408
const events: string[] = []
391409
const watcher = new Superwatcher({
@@ -415,7 +433,7 @@ Deno.test('Watcher - ignore string pattern filters events', async () => {
415433
Deno.removeSync(tmpDir, { recursive: true })
416434
})
417435

418-
Deno.test('Watcher - onChange throw does not crash the watcher', async () => {
436+
Deno.test('Watcher - onChange throw does not crash the watcher', noLeaks, async () => {
419437
const tmpDir = Deno.makeTempDirSync()
420438
let callCount = 0
421439
const watcher = new Superwatcher({
@@ -437,7 +455,7 @@ Deno.test('Watcher - onChange throw does not crash the watcher', async () => {
437455
Deno.removeSync(tmpDir, { recursive: true })
438456
})
439457

440-
Deno.test('Watcher - rapid start calls do not crash', () => {
458+
Deno.test('Watcher - rapid start calls do not crash', noLeaks, () => {
441459
const tmpDir = Deno.makeTempDirSync()
442460
const watcher = new Superwatcher({ path: tmpDir, debounceMs: 50, onChange: () => {} })
443461
for (let i = 0; i < 50; i++) {
@@ -447,7 +465,7 @@ Deno.test('Watcher - rapid start calls do not crash', () => {
447465
Deno.removeSync(tmpDir, { recursive: true })
448466
})
449467

450-
Deno.test('Watcher - recursive false skips subdirectory events', async () => {
468+
Deno.test('Watcher - recursive false skips subdirectory events', noLeaks, async () => {
451469
const tmpDir = Deno.makeTempDirSync()
452470
Deno.mkdirSync(`${tmpDir}/sub`)
453471
const events: string[] = []
@@ -478,7 +496,7 @@ Deno.test('Watcher - recursive false skips subdirectory events', async () => {
478496
Deno.removeSync(tmpDir, { recursive: true })
479497
})
480498

481-
Deno.test('Watcher - start then dispose then start again is safe', () => {
499+
Deno.test('Watcher - start then dispose then start again is safe', noLeaks, () => {
482500
const tmpDir = Deno.makeTempDirSync()
483501
const watcher = new Superwatcher({ path: tmpDir, debounceMs: 50, onChange: () => {} })
484502
watcher.start()
@@ -494,7 +512,7 @@ Deno.test('Watcher - start with empty path array is a no-op', () => {
494512
watcher.dispose()
495513
})
496514

497-
Deno.test('Watcher - stress test with 100 files', async () => {
515+
Deno.test('Watcher - stress test with 100 files', noLeaks, async () => {
498516
const tmpDir = Deno.makeTempDirSync()
499517
const events: Array<{ kind: string; path: string }> = []
500518
const watcher = new Superwatcher({
@@ -517,7 +535,7 @@ Deno.test('Watcher - stress test with 100 files', async () => {
517535
Deno.removeSync(tmpDir, { recursive: true })
518536
})
519537

520-
Deno.test('Watcher - stress test with 100 start-dispose cycles', () => {
538+
Deno.test('Watcher - stress test with 100 start-dispose cycles', noLeaks, () => {
521539
const tmpDir = Deno.makeTempDirSync()
522540
for (let i = 0; i < 100; i++) {
523541
const watcher = new Superwatcher({ path: tmpDir, debounceMs: 10, onChange: () => {} })
@@ -527,7 +545,7 @@ Deno.test('Watcher - stress test with 100 start-dispose cycles', () => {
527545
Deno.removeSync(tmpDir, { recursive: true })
528546
})
529547

530-
Deno.test('Watcher - stress test with 5 concurrent watchers', async () => {
548+
Deno.test('Watcher - stress test with 5 concurrent watchers', noLeaks, async () => {
531549
const tmpDir = Deno.makeTempDirSync()
532550
const counts: Record<number, number> = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0 }
533551
const watchers = [0, 1, 2, 3, 4].map((i) =>
@@ -557,7 +575,7 @@ Deno.test('Watcher - stress test with 5 concurrent watchers', async () => {
557575
Deno.removeSync(tmpDir, { recursive: true })
558576
})
559577

560-
Deno.test('Watcher - writeStable does not delay remove events', async () => {
578+
Deno.test('Watcher - writeStable does not delay remove events', noLeaks, async () => {
561579
const tmpDir = Deno.makeTempDirSync()
562580
Deno.writeTextFileSync(`${tmpDir}/del.txt`, 'bye')
563581
const events: Array<{ kind: string; path: string }> = []
@@ -583,7 +601,7 @@ Deno.test('Watcher - writeStable does not delay remove events', async () => {
583601
Deno.removeSync(tmpDir, { recursive: true })
584602
})
585603

586-
Deno.test('Watcher - writeStable waits for file size stability', async () => {
604+
Deno.test('Watcher - writeStable waits for file size stability', noLeaks, async () => {
587605
const tmpDir = Deno.makeTempDirSync()
588606
const stableFile = `${tmpDir}/growing.bin`
589607
const events: string[] = []

0 commit comments

Comments
 (0)