Skip to content

Commit 189bbc2

Browse files
committed
Add test when rename event occurs when file has already reappeared
1 parent c7bbb97 commit 189bbc2

File tree

3 files changed

+260
-32
lines changed

3 files changed

+260
-32
lines changed

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -267,13 +267,17 @@ interface Array<T> { length: number; [n: number]: T; }`
267267
inode: number | undefined;
268268
}
269269

270-
export interface ReloadWatchInvokeOptions {
270+
export interface WatchInvokeOptions {
271271
/** Invokes the directory watcher for the parent instead of the file changed */
272272
invokeDirectoryWatcherInsteadOfFileChanged: boolean;
273273
/** When new file is created, do not invoke watches for it */
274274
ignoreWatchInvokedWithTriggerAsFileCreate: boolean;
275275
/** Invoke the file delete, followed by create instead of file changed */
276276
invokeFileDeleteCreateAsPartInsteadOfChange: boolean;
277+
/** Dont invoke delete watches */
278+
ignoreDelete: boolean;
279+
/** Skip inode check on file or folder create*/
280+
skipInodeCheckOnCreate: boolean;
277281
}
278282

279283
export enum Tsc_WatchFile {
@@ -324,7 +328,7 @@ interface Array<T> { length: number; [n: number]: T; }`
324328
public defaultWatchFileKind?: () => WatchFileKind | undefined;
325329
public storeFilesChangingSignatureDuringEmit = true;
326330
watchFile: HostWatchFile;
327-
private readonly inodeWatching: boolean | undefined;
331+
private inodeWatching: boolean | undefined;
328332
private readonly inodes?: ESMap<Path, number>;
329333
watchDirectory: HostWatchDirectory;
330334
constructor(
@@ -376,7 +380,7 @@ interface Array<T> { length: number; [n: number]: T; }`
376380
tscWatchDirectory,
377381
defaultWatchFileKind: () => this.defaultWatchFileKind?.(),
378382
inodeWatching: !!this.inodeWatching,
379-
sysLog: this.write.bind(this)
383+
sysLog: s => this.write(s + this.newLine),
380384
});
381385
this.watchFile = watchFile;
382386
this.watchDirectory = watchDirectory;
@@ -441,16 +445,16 @@ interface Array<T> { length: number; [n: number]: T; }`
441445
}
442446
}
443447

444-
modifyFile(filePath: string, content: string, options?: Partial<ReloadWatchInvokeOptions>) {
448+
modifyFile(filePath: string, content: string, options?: Partial<WatchInvokeOptions>) {
445449
const path = this.toFullPath(filePath);
446450
const currentEntry = this.fs.get(path);
447451
if (!currentEntry || !isFsFile(currentEntry)) {
448452
throw new Error(`file not present: ${filePath}`);
449453
}
450454

451455
if (options && options.invokeFileDeleteCreateAsPartInsteadOfChange) {
452-
this.removeFileOrFolder(currentEntry, returnFalse);
453-
this.ensureFileOrFolder({ path: filePath, content });
456+
this.removeFileOrFolder(currentEntry, /*isRenaming*/ false, options);
457+
this.ensureFileOrFolder({ path: filePath, content }, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ undefined, /*ignoreParentWatch*/ undefined, options);
454458
}
455459
else {
456460
currentEntry.content = content;
@@ -475,7 +479,7 @@ interface Array<T> { length: number; [n: number]: T; }`
475479
Debug.assert(!!file);
476480

477481
// Only remove the file
478-
this.removeFileOrFolder(file, returnFalse, /*isRenaming*/ true);
482+
this.removeFileOrFolder(file, /*isRenaming*/ true);
479483

480484
// Add updated folder with new folder name
481485
const newFullPath = getNormalizedAbsolutePath(newFileName, this.currentDirectory);
@@ -495,7 +499,7 @@ interface Array<T> { length: number; [n: number]: T; }`
495499
Debug.assert(!!folder);
496500

497501
// Only remove the folder
498-
this.removeFileOrFolder(folder, returnFalse, /*isRenaming*/ true);
502+
this.removeFileOrFolder(folder, /*isRenaming*/ true);
499503

500504
// Add updated folder with new folder name
501505
const newFullPath = getNormalizedAbsolutePath(newFolderName, this.currentDirectory);
@@ -530,38 +534,38 @@ interface Array<T> { length: number; [n: number]: T; }`
530534
}
531535
}
532536

533-
ensureFileOrFolder(fileOrDirectoryOrSymLink: FileOrFolderOrSymLink, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean) {
537+
ensureFileOrFolder(fileOrDirectoryOrSymLink: FileOrFolderOrSymLink, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean, options?: Partial<WatchInvokeOptions>) {
534538
if (isFile(fileOrDirectoryOrSymLink)) {
535539
const file = this.toFsFile(fileOrDirectoryOrSymLink);
536540
// file may already exist when updating existing type declaration file
537541
if (!this.fs.get(file.path)) {
538-
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath), ignoreParentWatch);
539-
this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate);
542+
const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath), ignoreParentWatch, options);
543+
this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate, options);
540544
}
541545
}
542546
else if (isSymLink(fileOrDirectoryOrSymLink)) {
543547
const symLink = this.toFsSymLink(fileOrDirectoryOrSymLink);
544548
Debug.assert(!this.fs.get(symLink.path));
545-
const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath), ignoreParentWatch);
546-
this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate);
549+
const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath), ignoreParentWatch, options);
550+
this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate, options);
547551
}
548552
else {
549553
const fullPath = getNormalizedAbsolutePath(fileOrDirectoryOrSymLink.path, this.currentDirectory);
550-
this.ensureFolder(getDirectoryPath(fullPath), ignoreParentWatch);
551-
this.ensureFolder(fullPath, ignoreWatchInvokedWithTriggerAsFileCreate);
554+
this.ensureFolder(getDirectoryPath(fullPath), ignoreParentWatch, options);
555+
this.ensureFolder(fullPath, ignoreWatchInvokedWithTriggerAsFileCreate, options);
552556
}
553557
}
554558

555-
private ensureFolder(fullPath: string, ignoreWatch: boolean | undefined): FsFolder {
559+
private ensureFolder(fullPath: string, ignoreWatch: boolean | undefined, options: Partial<WatchInvokeOptions> | undefined): FsFolder {
556560
const path = this.toPath(fullPath);
557561
let folder = this.fs.get(path) as FsFolder;
558562
if (!folder) {
559563
folder = this.toFsFolder(fullPath);
560564
const baseFullPath = getDirectoryPath(fullPath);
561565
if (fullPath !== baseFullPath) {
562566
// Add folder in the base folder
563-
const baseFolder = this.ensureFolder(baseFullPath, ignoreWatch);
564-
this.addFileOrFolderInFolder(baseFolder, folder, ignoreWatch);
567+
const baseFolder = this.ensureFolder(baseFullPath, ignoreWatch, options);
568+
this.addFileOrFolderInFolder(baseFolder, folder, ignoreWatch, options);
565569
}
566570
else {
567571
// root folder
@@ -574,7 +578,7 @@ interface Array<T> { length: number; [n: number]: T; }`
574578
return folder;
575579
}
576580

577-
private addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean) {
581+
private addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean, options?: Partial<WatchInvokeOptions>) {
578582
if (!this.fs.has(fileOrDirectory.path)) {
579583
insertSorted(folder.entries, fileOrDirectory, (a, b) => compareStringsCaseSensitive(getBaseFileName(a.path), getBaseFileName(b.path)));
580584
}
@@ -585,11 +589,14 @@ interface Array<T> { length: number; [n: number]: T; }`
585589
if (ignoreWatch) {
586590
return;
587591
}
592+
const inodeWatching = this.inodeWatching;
593+
if (options?.skipInodeCheckOnCreate) this.inodeWatching = false;
588594
this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created);
589595
this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed);
596+
this.inodeWatching = inodeWatching;
590597
}
591598

592-
private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRemovableLeafFolder: (folder: FsFolder) => boolean, isRenaming = false) {
599+
private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRenaming?: boolean, options?: Partial<WatchInvokeOptions>) {
593600
const basePath = getDirectoryPath(fileOrDirectory.path);
594601
const baseFolder = this.fs.get(basePath) as FsFolder;
595602
if (basePath !== fileOrDirectory.path) {
@@ -602,21 +609,16 @@ interface Array<T> { length: number; [n: number]: T; }`
602609
if (isFsFolder(fileOrDirectory)) {
603610
Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming);
604611
}
605-
this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted);
612+
if (!options?.ignoreDelete) this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted);
606613
this.inodes?.delete(fileOrDirectory.path);
607-
this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed);
608-
if (basePath !== fileOrDirectory.path &&
609-
baseFolder.entries.length === 0 &&
610-
isRemovableLeafFolder(baseFolder)) {
611-
this.removeFileOrFolder(baseFolder, isRemovableLeafFolder);
612-
}
614+
if (!options?.ignoreDelete) this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed);
613615
}
614616

615617
deleteFile(filePath: string) {
616618
const path = this.toFullPath(filePath);
617619
const currentEntry = this.fs.get(path) as FsFile;
618620
Debug.assert(isFsFile(currentEntry));
619-
this.removeFileOrFolder(currentEntry, returnFalse);
621+
this.removeFileOrFolder(currentEntry);
620622
}
621623

622624
deleteFolder(folderPath: string, recursive?: boolean) {
@@ -630,11 +632,11 @@ interface Array<T> { length: number; [n: number]: T; }`
630632
this.deleteFolder(fsEntry.fullPath, recursive);
631633
}
632634
else {
633-
this.removeFileOrFolder(fsEntry, returnFalse);
635+
this.removeFileOrFolder(fsEntry);
634636
}
635637
});
636638
}
637-
this.removeFileOrFolder(currentEntry, returnFalse);
639+
this.removeFileOrFolder(currentEntry);
638640
}
639641

640642
private watchFileWorker(fileName: string, cb: FileWatcherCallback, pollingInterval: PollingInterval) {
@@ -947,11 +949,11 @@ interface Array<T> { length: number; [n: number]: T; }`
947949
}
948950
}
949951

950-
prependFile(path: string, content: string, options?: Partial<ReloadWatchInvokeOptions>): void {
952+
prependFile(path: string, content: string, options?: Partial<WatchInvokeOptions>): void {
951953
this.modifyFile(path, content + this.readFile(path), options);
952954
}
953955

954-
appendFile(path: string, content: string, options?: Partial<ReloadWatchInvokeOptions>): void {
956+
appendFile(path: string, content: string, options?: Partial<WatchInvokeOptions>): void {
955957
this.modifyFile(path, this.readFile(path) + content, options);
956958
}
957959

src/testRunner/unittests/tscWatch/watchEnvironment.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,45 @@ namespace ts.tscWatch {
612612
},
613613
]
614614
});
615+
616+
verifyTscWatch({
617+
scenario,
618+
subScenario: `fsWatch/when using file watching thats on inode when rename occurs when file is still on the disk`,
619+
commandLineArgs: ["-w", "--extendedDiagnostics"],
620+
sys: () => createWatchedSystem(
621+
{
622+
[libFile.path]: libFile.content,
623+
[`${projectRoot}/main.ts`]: `import { foo } from "./foo"; foo();`,
624+
[`${projectRoot}/foo.ts`]: `export declare function foo(): string;`,
625+
[`${projectRoot}/tsconfig.json`]: JSON.stringify({
626+
watchOptions: { watchFile: "useFsEvents" },
627+
files: ["foo.ts", "main.ts"]
628+
}),
629+
},
630+
{
631+
currentDirectory: projectRoot,
632+
inodeWatching: true,
633+
}
634+
),
635+
changes: [
636+
{
637+
caption: "Introduce error such that when callback happens file is already appeared",
638+
// vm's wq generates this kind of event
639+
// Skip delete event so inode changes but when the create's rename occurs file is on disk
640+
change: sys => sys.modifyFile(`${projectRoot}/foo.ts`, `export declare function foo2(): string;`, {
641+
invokeFileDeleteCreateAsPartInsteadOfChange: true,
642+
ignoreDelete: true,
643+
skipInodeCheckOnCreate: true
644+
}),
645+
timeouts: sys => sys.checkTimeoutQueueLengthAndRun(1),
646+
},
647+
{
648+
caption: "Replace file with rename event that fixes error",
649+
change: sys => sys.modifyFile(`${projectRoot}/foo.ts`, `export declare function foo(): string;`, { invokeFileDeleteCreateAsPartInsteadOfChange: true, }),
650+
timeouts: sys => sys.checkTimeoutQueueLengthAndRun(0),
651+
},
652+
]
653+
});
615654
});
616655
});
617656
}

0 commit comments

Comments
 (0)