Repository navigation
Expand file tree
/
Copy pathWatchFileSystemAdapter.test.ts
More file actions
588 lines (524 loc) · 22.3 KB
/
Copy pathWatchFileSystemAdapter.test.ts
File metadata and controls
588 lines (524 loc) · 22.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import Watchpack from 'watchpack';
import { Async } from '@rushstack/node-core-library';
import { watchGlobAsync } from '../../plugins/FileGlobSpecifier';
import { type IWatchedFileState, type StatCallback, WatchFileSystemAdapter } from '../WatchFileSystemAdapter';
const RUN_REQUEST_TIMEOUT_MS: number = 5000;
// Long enough for a watcher to finish its first scan, and for events to arrive
const SETTLE_MS: number = 250;
const TEST_TIMEOUT_MS: number = 30000;
// An hour ago, plus some milliseconds so that watchpack sees the file system's timestamps are precise. Files get
// old mtimes so that a new watcher doesn't report a file as changed just because it was written right before
// the watcher started.
const BASE_MTIME_MS: number = Math.floor(Date.now() / 1000) * 1000 - 3600 * 1000 + 123;
// A new watcher reports a file whose mtime is later than the watcher's start time. A file that gets an mtime in
// the future is reported by the next run's watcher for certain.
const FUTURE_MTIME_OFFSET_MS: number = 60 * 1000;
// Long enough for file timestamps to show which of two events came first, even where they are coarse
const TIMESTAMP_GAP_MS: number = 20;
// A file that the glob doesn't match, in a folder that the glob reads
const NOTES_PATH: string = 'src/sub/notes.txt';
// Watchpack calls fs.lstat() through this graceful-fs instance
const watcherFs: typeof fs = jest.requireActual(
require.resolve('graceful-fs', { paths: [path.dirname(require.resolve('watchpack/package.json'))] })
);
// The adapter's calls to fs.lstatSync() look up the function on this module when they are made
const nodeFs: typeof fs = jest.requireActual('node:fs');
interface IRunOptions {
watch?: boolean;
// The glob that the run reads. The default reads every folder under src.
pattern?: string;
beforeWatch?: () => Promise<void> | void;
}
interface IRun {
changed: string[];
isRunRequested(): boolean;
waitForRunRequestAsync(): Promise<boolean>;
/**
* Waits until the run's watcher has reported the path with the explanation, and has passed the report on
* in an 'aggregated' event. Then waits SETTLE_MS more, so that later events can arrive. Returns false if the
* watcher doesn't report the path.
*/
waitForReportAsync(relativePath: string, explanation: string): Promise<boolean>;
}
describe(WatchFileSystemAdapter.name, () => {
let rootFolder: string;
let writeCount: number;
let adapter: WatchFileSystemAdapter;
let watchSpy: jest.SpyInstance;
let collectSpy: jest.SpyInstance;
let closeSpy: jest.SpyInstance;
function writeFile(relativePath: string, mtimeMs?: number): void {
const filePath: string = path.join(rootFolder, relativePath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
writeCount++;
fs.writeFileSync(filePath, `export const value: number = ${writeCount};\n`);
const mtime: Date = new Date(mtimeMs ?? BASE_MTIME_MS + writeCount * 1000);
fs.utimesSync(filePath, mtime, mtime);
}
// Drives the adapter the way TaskOperationRunner does in watch mode
async function runAsync(options: IRunOptions = {}): Promise<IRun> {
const { watch = true, pattern = 'src/**/*.ts', beforeWatch } = options;
adapter.setBaseline();
const states: Map<string, IWatchedFileState> = await watchGlobAsync(pattern, {
cwd: rootFolder,
fs: adapter
});
const changed: string[] = [];
for (const [file, state] of states) {
if (state.changed) {
changed.push(file);
}
}
changed.sort();
await beforeWatch?.();
let requested: boolean = false;
let onRequest: (() => void) | undefined;
const watcherCount: number = watchSpy.mock.contexts.length;
if (watch) {
adapter.watch(() => {
requested = true;
onRequest?.();
});
}
// The reports of the run's watcher, as of its last 'aggregated' event
const reports: Set<string> = new Set();
let onReports: (() => void) | undefined;
const watcher: Watchpack | undefined = watchSpy.mock.contexts[watcherCount];
if (watcher) {
let newReports: string[] = [];
watcher.on('change', (filePath: string, modifiedTime: number, explanation: string) => {
newReports.push(`${explanation}: ${filePath}`);
});
watcher.on('remove', (filePath: string, explanation: string) => {
newReports.push(`${explanation}: ${filePath}`);
});
// Runs after the adapter's listener, which the adapter added first
watcher.on('aggregated', () => {
for (const report of newReports) {
reports.add(report);
}
newReports = [];
onReports?.();
});
}
return {
changed,
isRunRequested: () => requested,
waitForRunRequestAsync: () =>
new Promise<boolean>((resolve: (value: boolean) => void) => {
if (requested) {
resolve(true);
return;
}
const timeout: NodeJS.Timeout = setTimeout(() => resolve(false), RUN_REQUEST_TIMEOUT_MS);
onRequest = () => {
clearTimeout(timeout);
resolve(true);
};
}),
waitForReportAsync: async (relativePath: string, explanation: string): Promise<boolean> => {
const report: string = `${explanation}: ${path.join(rootFolder, relativePath)}`;
const isReported: boolean = await new Promise<boolean>((resolve: (value: boolean) => void) => {
const timeout: NodeJS.Timeout = setTimeout(() => resolve(false), RUN_REQUEST_TIMEOUT_MS);
onReports = () => {
if (reports.has(report)) {
clearTimeout(timeout);
resolve(true);
}
};
onReports();
});
await Async.sleepAsync(SETTLE_MS);
return isReported;
}
};
}
// The first run's watcher may request a run just because the folders are new, so tests only use it to start
async function startAsync(): Promise<void> {
writeFile('src/a.ts');
writeFile('src/sub/b.ts');
const run: IRun = await runAsync();
expect(run.changed).toEqual(['src/a.ts', 'src/sub/b.ts']);
await Async.sleepAsync(SETTLE_MS);
}
// Also writes the file at NOTES_PATH. Returns the second run, whose watcher has scanned the files without
// requesting a run. The next run collects that watcher's times.
async function startWatchingAsync(): Promise<IRun> {
writeFile(NOTES_PATH);
await startAsync();
const run2: IRun = await runAsync();
expect(run2.changed).toEqual([]);
await Async.sleepAsync(SETTLE_MS);
expect(run2.isRunRequested()).toBe(false);
return run2;
}
// Gives the file a future mtime, and waits until the run's watcher has recorded the change. Returns the mtime.
async function changeBeforeNextRunAsync(run: IRun, relativePath: string): Promise<number> {
const mtimeMs: number = Date.now() + FUTURE_MTIME_OFFSET_MS;
writeFile(relativePath, mtimeMs);
expect(await run.waitForRunRequestAsync()).toBe(true);
await Async.sleepAsync(SETTLE_MS);
return mtimeMs;
}
// From now on, the adapter sees a ctime for the file that is earlier than the run's start. A file that changed
// after the run started can have such a ctime where timestamps are coarse, or where the clock that sets them
// lags Date.now().
function backdateCtime(relativePath: string): void {
const backdatedPath: string = path.join(rootFolder, relativePath);
const lstatSync: typeof fs.lstatSync = nodeFs.lstatSync;
jest.spyOn(nodeFs, 'lstatSync').mockImplementation(((
filePath: fs.PathLike,
options?: fs.StatSyncOptions
) => {
const stats: fs.Stats | undefined = lstatSync(filePath, options) as fs.Stats | undefined;
if (stats && filePath === backdatedPath) {
stats.ctimeMs = BASE_MTIME_MS;
}
return stats;
}) as unknown as typeof fs.lstatSync);
}
// Replaces watchpack's fs.lstat(). The handler calls lstatAsync() to do the fs.lstat() and give watchpack the
// result.
function interceptWatcherLstat(handler: (filePath: string, lstatAsync: () => Promise<void>) => void): void {
const lstat: (filePath: string, callback: StatCallback) => void = watcherFs.lstat;
jest.spyOn(watcherFs, 'lstat').mockImplementation(((filePath: string, callback: StatCallback) => {
handler(filePath, async () => {
await new Promise<void>((resolve: () => void) => {
lstat(filePath, (error, stats) => {
callback(error, stats);
resolve();
});
});
});
}) as unknown as typeof watcherFs.lstat);
}
// Holds watchpack's next fs.lstat() of the file, as a slow file system can. Resolves when watchpack calls it,
// with a function that finishes it.
function holdNextWatcherLstatAsync(relativePath: string): Promise<(() => void) | undefined> {
const heldPath: string = path.join(rootFolder, relativePath);
return new Promise((resolve: (finish: (() => void) | undefined) => void) => {
const timeout: NodeJS.Timeout = setTimeout(() => resolve(undefined), RUN_REQUEST_TIMEOUT_MS);
let isHolding: boolean = false;
interceptWatcherLstat((filePath: string, lstatAsync: () => Promise<void>) => {
if (filePath === heldPath && !isHolding) {
isHolding = true;
clearTimeout(timeout);
resolve(() => {
void lstatAsync();
});
} else {
void lstatAsync();
}
});
});
}
// Holds watchpack's fs.lstat() of the folder until watchpack has finished its fs.lstat() of each of the files
function holdWatcherFolderLstat(relativeFolderPath: string, fileNames: string[]): void {
const folderPath: string = path.join(rootFolder, relativeFolderPath);
const unreadFiles: Set<string> = new Set(fileNames.map((name: string) => path.join(folderPath, name)));
let finishFolderLstat: (() => Promise<void>) | undefined;
const finishWhenFilesAreRead = (): void => {
if (finishFolderLstat && unreadFiles.size === 0) {
void finishFolderLstat();
finishFolderLstat = undefined;
}
};
interceptWatcherLstat((filePath: string, lstatAsync: () => Promise<void>) => {
if (filePath === folderPath) {
finishFolderLstat = lstatAsync;
finishWhenFilesAreRead();
} else {
void lstatAsync().then(() => {
unreadFiles.delete(filePath);
finishWhenFilesAreRead();
});
}
});
}
beforeEach(() => {
rootFolder = fs.mkdtempSync(path.join(fs.realpathSync.native(os.tmpdir()), 'heft-watch-fs-'));
writeCount = 0;
watchSpy = jest.spyOn(Watchpack.prototype, 'watch');
collectSpy = jest.spyOn(Watchpack.prototype, 'collectTimeInfoEntries');
closeSpy = jest.spyOn(Watchpack.prototype, 'close');
adapter = new WatchFileSystemAdapter();
});
afterEach(() => {
for (const watcher of watchSpy.mock.contexts) {
(watcher as Watchpack).close();
}
jest.restoreAllMocks();
fs.rmSync(rootFolder, { recursive: true, force: true });
});
it(
'requests a run when a watched file changes, and reports the file in that run',
async () => {
await startAsync();
const run2: IRun = await runAsync();
expect(run2.changed).toEqual([]);
await Async.sleepAsync(SETTLE_MS);
expect(run2.isRunRequested()).toBe(false);
writeFile('src/sub/b.ts');
expect(await run2.waitForRunRequestAsync()).toBe(true);
const run3: IRun = await runAsync();
expect(run3.changed).toEqual(['src/sub/b.ts']);
},
TEST_TIMEOUT_MS
);
it(
'requests another run for a file that changes while a run is in progress',
async () => {
await startAsync();
// The file changes after the glob has read it, and before the run starts watching
const run2: IRun = await runAsync({
beforeWatch: () => writeFile('src/sub/b.ts', Date.now())
});
expect(run2.changed).toEqual([]);
expect(await run2.waitForRunRequestAsync()).toBe(true);
const run3: IRun = await runAsync();
expect(run3.changed).toEqual(['src/sub/b.ts']);
},
TEST_TIMEOUT_MS
);
it(
'closes the previous watcher, after collecting its times, when the next run starts',
async () => {
writeFile('src/a.ts');
await runAsync();
expect(watchSpy).toHaveBeenCalledTimes(1);
const watcher1: Watchpack = watchSpy.mock.contexts[0];
expect(closeSpy).not.toHaveBeenCalled();
await runAsync();
expect(watchSpy).toHaveBeenCalledTimes(2);
const watcher2: Watchpack = watchSpy.mock.contexts[1];
expect(watcher2).not.toBe(watcher1);
expect(collectSpy).toHaveBeenCalledTimes(1);
expect(collectSpy.mock.contexts[0]).toBe(watcher1);
expect(closeSpy).toHaveBeenCalledTimes(1);
expect(closeSpy.mock.contexts[0]).toBe(watcher1);
expect(collectSpy.mock.invocationCallOrder[0]).toBeLessThan(closeSpy.mock.invocationCallOrder[0]);
await runAsync();
expect(closeSpy).toHaveBeenCalledTimes(2);
expect(closeSpy.mock.contexts[1]).toBe(watcher2);
},
TEST_TIMEOUT_MS
);
it(
'reports a file that changed after a run that did not start watching',
async () => {
await startAsync();
// Like a run in which copying files fails, so that TaskOperationRunner never calls watch()
const run2: IRun = await runAsync({ watch: false });
expect(run2.changed).toEqual([]);
writeFile('src/sub/b.ts');
await Async.sleepAsync(SETTLE_MS);
const run3: IRun = await runAsync();
expect(run3.changed).toEqual(['src/sub/b.ts']);
},
TEST_TIMEOUT_MS
);
// On Linux, a watch stays on the deleted folder. Windows doesn't let a watched folder be recreated.
(process.platform === 'linux' ? it : it.skip)(
'reports a change in a folder that was deleted and recreated, in the run after the change',
async () => {
await startAsync();
const run2: IRun = await runAsync();
expect(run2.changed).toEqual([]);
await Async.sleepAsync(SETTLE_MS);
// Delete and recreate the folder, as switching branches can
fs.rmSync(path.join(rootFolder, 'src/sub'), { recursive: true });
writeFile('src/sub/b.ts');
expect(await run2.waitForRunRequestAsync()).toBe(true);
await Async.sleepAsync(SETTLE_MS);
const run3: IRun = await runAsync();
expect(run3.changed).toEqual(['src/sub/b.ts']);
await Async.sleepAsync(SETTLE_MS);
expect(run3.isRunRequested()).toBe(false);
writeFile('src/sub/b.ts');
expect(await run3.waitForRunRequestAsync()).toBe(true);
const run4: IRun = await runAsync();
expect(run4.changed).toEqual(['src/sub/b.ts']);
await Async.sleepAsync(SETTLE_MS);
const run5: IRun = await runAsync();
expect(run5.changed).toEqual([]);
},
TEST_TIMEOUT_MS
);
it(
'does not request a run for a file that changed before the run, but does for a later change',
async () => {
const run2: IRun = await startWatchingAsync();
await changeBeforeNextRunAsync(run2, NOTES_PATH);
// The new watcher reports the file, because of its mtime. The glob doesn't match it, but the previous
// watcher's times show that it hasn't changed since the run started.
const run3: IRun = await runAsync();
expect(run3.changed).toEqual([]);
expect(await run3.waitForReportAsync(NOTES_PATH, 'scan (file)')).toBe(true);
expect(run3.isRunRequested()).toBe(false);
writeFile('src/sub/b.ts');
expect(await run3.waitForRunRequestAsync()).toBe(true);
const run4: IRun = await runAsync();
expect(run4.changed).toEqual(['src/sub/b.ts']);
},
TEST_TIMEOUT_MS
);
it(
'does not request another run for a watched file that changed before the run started',
async () => {
const run2: IRun = await startWatchingAsync();
await changeBeforeNextRunAsync(run2, 'src/sub/b.ts');
const run3: IRun = await runAsync();
expect(run3.changed).toEqual(['src/sub/b.ts']);
expect(await run3.waitForReportAsync('src/sub/b.ts', 'scan (file)')).toBe(true);
expect(run3.isRunRequested()).toBe(false);
},
TEST_TIMEOUT_MS
);
it(
'does not request another run for a file that the first run read',
async () => {
writeFile('src/b.ts', Date.now() + FUTURE_MTIME_OFFSET_MS);
await Async.sleepAsync(TIMESTAMP_GAP_MS);
// There is no previous watcher, so only the time that the run read is known
const run1: IRun = await runAsync();
expect(run1.changed).toEqual(['src/b.ts']);
expect(await run1.waitForReportAsync('src/b.ts', 'scan (file)')).toBe(true);
expect(run1.isRunRequested()).toBe(false);
},
TEST_TIMEOUT_MS
);
it(
'requests a run for a file that is written again during the run, with the same mtime',
async () => {
const run2: IRun = await startWatchingAsync();
const mtimeMs: number = await changeBeforeNextRunAsync(run2, NOTES_PATH);
const run3: IRun = await runAsync({
// The wait makes the file's ctime later than the run's start
beforeWatch: () => Async.sleepAsync(TIMESTAMP_GAP_MS).then(() => writeFile(NOTES_PATH, mtimeMs))
});
expect(run3.changed).toEqual([]);
expect(await run3.waitForRunRequestAsync()).toBe(true);
},
TEST_TIMEOUT_MS
);
it(
'requests a run for a watched file whose mtime moves back after the run read it',
async () => {
const run2: IRun = await startWatchingAsync();
const mtimeMs: number = await changeBeforeNextRunAsync(run2, 'src/sub/b.ts');
const run3: IRun = await runAsync({
beforeWatch: () => {
// Earlier than the mtime that the run read, but later than the watcher's start, so that the watcher
// reports the file
writeFile('src/sub/b.ts', mtimeMs - FUTURE_MTIME_OFFSET_MS / 2);
backdateCtime('src/sub/b.ts');
}
});
expect(run3.changed).toEqual(['src/sub/b.ts']);
expect(await run3.waitForReportAsync('src/sub/b.ts', 'scan (file)')).toBe(true);
expect(run3.isRunRequested()).toBe(true);
},
TEST_TIMEOUT_MS
);
it(
'does not request a run for a file whose change the previous watcher had not finished reading',
async () => {
await startWatchingAsync();
const lstatHeld: Promise<(() => void) | undefined> = holdNextWatcherLstatAsync(NOTES_PATH);
writeFile(NOTES_PATH, Date.now() + FUTURE_MTIME_OFFSET_MS);
const finishLstat: (() => void) | undefined = await lstatHeld;
expect(finishLstat).toBeDefined();
await Async.sleepAsync(TIMESTAMP_GAP_MS);
// The run starts while the previous watcher still has the file's old time
const run3: IRun = await runAsync();
finishLstat?.();
expect(run3.changed).toEqual([]);
expect(await run3.waitForReportAsync(NOTES_PATH, 'scan (file)')).toBe(true);
expect(run3.isRunRequested()).toBe(false);
},
TEST_TIMEOUT_MS
);
it(
'reports a watched file whose change the previous watcher had not finished reading, in that run',
async () => {
await startWatchingAsync();
const lstatHeld: Promise<(() => void) | undefined> = holdNextWatcherLstatAsync('src/sub/b.ts');
writeFile('src/sub/b.ts', Date.now() + FUTURE_MTIME_OFFSET_MS);
const finishLstat: (() => void) | undefined = await lstatHeld;
expect(finishLstat).toBeDefined();
await Async.sleepAsync(TIMESTAMP_GAP_MS);
const run3: IRun = await runAsync();
finishLstat?.();
expect(run3.changed).toEqual(['src/sub/b.ts']);
expect(await run3.waitForReportAsync('src/sub/b.ts', 'scan (file)')).toBe(true);
expect(run3.isRunRequested()).toBe(false);
},
TEST_TIMEOUT_MS
);
it(
'does not request a run when a folder that the run read is reported as its watcher is attached',
async () => {
const run2: IRun = await startWatchingAsync();
await changeBeforeNextRunAsync(run2, NOTES_PATH);
// The watcher of src attaches to the watcher of src/sub when its fs.lstat() of src/sub finishes. If
// src/sub's watcher has read a file with a new time by then, the watcher reports src/sub.
const run3: IRun = await runAsync({
beforeWatch: () => holdWatcherFolderLstat('src/sub', ['b.ts', 'notes.txt'])
});
expect(run3.changed).toEqual([]);
expect(await run3.waitForReportAsync('src/sub', 'watch (outdated on attach)')).toBe(true);
expect(run3.isRunRequested()).toBe(false);
},
TEST_TIMEOUT_MS
);
it(
'requests a run when a folder that the run did not read is reported as its watcher is attached',
async () => {
const run2: IRun = await startWatchingAsync();
await changeBeforeNextRunAsync(run2, NOTES_PATH);
// The run reads src and src/sub/b.ts, but not src/sub, so the watcher doesn't report the files in src/sub
// one by one. The watcher of src attaches to the watcher that src/sub has for b.ts, and reports src/sub.
const run3: IRun = await runAsync({
pattern: 'src/*.ts',
beforeWatch: () => {
adapter.getStateAndTrack(path.join(rootFolder, 'src/sub/b.ts'));
holdWatcherFolderLstat('src/sub', ['b.ts', 'notes.txt']);
}
});
expect(run3.changed).toEqual([]);
expect(await run3.waitForReportAsync('src/sub', 'watch (outdated on attach)')).toBe(true);
expect(run3.isRunRequested()).toBe(true);
},
TEST_TIMEOUT_MS
);
it(
'requests a run for a file system event, even if the file looks unchanged',
async () => {
const run2: IRun = await startWatchingAsync();
const mtimeMs: number = await changeBeforeNextRunAsync(run2, NOTES_PATH);
const run3: IRun = await runAsync();
expect(await run3.waitForReportAsync(NOTES_PATH, 'scan (file)')).toBe(true);
backdateCtime(NOTES_PATH);
writeFile(NOTES_PATH, mtimeMs);
expect(await run3.waitForRunRequestAsync()).toBe(true);
},
TEST_TIMEOUT_MS
);
it(
'requests a run for a watched file that is deleted during the run',
async () => {
await startWatchingAsync();
const run3: IRun = await runAsync({
beforeWatch: () => fs.unlinkSync(path.join(rootFolder, 'src/sub/b.ts'))
});
expect(run3.changed).toEqual([]);
expect(await run3.waitForRunRequestAsync()).toBe(true);
},
TEST_TIMEOUT_MS
);
});