diff --git a/src/testRunner/unittests/sys/symlinkWatching.ts b/src/testRunner/unittests/sys/symlinkWatching.ts index c0bd12e3d6026..33b8b51bef17e 100644 --- a/src/testRunner/unittests/sys/symlinkWatching.ts +++ b/src/testRunner/unittests/sys/symlinkWatching.ts @@ -69,66 +69,175 @@ describe("unittests:: sys:: symlinkWatching::", () => { }); } + interface EventAndFileName { + event: string; + fileName: string | null | undefined; + } + type FsWatch = (dir: string, recursive: boolean, cb: ts.FsWatchCallback, sys: System) => ts.FileWatcher; + interface WatchDirectoryResult { + dir: string; + watcher: ts.FileWatcher; + actual: EventAndFileName[]; + } + function watchDirectory( + sys: System, + fsWatch: FsWatch, + dir: string, + recursive: boolean, + ) { + const result: WatchDirectoryResult = { + dir, + watcher: fsWatch( + dir, + recursive, + (event, fileName) => result.actual.push({ event, fileName }), + sys, + ), + actual: [], + }; + return result; + } + + function initializeWatchDirectoryResult(result: WatchDirectoryResult) { + result.actual.length = 0; + } + + async function verfiyWatchDirectoryResult( + dirResult: WatchDirectoryResult, + expectedDirResult: readonly EventAndFileName[], + linkResult: WatchDirectoryResult, + expectedLinkResult: readonly EventAndFileName[], + subDirResult?: WatchDirectoryResult, + expectedSubDirResult?: readonly EventAndFileName[], + linkedSubDirResult?: WatchDirectoryResult, + expectedLinkedSubDirResult?: readonly EventAndFileName[], + skipAsserts?: boolean, + ) { + const deferred = defer(); + ts.sys.setTimeout!(() => { + console.log("dir", dirResult.actual); + console.log("link", linkResult.actual); + console.log("subDir", subDirResult?.actual); + console.log("linkedSubDir", linkedSubDirResult?.actual); + deferred.resolve(); + }, 2000); + await deferred.promise; + if (!skipAsserts) { + assert.deepEqual(dirResult.actual, expectedDirResult); + assert.deepEqual(linkResult.actual, expectedLinkResult); + assert.deepEqual(subDirResult?.actual, expectedSubDirResult); + assert.deepEqual(linkedSubDirResult?.actual, expectedLinkedSubDirResult); + } + } + function verifyWatchDirectoryUsingFsEvents( sys: System, + fsWatch: FsWatch, dir: string, link: string, - fsWatch: (dir: string, cb: ts.FsWatchCallback, sys: System) => ts.FileWatcher, isMacOs: boolean, isWindows: boolean, ) { it(`watchDirectory using fsEvents`, async () => { - interface Expected { - indexForDefer: number; - deferred: readonly Deferred[]; - expectedFileName: string; - expectedEvent: string[]; - } - type ExpectedForOperation = Pick; + console.log("watchDirectory using fsEvents"); interface TableOfEvents { - fileCreate: ExpectedForOperation; - linkFileCreate: ExpectedForOperation; - fileChange: ExpectedForOperation; - fileModifiedTimeChange: ExpectedForOperation; - linkModifiedTimeChange: ExpectedForOperation; - linkFileChange: ExpectedForOperation; - fileDelete: ExpectedForOperation; - linkFileDelete: ExpectedForOperation; + fileCreate: readonly EventAndFileName[]; + linkFileCreate: readonly EventAndFileName[]; + fileChange: readonly EventAndFileName[]; + fileModifiedTimeChange: readonly EventAndFileName[]; + linkModifiedTimeChange: readonly EventAndFileName[]; + linkFileChange: readonly EventAndFileName[]; + fileDelete: readonly EventAndFileName[]; + linkFileDelete: readonly EventAndFileName[]; } const tableOfEvents: TableOfEvents = isMacOs ? { - fileCreate: { expectedEvent: ["rename"], expectedFileName: "file1.ts" }, - linkFileCreate: { expectedEvent: ["rename"], expectedFileName: "file2.ts" }, - fileChange: { expectedEvent: ["rename"], expectedFileName: "file1.ts" }, - linkFileChange: { expectedEvent: ["rename"], expectedFileName: "file2.ts" }, - fileModifiedTimeChange: { expectedEvent: ["rename"], expectedFileName: "file1.ts" }, - linkModifiedTimeChange: { expectedEvent: ["rename"], expectedFileName: "file2.ts" }, - fileDelete: { expectedEvent: ["rename"], expectedFileName: "file1.ts" }, - linkFileDelete: { expectedEvent: ["rename"], expectedFileName: "file2.ts" }, + fileCreate: [ + { event: "rename", fileName: "file1.ts" }, + ], + linkFileCreate: [ + { event: "rename", fileName: "file2.ts" }, + ], + fileChange: [ + { event: "rename", fileName: "file1.ts" }, + ], + linkFileChange: [ + { event: "rename", fileName: "file2.ts" }, + ], + fileModifiedTimeChange: [ + { event: "rename", fileName: "file1.ts" }, + ], + linkModifiedTimeChange: [ + { event: "rename", fileName: "file2.ts" }, + ], + fileDelete: [ + { event: "rename", fileName: "file1.ts" }, + ], + linkFileDelete: [ + { event: "rename", fileName: "file2.ts" }, + ], } : isWindows ? { - fileCreate: { expectedEvent: ["rename", "change"], expectedFileName: "file1.ts" }, - linkFileCreate: { expectedEvent: ["rename", "change"], expectedFileName: "file2.ts" }, - fileChange: { expectedEvent: ["change", "change"], expectedFileName: "file1.ts" }, - linkFileChange: { expectedEvent: ["change", "change"], expectedFileName: "file2.ts" }, - fileModifiedTimeChange: { expectedEvent: ["change"], expectedFileName: "file1.ts" }, - linkModifiedTimeChange: { expectedEvent: ["change"], expectedFileName: "file2.ts" }, - fileDelete: { expectedEvent: ["rename"], expectedFileName: "file1.ts" }, - linkFileDelete: { expectedEvent: ["rename"], expectedFileName: "file2.ts" }, + fileCreate: [ + { event: "rename", fileName: "file1.ts" }, + { event: "change", fileName: "file1.ts" }, + ], + linkFileCreate: [ + { event: "rename", fileName: "file2.ts" }, + { event: "change", fileName: "file2.ts" }, + ], + fileChange: [ + { event: "change", fileName: "file1.ts" }, + { event: "change", fileName: "file1.ts" }, + ], + linkFileChange: [ + { event: "change", fileName: "file2.ts" }, + { event: "change", fileName: "file2.ts" }, + ], + fileModifiedTimeChange: [ + { event: "change", fileName: "file1.ts" }, + ], + linkModifiedTimeChange: [ + { event: "change", fileName: "file2.ts" }, + ], + fileDelete: [ + { event: "rename", fileName: "file1.ts" }, + ], + linkFileDelete: [ + { event: "rename", fileName: "file2.ts" }, + ], } : { - fileCreate: { expectedEvent: ["rename", "change"], expectedFileName: "file1.ts" }, - linkFileCreate: { expectedEvent: ["rename", "change"], expectedFileName: "file2.ts" }, - fileChange: { expectedEvent: ["change"], expectedFileName: "file1.ts" }, - linkFileChange: { expectedEvent: ["change"], expectedFileName: "file2.ts" }, - fileModifiedTimeChange: { expectedEvent: ["change"], expectedFileName: "file1.ts" }, - linkModifiedTimeChange: { expectedEvent: ["change"], expectedFileName: "file2.ts" }, - fileDelete: { expectedEvent: ["rename"], expectedFileName: "file1.ts" }, - linkFileDelete: { expectedEvent: ["rename"], expectedFileName: "file2.ts" }, + fileCreate: [ + { event: "rename", fileName: "file1.ts" }, + { event: "change", fileName: "file1.ts" }, + ], + linkFileCreate: [ + { event: "rename", fileName: "file2.ts" }, + { event: "change", fileName: "file2.ts" }, + ], + fileChange: [ + { event: "change", fileName: "file1.ts" }, + ], + linkFileChange: [ + { event: "change", fileName: "file2.ts" }, + ], + fileModifiedTimeChange: [ + { event: "change", fileName: "file1.ts" }, + ], + linkModifiedTimeChange: [ + { event: "change", fileName: "file2.ts" }, + ], + fileDelete: [ + { event: "rename", fileName: "file1.ts" }, + ], + linkFileDelete: [ + { event: "rename", fileName: "file2.ts" }, + ], }; - const dirResult = watchDirectory(dir); - const linkResult = watchDirectory(link); + const dirResult = nonRecursiveWatchDirectory(dir); + const linkResult = nonRecursiveWatchDirectory(link); await operation("fileCreate"); await operation("linkFileCreate"); @@ -145,70 +254,213 @@ describe("unittests:: sys:: symlinkWatching::", () => { dirResult.watcher.close(); linkResult.watcher.close(); - function watchDirectory(dir: string) { - const result = { - dir, - watcher: fsWatch( - dir, - (event, fileName) => { - console.log(dir, result.expected.indexForDefer, event, fileName); // To ensure we can get the data on all OS - assert.equal(event, result.expected.expectedEvent[result.expected.indexForDefer]); - assert.equal(fileName, result.expected.expectedFileName); - result.expected.deferred[result.expected.indexForDefer++].resolve(); - }, - sys, - ), - expected: undefined! as Expected, - }; - return result; + function nonRecursiveWatchDirectory(dir: string) { + return watchDirectory(sys, fsWatch, dir, /*recursive*/ false); } async function operation(opType: keyof TableOfEvents) { - const expected = tableOfEvents[opType]; console.log(""); console.log(opType); - dirResult.expected = { - indexForDefer: 0, - deferred: ts.arrayOf(expected.expectedEvent.length, defer), - ...expected, - }; - linkResult.expected = { - indexForDefer: 0, - deferred: ts.arrayOf(expected.expectedEvent.length, defer), - ...expected, - }; - let op; + initializeWatchDirectoryResult(dirResult); + initializeWatchDirectoryResult(linkResult); switch (opType) { case "fileCreate": - op = () => sys.writeFile(`${dir}/file1.ts`, "export const x = 100;"); + sys.writeFile(`${dir}/file1.ts`, "export const x = 100;"); break; case "linkFileCreate": - op = () => sys.writeFile(`${link}/file2.ts`, "export const x = 100;"); + sys.writeFile(`${link}/file2.ts`, "export const x = 100;"); break; case "fileChange": - op = () => sys.writeFile(`${dir}/file1.ts`, "export const x2 = 100;"); + sys.writeFile(`${dir}/file1.ts`, "export const x2 = 100;"); break; case "linkFileChange": - op = () => sys.writeFile(`${link}/file2.ts`, "export const x2 = 100;"); + sys.writeFile(`${link}/file2.ts`, "export const x2 = 100;"); break; case "fileModifiedTimeChange": - op = () => sys.setModifiedTime!(`${dir}/file1.ts`, new Date()); + sys.setModifiedTime!(`${dir}/file1.ts`, new Date()); break; case "linkModifiedTimeChange": - op = () => sys.setModifiedTime!(`${link}/file2.ts`, new Date()); + sys.setModifiedTime!(`${link}/file2.ts`, new Date()); break; case "fileDelete": - op = () => sys.deleteFile!(`${dir}/file1.ts`); + sys.deleteFile!(`${dir}/file1.ts`); break; case "linkFileDelete": - op = () => sys.deleteFile!(`${link}/file2.ts`); + sys.deleteFile!(`${link}/file2.ts`); + break; + default: + ts.Debug.assertNever(opType); + } + + await verfiyWatchDirectoryResult(dirResult, tableOfEvents[opType], linkResult, tableOfEvents[opType]); + } + }); + } + + function verifyRecursiveWatchDirectoryUsingFsEvents( + sys: System, + fsWatch: FsWatch, + dir: string, + link: string, + isMacOs: boolean, + ) { + it(`recursive watchDirectory using fsEvents`, async () => { + console.log("recrusive watchDirectory using fsEvents"); + + // Need a table of expected events for each OS and operation instead + interface TableOfEvents { + init?: readonly EventAndFileName[]; + fileCreate: readonly EventAndFileName[]; + // linkFileCreate: readonly ExpectedForOperation[]; + // fileChange: readonly ExpectedForOperation[]; + // fileModifiedTimeChange: readonly ExpectedForOperation[]; + // linkModifiedTimeChange: readonly ExpectedForOperation[]; + // linkFileChange: readonly ExpectedForOperation[]; + // fileDelete: readonly ExpectedForOperation[]; + // linkFileDelete: readonly ExpectedForOperation[]; + } + const eventsForDir: TableOfEvents = isMacOs ? + { + fileCreate: [ + { event: "rename", fileName: "sub" }, + { event: "rename", fileName: "sub/folder" }, + { event: "rename", fileName: "sub/folder/file.ts" }, + { event: "rename", fileName: "linkedsub" }, + ], + } : + { + fileCreate: [ + { event: "rename", fileName: "sub/folder/file1.ts" }, + { event: "change", fileName: "sub/folder/file1.ts" }, + { event: "change", fileName: "sub/folder" }, + { event: "change", fileName: "sub" }, + ], + }; + + const eventsForLink: TableOfEvents = isMacOs ? + { + fileCreate: [ + { event: "rename", fileName: "sub" }, + { event: "rename", fileName: "sub/folder" }, + { event: "rename", fileName: "sub/folder/file.ts" }, + { event: "rename", fileName: "linkedsub" }, + ], + } : + { + fileCreate: [ + { event: "rename", fileName: "sub/folder/file1.ts" }, + { event: "change", fileName: "sub/folder/file1.ts" }, + { event: "change", fileName: "sub" }, + ], + }; + + const eventsForSubDir: TableOfEvents = isMacOs ? + { + fileCreate: [ + { event: "rename", fileName: "folder/file2.ts" }, + { event: "rename", fileName: "folder/file1.ts" }, + ], + } : + { + fileCreate: [ + { event: "rename", fileName: "folder/file1.ts" }, + { event: "change", fileName: "folder/file1.ts" }, + { event: "change", fileName: "folder" }, + ], + }; + + const eventsForLinkSubDir: TableOfEvents = isMacOs ? + { + fileCreate: [ + { event: "rename", fileName: "folder" }, + { event: "rename", fileName: "folder/file1.ts" }, + ], + } : + { + fileCreate: [ + { event: "rename", fileName: "folder/file1.ts" }, + { event: "change", fileName: "folder/file1.ts" }, + { event: "change", fileName: "folder" }, + ], + }; + + const dirResult = recursiveWatchDirectory(dir); + const linkResult = recursiveWatchDirectory(link); + const subDirResult = recursiveWatchDirectory(`${dir}/sub`); + const linkedSubDirResult = recursiveWatchDirectory(`${dir}/linkedsub`); + + await operation("init"); + await operation("fileCreate"); + // await operation("linkFileCreate"); + + // await operation("fileChange"); + // await operation("linkFileChange"); + + // await operation("fileModifiedTimeChange"); + // await operation("linkModifiedTimeChange"); + + // await operation("fileDelete"); + // await operation("linkFileDelete"); + + dirResult.watcher.close(); + linkResult.watcher.close(); + subDirResult.watcher.close(); + linkedSubDirResult.watcher.close(); + + function recursiveWatchDirectory(dir: string) { + return watchDirectory(sys, fsWatch, dir, /*recursive*/ true); + } + + async function operation(opType: keyof TableOfEvents) { + console.log(""); + console.log(opType); + initializeWatchDirectoryResult(dirResult); + initializeWatchDirectoryResult(linkResult); + initializeWatchDirectoryResult(subDirResult); + initializeWatchDirectoryResult(linkedSubDirResult); + switch (opType) { + case "init": + sys.writeFile(`${dir}/sub/folder/init.ts`, "export const x = 100;"); + break; + case "fileCreate": + sys.writeFile(`${dir}/sub/folder/file1.ts`, "export const x = 100;"); break; + // case "linkFileCreate": + // sys.writeFile(`${link}/file2.ts`, "export const x = 100;"); + // break; + // case "fileChange": + // sys.writeFile(`${dir}/file1.ts`, "export const x2 = 100;"); + // break; + // case "linkFileChange": + // sys.writeFile(`${link}/file2.ts`, "export const x2 = 100;"); + // break; + // case "fileModifiedTimeChange": + // sys.setModifiedTime!(`${dir}/file1.ts`, new Date()); + // break; + // case "linkModifiedTimeChange": + // sys.setModifiedTime!(`${link}/file2.ts`, new Date()); + // break; + // case "fileDelete": + // sys.deleteFile!(`${dir}/file1.ts`); + // break; + // case "linkFileDelete": + // sys.deleteFile!(`${link}/file2.ts`); + // break; default: ts.Debug.assertNever(opType); } - delayedOp(op); - await Promise.all(dirResult.expected.deferred.map(d => d.promise)); - await Promise.all(linkResult.expected.deferred.map(d => d.promise)); + await verfiyWatchDirectoryResult( + dirResult, + eventsForDir[opType]!, + linkResult, + eventsForLink[opType]!, + subDirResult, + eventsForSubDir[opType], + linkedSubDirResult, + eventsForLinkSubDir[opType], + // opType === "init", + /*skipAsserts*/ true, + ); } }); } @@ -231,6 +483,9 @@ describe("unittests:: sys:: symlinkWatching::", () => { withSwallowException(() => fs.symlinkSync(`${root}/dirpolling`, `${root}/linkeddirpolling`, "junction")); ts.sys.writeFile(`${root}/dirfsevents/file.ts`, "export const x = 10;"); withSwallowException(() => fs.symlinkSync(`${root}/dirfsevents`, `${root}/linkeddirfsevents`, "junction")); + ts.sys.writeFile(`${root}/recursivefsevents/sub/folder/file.ts`, "export const x = 10;"); + withSwallowException(() => fs.symlinkSync(`${root}/recursivefsevents`, `${root}/recursivelinkedfsevents`, "junction")); + withSwallowException(() => fs.symlinkSync(`${root}/recursivefsevents/sub`, `${root}/recursivefsevents/linkedsub`, "junction")); }); after(() => { cleanup(); @@ -247,38 +502,48 @@ describe("unittests:: sys:: symlinkWatching::", () => { catch { /* swallow */ } } - verifyWatchFile( - "watchFile using polling", - ts.sys, - `${root}/polling/file.ts`, - `${root}/linkedpolling/file.ts`, - { watchFile: ts.WatchFileKind.PriorityPollingInterval }, - ); - verifyWatchFile( - "watchFile using fsEvents", - ts.sys, - `${root}/fsevents/file.ts`, - `${root}/linkedfsevents/file.ts`, - { watchFile: ts.WatchFileKind.UseFsEvents }, - ); + // verifyWatchFile( + // "watchFile using polling", + // ts.sys, + // `${root}/polling/file.ts`, + // `${root}/linkedpolling/file.ts`, + // { watchFile: ts.WatchFileKind.PriorityPollingInterval }, + // ); + // verifyWatchFile( + // "watchFile using fsEvents", + // ts.sys, + // `${root}/fsevents/file.ts`, + // `${root}/linkedfsevents/file.ts`, + // { watchFile: ts.WatchFileKind.UseFsEvents }, + // ); - verifyWatchFile( - "watchDirectory using polling", - ts.sys, - `${root}/dirpolling`, - `${root}/linkeddirpolling`, - { watchFile: ts.WatchFileKind.PriorityPollingInterval }, - getFileName(), - ); + // verifyWatchFile( + // "watchDirectory using polling", + // ts.sys, + // `${root}/dirpolling`, + // `${root}/linkeddirpolling`, + // { watchFile: ts.WatchFileKind.PriorityPollingInterval }, + // getFileName(), + // ); verifyWatchDirectoryUsingFsEvents( ts.sys, + (dir, _recursive, cb) => fs.watch(dir, { persistent: true }, cb), `${root}/dirfsevents`, `${root}/linkeddirfsevents`, - (dir, cb) => fs.watch(dir, { persistent: true }, cb), isMacOs, isWindows, ); + + if (isMacOs || isWindows) { + verifyRecursiveWatchDirectoryUsingFsEvents( + ts.sys, + (dir, recursive, cb) => fs.watch(dir, { persistent: true, recursive }, cb), + `${root}/recursivefsevents`, + `${root}/recursivelinkedfsevents`, + isMacOs, + ); + } }); describe("with virtualFileSystem::", () => { @@ -316,29 +581,45 @@ describe("unittests:: sys:: symlinkWatching::", () => { // TODO (sheetal) add test for each os behaviour // verifyWatchDirectoryUsingFsEvents( // getSys(), + // (dir, recursive, cb, sys) => sys.fsWatchWorker(dir, recursive, cb), // `${root}/folder`, // `${root}/linked`, - // (dir, cb, sys) => sys.fsWatchWorker(dir, /*recursive*/ false, cb), // /*isMacOs*/ false, // /*isWindows*/ true, // ); // verifyWatchDirectoryUsingFsEvents( // getSys(), + // (dir, recursive, cb, sys) => sys.fsWatchWorker(dir, recursive, cb), // `${root}/folder`, // `${root}/linked`, - // (dir, cb, sys) => sys.fsWatchWorker(dir, /*recursive*/ false, cb), // /*isMacOs*/ true, // /*isWindows*/ false, // ); // verifyWatchDirectoryUsingFsEvents( // getSys(), + // (dir, recursive, cb, sys) => sys.fsWatchWorker(dir, recursive, cb), // `${root}/folder`, // `${root}/linked`, - // (dir, cb, sys) => sys.fsWatchWorker(dir, /*recursive*/ false, cb), // /*isMacOs*/ false, // /*isWindows*/ false, // ); + + // verifyRecursiveWatchDirectoryUsingFsEvents( + // getSys(), + // (dir, recursive, cb, sys) => sys.fsWatchWorker(dir, recursive, cb), + // `${root}/recursivefsevents`, + // `${root}/recursivelinkedfsevents`, + // /*isMacOs*/ false, + // ); + + // verifyRecursiveWatchDirectoryUsingFsEvents( + // getSys(), + // (dir, recursive, cb, sys) => sys.fsWatchWorker(dir, recursive, cb), + // `${root}/recursivefsevents`, + // `${root}/recursivelinkedfsevents`, + // /*isMacOs*/ true, + // ); }); });