diff --git a/src/testRunner/unittests/sys/symlinkWatching.ts b/src/testRunner/unittests/sys/symlinkWatching.ts index b3aeadf175850..83499f0497b3f 100644 --- a/src/testRunner/unittests/sys/symlinkWatching.ts +++ b/src/testRunner/unittests/sys/symlinkWatching.ts @@ -6,13 +6,14 @@ import { import * as ts from "../../_namespaces/ts"; import { defer, + Deferred, } from "../../_namespaces/Utils"; import { createWatchedSystem, } from "../helpers/virtualFileSystemWithWatch"; describe("unittests:: sys:: symlinkWatching::", () => { - function delayedOp(op: () => void) { - ts.sys.setTimeout!(op, 100); + function delayedOp(op: () => void, delay: number) { + ts.sys.setTimeout!(op, delay); } function modifiedTimeToString(d: Date | undefined) { @@ -31,24 +32,15 @@ describe("unittests:: sys:: symlinkWatching::", () => { it(scenario, async () => { const fileResult = watchFile(file); const linkResult = watchFile(link); - delayedOp(() => sys.writeFile(getFileName?.(file) ?? file, "export const x = 100;")); - // Should invoke on file as well as link - await fileResult.deferred[0].promise; - await linkResult.deferred[0].promise; - - delayedOp(() => sys.writeFile(getFileName?.(link) ?? link, "export const x = 100;")); - // Should invoke on file as well as link - await fileResult.deferred[1].promise; - await linkResult.deferred[1].promise; + await writeFile(file); + await writeFile(link); fileResult.watcher.close(); linkResult.watcher.close(); function watchFile(toWatch: string) { - const deferred = [defer(), defer()]; - let indexForDefer = 0; - return { + const result = { watcher: sys.watchFile!( toWatch, (fileName, eventKind, modifiedTime) => { @@ -56,69 +48,281 @@ describe("unittests:: sys:: symlinkWatching::", () => { assert.equal(eventKind, ts.FileWatcherEventKind.Changed); const actual = modifiedTimeToString(modifiedTime); assert(actual === undefined || actual === modifiedTimeToString(sys.getModifiedTime!(file))); - deferred[indexForDefer++].resolve(); + result.deferred.resolve(); }, 10, watchOptions, ), - deferred, + deferred: undefined! as Deferred, }; + return result; + } + + async function writeFile(onFile: string) { + fileResult.deferred = defer(); + linkResult.deferred = defer(); + delayedOp(() => sys.writeFile(getFileName?.(onFile) ?? onFile, "export const x = 100;"), 100); + // Should invoke on file as well as link + await fileResult.deferred.promise; + await linkResult.deferred.promise; } }); } + interface EventAndFileName { + event: string; + fileName: string | null | undefined; + } + interface ExpectedEventAndFileName { + event: string | readonly string[]; // Its expected event name or any of the event names + 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: fileName ? ts.normalizeSlashes(fileName) : fileName }), + sys, + ), + actual: [], + }; + return result; + } + + function initializeWatchDirectoryResult(...results: WatchDirectoryResult[]) { + results.forEach(result => result.actual.length = 0); + } + + function verfiyWatchDirectoryResult( + opType: string, + dirResult: WatchDirectoryResult, + expectedDirResult: readonly ExpectedEventAndFileName[], + linkResult: WatchDirectoryResult, + expectedLinkResult: readonly ExpectedEventAndFileName[], + skipAsserts?: boolean, + ) { + const deferred = defer(); + delayedOp(() => { + console.log("dir", dirResult.actual, expectedDirResult); + console.log("link", linkResult.actual, expectedLinkResult); + if (!skipAsserts) { + verifyEventAndFileNames(`${opType}:: dir`, dirResult.actual, expectedDirResult); + verifyEventAndFileNames(`${opType}:: link`, linkResult.actual, expectedLinkResult); + } + deferred.resolve(); + }, 4000); + return deferred.promise; + } + + function verifyEventAndFileNames( + prefix: string, + actual: readonly EventAndFileName[], + expected: readonly ExpectedEventAndFileName[], + ) { + assert(actual.length >= expected.length, `${prefix}:: Expected ${JSON.stringify(expected)} events, got ${JSON.stringify(actual)}`); + let expectedIndex = 0; + for (const a of actual) { + if (isExpectedEventAndFileName(a, expected[expectedIndex])) { + expectedIndex++; + return; + } + // Previous event repeated? + if (isExpectedEventAndFileName(a, expected[expectedIndex - 1])) return; + ts.Debug.fail(`${prefix}:: Expected ${JSON.stringify(expected)} events, got ${JSON.stringify(actual)}`); + } + } + + function isExpectedEventAndFileName(actual: EventAndFileName, expected: ExpectedEventAndFileName | undefined) { + return !!expected && + actual.fileName === expected.fileName && + (ts.isString(expected.event) ? actual.event === expected.event : ts.contains(expected.event, actual.event)); + } + + interface FsEventsForWatchDirectory { + fileCreate: readonly ExpectedEventAndFileName[]; + linkFileCreate: readonly ExpectedEventAndFileName[]; + fileChange: readonly ExpectedEventAndFileName[]; + fileModifiedTimeChange: readonly ExpectedEventAndFileName[]; + linkModifiedTimeChange: readonly ExpectedEventAndFileName[]; + linkFileChange: readonly ExpectedEventAndFileName[]; + fileDelete: readonly ExpectedEventAndFileName[]; + linkFileDelete: readonly ExpectedEventAndFileName[]; + } 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 () => { - const expectedEvent = ["rename", "change", "rename", "change"]; - const expectedFileName = ["file1.ts", "file1.ts", "file2.ts", "file2.ts"]; - const fileResult = watchDirectory(dir); - const linkResult = watchDirectory(link); - delayedOp(() => sys.writeFile(`${dir}/file1.ts`, "export const x = 100;")); - - // Should invoke on file as well as link, rename and change - await fileResult.deferred[0].promise; - await linkResult.deferred[0].promise; - if (!isMacOs) { - // MacOs does not get change events when new file is created - await fileResult.deferred[1].promise; - await linkResult.deferred[1].promise; - } + console.log("watchDirectory using fsEvents"); + const tableOfEvents: FsEventsForWatchDirectory = isMacOs ? + { + fileCreate: [ + { event: "rename", fileName: "file1.ts" }, + ], + linkFileCreate: [ + { event: "rename", fileName: "file2.ts" }, + ], + fileChange: [ + // On MacOs 18 and below we might get rename or change and its not deterministic + { event: ["rename", "change"], fileName: "file1.ts" }, + ], + linkFileChange: [ + // On MacOs 18 and below we might get rename or change and its not deterministic + { event: ["rename", "change"], fileName: "file2.ts" }, + ], + fileModifiedTimeChange: [ + // On MacOs 18 and below we might get rename or change and its not deterministic + { event: ["rename", "change"], fileName: "file1.ts" }, + ], + linkModifiedTimeChange: [ + // On MacOs 18 and below we might get rename or change and its not deterministic + { event: ["rename", "change"], fileName: "file2.ts" }, + ], + fileDelete: [ + { event: "rename", fileName: "file1.ts" }, + ], + linkFileDelete: [ + { event: "rename", fileName: "file2.ts" }, + ], + } : + isWindows ? + { + 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: [ + { 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 = nonRecursiveWatchDirectory(dir); + const linkResult = nonRecursiveWatchDirectory(link); - delayedOp(() => sys.writeFile(`${link}/file2.ts`, "export const x = 100;")); - // // Should invoke on file as well as link, rename and change - await fileResult.deferred[2].promise; - await linkResult.deferred[2].promise; - if (!isMacOs) { - // MacOs does not get change events when new file is created - await fileResult.deferred[3].promise; - await linkResult.deferred[3].promise; - } - fileResult.watcher.close(); + 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(); - function watchDirectory(dir: string) { - const deferred = [defer(), defer(), defer(), defer()]; - let indexForDefer = 0; - return { - dir, - watcher: fsWatch( - dir, - (event, fileName) => { - assert.equal(event, expectedEvent[indexForDefer]); - assert(!fileName || fileName === expectedFileName[indexForDefer]); - deferred[indexForDefer++].resolve(); - if (isMacOs) indexForDefer++; // MacOs does not get change events when new file is created so skip that one - }, - sys, - ), - deferred, - }; + function nonRecursiveWatchDirectory(dir: string) { + return watchDirectory(sys, fsWatch, dir, /*recursive*/ false); + } + + async function operation(opType: keyof FsEventsForWatchDirectory) { + console.log(""); + console.log(opType); + initializeWatchDirectoryResult(dirResult, linkResult); + switch (opType) { + case "fileCreate": + sys.writeFile(`${dir}/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); + } + + await verfiyWatchDirectoryResult( + opType, + dirResult, + tableOfEvents[opType], + linkResult, + tableOfEvents[opType], + ); } }); } @@ -129,6 +333,8 @@ describe("unittests:: sys:: symlinkWatching::", () => { describe("with ts.sys::", () => { const root = ts.normalizePath(IO.joinPath(IO.getWorkspaceRoot(), "tests/baselines/symlinks")); + const isMacOs = process.platform === "darwin"; + const isWindows = process.platform === "win32"; before(() => { cleanup(); ts.sys.writeFile(`${root}/polling/file.ts`, "export const x = 10;"); @@ -181,10 +387,11 @@ describe("unittests:: sys:: symlinkWatching::", () => { verifyWatchDirectoryUsingFsEvents( ts.sys, + (dir, _recursive, cb) => fs.watch(dir, { persistent: true }, cb), `${root}/dirfsevents`, `${root}/linkeddirfsevents`, - (dir, cb) => fs.watch(dir, { persistent: true }, cb), - process.platform === "darwin", + isMacOs, + isWindows, ); }); @@ -220,21 +427,32 @@ describe("unittests:: sys:: symlinkWatching::", () => { getFileName(), ); - verifyWatchDirectoryUsingFsEvents( - getSys(), - `${root}/folder`, - `${root}/linked`, - (dir, cb, sys) => sys.fsWatchWorker(dir, /*recursive*/ false, cb), - /*isMacOs*/ false, - ); + // TODO (sheetal) add test for each os behaviour + // verifyWatchDirectoryUsingFsEvents( + // getSys(), + // (dir, recursive, cb, sys) => sys.fsWatchWorker(dir, recursive, cb), + // `${root}/folder`, + // `${root}/linked`, + // /*isMacOs*/ false, + // /*isWindows*/ true, + // ); + + // verifyWatchDirectoryUsingFsEvents( + // getSys(), + // (dir, recursive, cb, sys) => sys.fsWatchWorker(dir, recursive, cb), + // `${root}/folder`, + // `${root}/linked`, + // /*isMacOs*/ true, + // /*isWindows*/ false, + // ); - // TODO (sheetal) add test for mac os behaviour so we have it on host to verify // 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 + // /*isMacOs*/ false, + // /*isWindows*/ false, // ); }); });