• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1namespace ts.TestFSWithWatch {
2    export const libFile: File = {
3        path: "/a/lib/lib.d.ts",
4        content: `/// <reference no-default-lib="true"/>
5interface Boolean {}
6interface Function {}
7interface CallableFunction {}
8interface NewableFunction {}
9interface IArguments {}
10interface Number { toExponential: any; }
11interface Object {}
12interface RegExp {}
13interface String { charAt: any; }
14interface Array<T> { length: number; [n: number]: T; }`
15    };
16
17    function getExecutingFilePathFromLibFile(): string {
18        return combinePaths(getDirectoryPath(libFile.path), "tsc.js");
19    }
20
21    export interface TestServerHostCreationParameters {
22        useCaseSensitiveFileNames?: boolean;
23        executingFilePath?: string;
24        currentDirectory?: string;
25        newLine?: string;
26        windowsStyleRoot?: string;
27        environmentVariables?: ESMap<string, string>;
28        runWithoutRecursiveWatches?: boolean;
29        runWithFallbackPolling?: boolean;
30        inodeWatching?: boolean;
31    }
32
33    export function createWatchedSystem(fileOrFolderList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost {
34        return new TestServerHost(fileOrFolderList, params);
35    }
36
37    export function createServerHost(fileOrFolderList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[], params?: TestServerHostCreationParameters): TestServerHost {
38        const host = new TestServerHost(fileOrFolderList, params);
39        // Just like sys, patch the host to use writeFile
40        patchWriteFileEnsuringDirectory(host);
41        return host;
42    }
43
44    export interface File {
45        path: string;
46        content: string;
47        fileSize?: number;
48    }
49
50    export interface Folder {
51        path: string;
52    }
53
54    export interface SymLink {
55        /** Location of the symlink. */
56        path: string;
57        /** Relative path to the real file. */
58        symLink: string;
59    }
60
61    export type FileOrFolderOrSymLink = File | Folder | SymLink;
62    export interface FileOrFolderOrSymLinkMap {
63        [path: string]: string | Omit<FileOrFolderOrSymLink, "path">;
64    }
65    export function isFile(fileOrFolderOrSymLink: FileOrFolderOrSymLink): fileOrFolderOrSymLink is File {
66        return isString((fileOrFolderOrSymLink as File).content);
67    }
68
69    export function isSymLink(fileOrFolderOrSymLink: FileOrFolderOrSymLink): fileOrFolderOrSymLink is SymLink {
70        return isString((fileOrFolderOrSymLink as SymLink).symLink);
71    }
72
73    interface FSEntryBase {
74        path: Path;
75        fullPath: string;
76        modifiedTime: Date;
77    }
78
79    interface FsFile extends FSEntryBase {
80        content: string;
81        fileSize?: number;
82    }
83
84    interface FsFolder extends FSEntryBase {
85        entries: SortedArray<FSEntry>;
86    }
87
88    interface FsSymLink extends FSEntryBase {
89        symLink: string;
90    }
91
92    export type FSEntry = FsFile | FsFolder | FsSymLink;
93
94    function isFsFolder(s: FSEntry | undefined): s is FsFolder {
95        return !!s && isArray((s as FsFolder).entries);
96    }
97
98    function isFsFile(s: FSEntry | undefined): s is FsFile {
99        return !!s && isString((s as FsFile).content);
100    }
101
102    function isFsSymLink(s: FSEntry | undefined): s is FsSymLink {
103        return !!s && isString((s as FsSymLink).symLink);
104    }
105
106    function invokeWatcherCallbacks<T>(callbacks: readonly T[] | undefined, invokeCallback: (cb: T) => void): void {
107        if (callbacks) {
108            // The array copy is made to ensure that even if one of the callback removes the callbacks,
109            // we dont miss any callbacks following it
110            const cbs = callbacks.slice();
111            for (const cb of cbs) {
112                invokeCallback(cb);
113            }
114        }
115    }
116
117    function createWatcher<T>(map: MultiMap<Path, T>, path: Path, callback: T): FileWatcher {
118        map.add(path, callback);
119        let closed = false;
120        return {
121            close: () => {
122                Debug.assert(!closed);
123                map.remove(path, callback);
124                closed = true;
125            }
126        };
127    }
128
129    export function getDiffInKeys<T>(map: ESMap<string, T>, expectedKeys: readonly string[]) {
130        if (map.size === expectedKeys.length) {
131            return "";
132        }
133        const notInActual: string[] = [];
134        const duplicates: string[] = [];
135        const seen = new Map<string, true>();
136        forEach(expectedKeys, expectedKey => {
137            if (seen.has(expectedKey)) {
138                duplicates.push(expectedKey);
139                return;
140            }
141            seen.set(expectedKey, true);
142            if (!map.has(expectedKey)) {
143                notInActual.push(expectedKey);
144            }
145        });
146        const inActualNotExpected: string[] = [];
147        map.forEach((_value, key) => {
148            if (!seen.has(key)) {
149                inActualNotExpected.push(key);
150            }
151            seen.set(key, true);
152        });
153        return `\n\nNotInActual: ${notInActual}\nDuplicates: ${duplicates}\nInActualButNotInExpected: ${inActualNotExpected}`;
154    }
155
156    export function verifyMapSize(caption: string, map: ESMap<string, any>, expectedKeys: readonly string[]) {
157        assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}${getDiffInKeys(map, expectedKeys)}`);
158    }
159
160    export type MapValueTester<T, U> = [ESMap<string, U[]> | undefined, (value: T) => U];
161
162    export function checkMap<T, U = undefined>(caption: string, actual: MultiMap<string, T>, expectedKeys: ReadonlyESMap<string, number>, valueTester?: MapValueTester<T,U>): void;
163    export function checkMap<T, U = undefined>(caption: string, actual: MultiMap<string, T>, expectedKeys: readonly string[], eachKeyCount: number, valueTester?: MapValueTester<T, U>): void;
164    export function checkMap<T>(caption: string, actual: ESMap<string, T> | MultiMap<string, T>, expectedKeys: readonly string[], eachKeyCount: undefined): void;
165    export function checkMap<T, U = undefined>(
166        caption: string,
167        actual: ESMap<string, T> | MultiMap<string, T>,
168        expectedKeysMapOrArray: ReadonlyESMap<string, number> | readonly string[],
169        eachKeyCountOrValueTester?: number | MapValueTester<T, U>,
170        valueTester?: MapValueTester<T, U>) {
171        const expectedKeys = isArray(expectedKeysMapOrArray) ? arrayToMap(expectedKeysMapOrArray, s => s, () => eachKeyCountOrValueTester as number) : expectedKeysMapOrArray;
172        verifyMapSize(caption, actual, isArray(expectedKeysMapOrArray) ? expectedKeysMapOrArray : arrayFrom(expectedKeys.keys()));
173        if (!isNumber(eachKeyCountOrValueTester)) {
174            valueTester = eachKeyCountOrValueTester;
175        }
176        const [expectedValues, valueMapper] = valueTester || [undefined, undefined!];
177        expectedKeys.forEach((count, name) => {
178            assert.isTrue(actual.has(name), `${caption}: expected to contain ${name}, actual keys: ${arrayFrom(actual.keys())}`);
179            // Check key information only if eachKeyCount is provided
180            if (!isArray(expectedKeysMapOrArray) || eachKeyCountOrValueTester !== undefined) {
181                assert.equal((actual as MultiMap<string, T>).get(name)!.length, count, `${caption}: Expected to be have ${count} entries for ${name}. Actual entry: ${JSON.stringify(actual.get(name))}`);
182                if (expectedValues) {
183                    assert.deepEqual(
184                        (actual as MultiMap<string, T>).get(name)!.map(valueMapper),
185                        expectedValues.get(name),
186                        `${caption}:: expected values mismatch for ${name}`
187                    );
188                }
189            }
190        });
191    }
192
193    export function checkArray(caption: string, actual: readonly string[], expected: readonly string[]) {
194        checkMap(caption, arrayToMap(actual, identity), expected, /*eachKeyCount*/ undefined);
195    }
196
197    export function checkOutputContains(host: TestServerHost, expected: readonly string[]) {
198        const mapExpected = new Set(expected);
199        const mapSeen = new Set<string>();
200        for (const f of host.getOutput()) {
201            assert.isFalse(mapSeen.has(f), `Already found ${f} in ${JSON.stringify(host.getOutput())}`);
202            if (mapExpected.has(f)) {
203                mapExpected.delete(f);
204                mapSeen.add(f);
205            }
206        }
207        assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(arrayFrom(mapExpected.keys()))} in ${JSON.stringify(host.getOutput())}`);
208    }
209
210    export function checkOutputDoesNotContain(host: TestServerHost, expectedToBeAbsent: string[] | readonly string[]) {
211        const mapExpectedToBeAbsent = new Set(expectedToBeAbsent);
212        for (const f of host.getOutput()) {
213            assert.isFalse(mapExpectedToBeAbsent.has(f), `Contains ${f} in ${JSON.stringify(host.getOutput())}`);
214        }
215    }
216
217    interface CallbackData {
218        cb: TimeOutCallback;
219        args: any[];
220        ms: number | undefined;
221        time: number;
222    }
223    class Callbacks {
224        private map: { cb: TimeOutCallback; args: any[]; ms: number | undefined; time: number; }[] = [];
225        private nextId = 1;
226
227        constructor(private host: TestServerHost) {
228        }
229
230        getNextId() {
231            return this.nextId;
232        }
233
234        register(cb: TimeOutCallback, args: any[], ms?: number) {
235            const timeoutId = this.nextId;
236            this.nextId++;
237            this.map[timeoutId] = { cb, args, ms, time: this.host.getTime() };
238            return timeoutId;
239        }
240
241        unregister(id: any) {
242            if (typeof id === "number") {
243                delete this.map[id];
244            }
245        }
246
247        count() {
248            let n = 0;
249            for (const _ in this.map) {
250                n++;
251            }
252            return n;
253        }
254
255        private invokeCallback({ cb, args, ms, time }: CallbackData) {
256            if (ms !== undefined) {
257                const newTime = ms + time;
258                if (this.host.getTime() < newTime) {
259                    this.host.setTime(newTime);
260                }
261            }
262            cb(...args);
263        }
264
265        invoke(invokeKey?: number) {
266            if (invokeKey) {
267                this.invokeCallback(this.map[invokeKey]);
268                delete this.map[invokeKey];
269                return;
270            }
271
272            // Note: invoking a callback may result in new callbacks been queued,
273            // so do not clear the entire callback list regardless. Only remove the
274            // ones we have invoked.
275            for (const key in this.map) {
276                this.invokeCallback(this.map[key]);
277                delete this.map[key];
278            }
279        }
280    }
281
282    type TimeOutCallback = (...args: any[]) => void;
283
284    export interface TestFileWatcher {
285        cb: FileWatcherCallback;
286        pollingInterval: PollingInterval;
287    }
288
289    export interface TestFsWatcher {
290        cb: FsWatchCallback;
291        inode: number | undefined;
292    }
293
294    export interface WatchInvokeOptions {
295        /** Invokes the directory watcher for the parent instead of the file changed */
296        invokeDirectoryWatcherInsteadOfFileChanged: boolean;
297        /** When new file is created, do not invoke watches for it */
298        ignoreWatchInvokedWithTriggerAsFileCreate: boolean;
299        /** Invoke the file delete, followed by create instead of file changed */
300        invokeFileDeleteCreateAsPartInsteadOfChange: boolean;
301        /** Dont invoke delete watches */
302        ignoreDelete: boolean;
303        /** Skip inode check on file or folder create*/
304        skipInodeCheckOnCreate: boolean;
305        /** When invoking rename event on fs watch, send event with file name suffixed with tilde */
306        useTildeAsSuffixInRenameEventFileName: boolean;
307    }
308
309    export enum Tsc_WatchFile {
310        DynamicPolling = "DynamicPriorityPolling",
311    }
312
313    export enum Tsc_WatchDirectory {
314        WatchFile = "RecursiveDirectoryUsingFsWatchFile",
315        NonRecursiveWatchDirectory = "RecursiveDirectoryUsingNonRecursiveWatchDirectory",
316        DynamicPolling = "RecursiveDirectoryUsingDynamicPriorityPolling"
317    }
318
319    export const timeIncrements = 1000;
320    export interface TestServerHostOptions {
321        useCaseSensitiveFileNames: boolean;
322        executingFilePath: string;
323        currentDirectory: string;
324        newLine?: string;
325        useWindowsStylePaths?: boolean;
326        environmentVariables?: ESMap<string, string>;
327    }
328
329    export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, ModuleResolutionHost {
330        args: string[] = [];
331
332        private readonly output: string[] = [];
333
334        private fs: ESMap<Path, FSEntry> = new Map();
335        private time = timeIncrements;
336        getCanonicalFileName: (s: string) => string;
337        private toPath: (f: string) => Path;
338        private timeoutCallbacks = new Callbacks(this);
339        private immediateCallbacks = new Callbacks(this);
340        readonly screenClears: number[] = [];
341
342        readonly watchedFiles = createMultiMap<Path, TestFileWatcher>();
343        readonly fsWatches = createMultiMap<Path, TestFsWatcher>();
344        readonly fsWatchesRecursive = createMultiMap<Path, TestFsWatcher>();
345        runWithFallbackPolling: boolean;
346        public readonly useCaseSensitiveFileNames: boolean;
347        public readonly newLine: string;
348        public readonly windowsStyleRoot?: string;
349        private readonly environmentVariables?: ESMap<string, string>;
350        private readonly executingFilePath: string;
351        private readonly currentDirectory: string;
352        public require: ((initialPath: string, moduleName: string) => RequireResult) | undefined;
353        public storeFilesChangingSignatureDuringEmit = true;
354        watchFile: HostWatchFile;
355        private inodeWatching: boolean | undefined;
356        private readonly inodes?: ESMap<Path, number>;
357        watchDirectory: HostWatchDirectory;
358        constructor(
359            fileOrFolderorSymLinkList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[],
360            {
361                useCaseSensitiveFileNames, executingFilePath, currentDirectory,
362                newLine, windowsStyleRoot, environmentVariables,
363                runWithoutRecursiveWatches, runWithFallbackPolling,
364                inodeWatching,
365            }: TestServerHostCreationParameters = {}) {
366            this.useCaseSensitiveFileNames = !!useCaseSensitiveFileNames;
367            this.newLine = newLine || "\n";
368            this.windowsStyleRoot = windowsStyleRoot;
369            this.environmentVariables = environmentVariables;
370            currentDirectory = currentDirectory || "/";
371            this.getCanonicalFileName = createGetCanonicalFileName(!!useCaseSensitiveFileNames);
372            this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
373            this.executingFilePath = this.getHostSpecificPath(executingFilePath || getExecutingFilePathFromLibFile());
374            this.currentDirectory = this.getHostSpecificPath(currentDirectory);
375            this.runWithFallbackPolling = !!runWithFallbackPolling;
376            const tscWatchFile = this.environmentVariables && this.environmentVariables.get("TSC_WATCHFILE");
377            const tscWatchDirectory = this.environmentVariables && this.environmentVariables.get("TSC_WATCHDIRECTORY");
378            if (inodeWatching) {
379                this.inodeWatching = true;
380                this.inodes = new Map();
381            }
382
383            const { watchFile, watchDirectory } = createSystemWatchFunctions({
384                // We dont have polling watch file
385                // it is essentially fsWatch but lets get that separate from fsWatch and
386                // into watchedFiles for easier testing
387                pollingWatchFileWorker: this.watchFileWorker.bind(this),
388                getModifiedTime: this.getModifiedTime.bind(this),
389                setTimeout: this.setTimeout.bind(this),
390                clearTimeout: this.clearTimeout.bind(this),
391                fsWatchWorker: this.fsWatchWorker.bind(this),
392                fileSystemEntryExists: this.fileSystemEntryExists.bind(this),
393                useCaseSensitiveFileNames: this.useCaseSensitiveFileNames,
394                getCurrentDirectory: this.getCurrentDirectory.bind(this),
395                fsSupportsRecursiveFsWatch: tscWatchDirectory ? false : !runWithoutRecursiveWatches,
396                getAccessibleSortedChildDirectories: path => this.getDirectories(path),
397                realpath: this.realpath.bind(this),
398                tscWatchFile,
399                tscWatchDirectory,
400                inodeWatching: !!this.inodeWatching,
401                sysLog: s => this.write(s + this.newLine),
402            });
403            this.watchFile = watchFile;
404            this.watchDirectory = watchDirectory;
405            this.reloadFS(fileOrFolderorSymLinkList);
406        }
407
408        private nextInode = 0;
409        private setInode(path: Path) {
410            if (this.inodes) this.inodes.set(path, this.nextInode++);
411        }
412
413        // Output is pretty
414        writeOutputIsTTY() {
415            return true;
416        }
417
418        getNewLine() {
419            return this.newLine;
420        }
421
422        toNormalizedAbsolutePath(s: string) {
423            return getNormalizedAbsolutePath(s, this.currentDirectory);
424        }
425
426        toFullPath(s: string) {
427            return this.toPath(this.toNormalizedAbsolutePath(s));
428        }
429
430        getHostSpecificPath(s: string) {
431            if (this.windowsStyleRoot && s.startsWith(directorySeparator)) {
432                return this.windowsStyleRoot + s.substring(1);
433            }
434            return s;
435        }
436
437        now() {
438            this.time += timeIncrements;
439            return new Date(this.time);
440        }
441
442        getTime() {
443            return this.time;
444        }
445
446        setTime(time: number) {
447            this.time = time;
448        }
449
450        private reloadFS(fileOrFolderOrSymLinkList: FileOrFolderOrSymLinkMap | readonly FileOrFolderOrSymLink[]) {
451            Debug.assert(this.fs.size === 0);
452            if (isArray(fileOrFolderOrSymLinkList)) {
453                fileOrFolderOrSymLinkList.forEach(f => this.ensureFileOrFolder(!this.windowsStyleRoot ?
454                    f :
455                    { ...f, path: this.getHostSpecificPath(f.path) }
456                ));
457            }
458            else {
459                for (const key in fileOrFolderOrSymLinkList) {
460                    if (hasProperty(fileOrFolderOrSymLinkList, key)) {
461                        const path = this.getHostSpecificPath(key);
462                        const value = fileOrFolderOrSymLinkList[key];
463                        if (isString(value)) {
464                            this.ensureFileOrFolder({ path, content: value });
465                        }
466                        else {
467                            this.ensureFileOrFolder({ path, ...value });
468                        }
469                    }
470                }
471            }
472        }
473
474        modifyFile(filePath: string, content: string, options?: Partial<WatchInvokeOptions>) {
475            const path = this.toFullPath(filePath);
476            const currentEntry = this.fs.get(path);
477            if (!currentEntry || !isFsFile(currentEntry)) {
478                throw new Error(`file not present: ${filePath}`);
479            }
480
481            if (options && options.invokeFileDeleteCreateAsPartInsteadOfChange) {
482                this.removeFileOrFolder(currentEntry, /*isRenaming*/ false, options);
483                this.ensureFileOrFolder({ path: filePath, content }, /*ignoreWatchInvokedWithTriggerAsFileCreate*/ undefined, /*ignoreParentWatch*/ undefined, options);
484            }
485            else {
486                currentEntry.content = content;
487                currentEntry.modifiedTime = this.now();
488                this.fs.get(getDirectoryPath(currentEntry.path))!.modifiedTime = this.now();
489                if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) {
490                    const directoryFullPath = getDirectoryPath(currentEntry.fullPath);
491                    this.invokeFileWatcher(directoryFullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime);
492                    this.invokeFsWatchesCallbacks(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
493                    this.invokeRecursiveFsWatches(directoryFullPath, "rename", currentEntry.modifiedTime, currentEntry.fullPath, options.useTildeAsSuffixInRenameEventFileName);
494                }
495                else {
496                    this.invokeFileAndFsWatches(currentEntry.fullPath, FileWatcherEventKind.Changed, currentEntry.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
497                }
498            }
499        }
500
501        renameFile(fileName: string, newFileName: string) {
502            const fullPath = getNormalizedAbsolutePath(fileName, this.currentDirectory);
503            const path = this.toPath(fullPath);
504            const file = this.fs.get(path) as FsFile;
505            Debug.assert(!!file);
506
507            // Only remove the file
508            this.removeFileOrFolder(file, /*isRenaming*/ true);
509
510            // Add updated folder with new folder name
511            const newFullPath = getNormalizedAbsolutePath(newFileName, this.currentDirectory);
512            const newFile = this.toFsFile({ path: newFullPath, content: file.content });
513            const newPath = newFile.path;
514            const basePath = getDirectoryPath(path);
515            Debug.assert(basePath !== path);
516            Debug.assert(basePath === getDirectoryPath(newPath));
517            const baseFolder = this.fs.get(basePath) as FsFolder;
518            this.addFileOrFolderInFolder(baseFolder, newFile);
519        }
520
521        renameFolder(folderName: string, newFolderName: string) {
522            const fullPath = getNormalizedAbsolutePath(folderName, this.currentDirectory);
523            const path = this.toPath(fullPath);
524            const folder = this.fs.get(path) as FsFolder;
525            Debug.assert(!!folder);
526
527            // Only remove the folder
528            this.removeFileOrFolder(folder, /*isRenaming*/ true);
529
530            // Add updated folder with new folder name
531            const newFullPath = getNormalizedAbsolutePath(newFolderName, this.currentDirectory);
532            const newFolder = this.toFsFolder(newFullPath);
533            const newPath = newFolder.path;
534            const basePath = getDirectoryPath(path);
535            Debug.assert(basePath !== path);
536            Debug.assert(basePath === getDirectoryPath(newPath));
537            const baseFolder = this.fs.get(basePath) as FsFolder;
538            this.addFileOrFolderInFolder(baseFolder, newFolder);
539
540            // Invoke watches for files in the folder as deleted (from old path)
541            this.renameFolderEntries(folder, newFolder);
542        }
543
544        private renameFolderEntries(oldFolder: FsFolder, newFolder: FsFolder) {
545            for (const entry of oldFolder.entries) {
546                this.fs.delete(entry.path);
547                this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Deleted);
548
549                entry.fullPath = combinePaths(newFolder.fullPath, getBaseFileName(entry.fullPath));
550                entry.path = this.toPath(entry.fullPath);
551                if (newFolder !== oldFolder) {
552                    newFolder.entries.push(entry);
553                }
554                this.fs.set(entry.path, entry);
555                this.setInode(entry.path);
556                this.invokeFileAndFsWatches(entry.fullPath, FileWatcherEventKind.Created);
557                if (isFsFolder(entry)) {
558                    this.renameFolderEntries(entry, entry);
559                }
560            }
561        }
562
563        ensureFileOrFolder(fileOrDirectoryOrSymLink: FileOrFolderOrSymLink, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean, ignoreParentWatch?: boolean, options?: Partial<WatchInvokeOptions>) {
564            if (isFile(fileOrDirectoryOrSymLink)) {
565                const file = this.toFsFile(fileOrDirectoryOrSymLink);
566                // file may already exist when updating existing type declaration file
567                if (!this.fs.get(file.path)) {
568                    const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath), ignoreParentWatch, options);
569                    this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate, options);
570                }
571            }
572            else if (isSymLink(fileOrDirectoryOrSymLink)) {
573                const symLink = this.toFsSymLink(fileOrDirectoryOrSymLink);
574                Debug.assert(!this.fs.get(symLink.path));
575                const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath), ignoreParentWatch, options);
576                this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate, options);
577            }
578            else {
579                const fullPath = getNormalizedAbsolutePath(fileOrDirectoryOrSymLink.path, this.currentDirectory);
580                this.ensureFolder(getDirectoryPath(fullPath), ignoreParentWatch, options);
581                this.ensureFolder(fullPath, ignoreWatchInvokedWithTriggerAsFileCreate, options);
582            }
583        }
584
585        private ensureFolder(fullPath: string, ignoreWatch: boolean | undefined, options: Partial<WatchInvokeOptions> | undefined): FsFolder {
586            const path = this.toPath(fullPath);
587            let folder = this.fs.get(path) as FsFolder;
588            if (!folder) {
589                folder = this.toFsFolder(fullPath);
590                const baseFullPath = getDirectoryPath(fullPath);
591                if (fullPath !== baseFullPath) {
592                    // Add folder in the base folder
593                    const baseFolder = this.ensureFolder(baseFullPath, ignoreWatch, options);
594                    this.addFileOrFolderInFolder(baseFolder, folder, ignoreWatch, options);
595                }
596                else {
597                    // root folder
598                    Debug.assert(this.fs.size === 0 || !!this.windowsStyleRoot);
599                    this.fs.set(path, folder);
600                    this.setInode(path);
601                }
602            }
603            Debug.assert(isFsFolder(folder));
604            return folder;
605        }
606
607        private addFileOrFolderInFolder(folder: FsFolder, fileOrDirectory: FsFile | FsFolder | FsSymLink, ignoreWatch?: boolean, options?: Partial<WatchInvokeOptions>) {
608            if (!this.fs.has(fileOrDirectory.path)) {
609                insertSorted(folder.entries, fileOrDirectory, (a, b) => compareStringsCaseSensitive(getBaseFileName(a.path), getBaseFileName(b.path)));
610            }
611            folder.modifiedTime = this.now();
612            this.fs.set(fileOrDirectory.path, fileOrDirectory);
613            this.setInode(fileOrDirectory.path);
614
615            if (ignoreWatch) {
616                return;
617            }
618            const inodeWatching = this.inodeWatching;
619            if (options?.skipInodeCheckOnCreate) this.inodeWatching = false;
620            this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Created, fileOrDirectory.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
621            this.invokeFileAndFsWatches(folder.fullPath, FileWatcherEventKind.Changed, fileOrDirectory.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
622            this.inodeWatching = inodeWatching;
623        }
624
625        private removeFileOrFolder(fileOrDirectory: FsFile | FsFolder | FsSymLink, isRenaming?: boolean, options?: Partial<WatchInvokeOptions>) {
626            const basePath = getDirectoryPath(fileOrDirectory.path);
627            const baseFolder = this.fs.get(basePath) as FsFolder;
628            if (basePath !== fileOrDirectory.path) {
629                Debug.assert(!!baseFolder);
630                baseFolder.modifiedTime = this.now();
631                filterMutate(baseFolder.entries, entry => entry !== fileOrDirectory);
632            }
633            this.fs.delete(fileOrDirectory.path);
634
635            if (isFsFolder(fileOrDirectory)) {
636                Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming);
637            }
638            if (!options?.ignoreDelete) this.invokeFileAndFsWatches(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted, /*modifiedTime*/ undefined, options?.useTildeAsSuffixInRenameEventFileName);
639            this.inodes?.delete(fileOrDirectory.path);
640            if (!options?.ignoreDelete) this.invokeFileAndFsWatches(baseFolder.fullPath, FileWatcherEventKind.Changed, baseFolder.modifiedTime, options?.useTildeAsSuffixInRenameEventFileName);
641        }
642
643        deleteFile(filePath: string) {
644            const path = this.toFullPath(filePath);
645            const currentEntry = this.fs.get(path) as FsFile;
646            Debug.assert(isFsFile(currentEntry));
647            this.removeFileOrFolder(currentEntry);
648        }
649
650        deleteFolder(folderPath: string, recursive?: boolean) {
651            const path = this.toFullPath(folderPath);
652            const currentEntry = this.fs.get(path) as FsFolder;
653            Debug.assert(isFsFolder(currentEntry));
654            if (recursive && currentEntry.entries.length) {
655                const subEntries = currentEntry.entries.slice();
656                subEntries.forEach(fsEntry => {
657                    if (isFsFolder(fsEntry)) {
658                        this.deleteFolder(fsEntry.fullPath, recursive);
659                    }
660                    else {
661                        this.removeFileOrFolder(fsEntry);
662                    }
663                });
664            }
665            this.removeFileOrFolder(currentEntry);
666        }
667
668        private watchFileWorker(fileName: string, cb: FileWatcherCallback, pollingInterval: PollingInterval) {
669            return createWatcher(
670                this.watchedFiles,
671                this.toFullPath(fileName),
672                { cb, pollingInterval }
673            );
674        }
675
676        private fsWatchWorker(
677            fileOrDirectory: string,
678            recursive: boolean,
679            cb: FsWatchCallback,
680        ) {
681            if (this.runWithFallbackPolling) throw new Error("Need to use fallback polling instead of file system native watching");
682            const path = this.toFullPath(fileOrDirectory);
683            // Error if the path does not exist
684            if (this.inodeWatching && !this.inodes?.has(path)) throw new Error();
685            const result = createWatcher(
686                recursive ? this.fsWatchesRecursive : this.fsWatches,
687                path,
688                {
689                    cb,
690                    inode: this.inodes?.get(path)
691                }
692            ) as FsWatchWorkerWatcher;
693            result.on = noop;
694            return result;
695        }
696
697        invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind, modifiedTime: Date | undefined) {
698            invokeWatcherCallbacks(this.watchedFiles.get(this.toPath(fileFullPath)), ({ cb }) => cb(fileFullPath, eventKind, modifiedTime));
699        }
700
701        private fsWatchCallback(map: MultiMap<Path, TestFsWatcher>, fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, entryFullPath: string | undefined, useTildeSuffix: boolean | undefined) {
702            const path = this.toPath(fullPath);
703            const currentInode = this.inodes?.get(path);
704            invokeWatcherCallbacks(map.get(path), ({ cb, inode }) => {
705                // TODO::
706                if (this.inodeWatching && inode !== undefined && inode !== currentInode) return;
707                let relativeFileName = (entryFullPath ? this.getRelativePathToDirectory(fullPath, entryFullPath) : "");
708                if (useTildeSuffix) relativeFileName = (relativeFileName ? relativeFileName : getBaseFileName(fullPath)) + "~";
709                cb(eventName, relativeFileName, modifiedTime);
710            });
711        }
712
713        invokeFsWatchesCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
714            this.fsWatchCallback(this.fsWatches, fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
715        }
716
717        invokeFsWatchesRecursiveCallbacks(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
718            this.fsWatchCallback(this.fsWatchesRecursive, fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
719        }
720
721        private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) {
722            return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false);
723        }
724
725        private invokeRecursiveFsWatches(fullPath: string, eventName: "rename" | "change", modifiedTime?: Date, entryFullPath?: string, useTildeSuffix?: boolean) {
726            this.invokeFsWatchesRecursiveCallbacks(fullPath, eventName, modifiedTime, entryFullPath, useTildeSuffix);
727            const basePath = getDirectoryPath(fullPath);
728            if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) {
729                this.invokeRecursiveFsWatches(basePath, eventName, modifiedTime, entryFullPath || fullPath, useTildeSuffix);
730            }
731        }
732
733        invokeFsWatches(fullPath: string, eventName: "rename" | "change", modifiedTime: Date | undefined, useTildeSuffix: boolean | undefined) {
734            this.invokeFsWatchesCallbacks(fullPath, eventName, modifiedTime, fullPath, useTildeSuffix);
735            this.invokeFsWatchesCallbacks(getDirectoryPath(fullPath), eventName, modifiedTime, fullPath, useTildeSuffix);
736            this.invokeRecursiveFsWatches(fullPath, eventName, modifiedTime, /*entryFullPath*/ undefined, useTildeSuffix);
737        }
738
739        private invokeFileAndFsWatches(fileOrFolderFullPath: string, eventKind: FileWatcherEventKind, modifiedTime?: Date, useTildeSuffix?: boolean) {
740            this.invokeFileWatcher(fileOrFolderFullPath, eventKind, modifiedTime);
741            this.invokeFsWatches(fileOrFolderFullPath, eventKind === FileWatcherEventKind.Changed ? "change" : "rename", modifiedTime, useTildeSuffix);
742        }
743
744        private toFsEntry(path: string): FSEntryBase {
745            const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory);
746            return {
747                path: this.toPath(fullPath),
748                fullPath,
749                modifiedTime: this.now()
750            };
751        }
752
753        private toFsFile(file: File): FsFile {
754            const fsFile = this.toFsEntry(file.path) as FsFile;
755            fsFile.content = file.content;
756            fsFile.fileSize = file.fileSize;
757            return fsFile;
758        }
759
760        private toFsSymLink(symLink: SymLink): FsSymLink {
761            const fsSymLink = this.toFsEntry(symLink.path) as FsSymLink;
762            fsSymLink.symLink = getNormalizedAbsolutePath(symLink.symLink, getDirectoryPath(fsSymLink.fullPath));
763            return fsSymLink;
764        }
765
766        private toFsFolder(path: string): FsFolder {
767            const fsFolder = this.toFsEntry(path) as FsFolder;
768            fsFolder.entries = [] as FSEntry[] as SortedArray<FSEntry>; // https://github.com/Microsoft/TypeScript/issues/19873
769            return fsFolder;
770        }
771
772        private getRealFsEntry<T extends FSEntry>(isFsEntry: (fsEntry: FSEntry) => fsEntry is T, path: Path, fsEntry = this.fs.get(path)!): T | undefined {
773            if (isFsEntry(fsEntry)) {
774                return fsEntry;
775            }
776
777            if (isFsSymLink(fsEntry)) {
778                return this.getRealFsEntry(isFsEntry, this.toPath(fsEntry.symLink));
779            }
780
781            if (fsEntry) {
782                // This fs entry is something else
783                return undefined;
784            }
785
786            const realpath = this.toPath(this.realpath(path));
787            if (path !== realpath) {
788                return this.getRealFsEntry(isFsEntry, realpath);
789            }
790
791            return undefined;
792        }
793
794        private isFsFile(fsEntry: FSEntry) {
795            return !!this.getRealFile(fsEntry.path, fsEntry);
796        }
797
798        private getRealFile(path: Path, fsEntry?: FSEntry): FsFile | undefined {
799            return this.getRealFsEntry(isFsFile, path, fsEntry);
800        }
801
802        private isFsFolder(fsEntry: FSEntry) {
803            return !!this.getRealFolder(fsEntry.path, fsEntry);
804        }
805
806        private getRealFolder(path: Path, fsEntry = this.fs.get(path)): FsFolder | undefined {
807            return this.getRealFsEntry(isFsFolder, path, fsEntry);
808        }
809
810        fileSystemEntryExists(s: string, entryKind: FileSystemEntryKind) {
811            return entryKind === FileSystemEntryKind.File ? this.fileExists(s) : this.directoryExists(s);
812        }
813
814        fileExists(s: string) {
815            const path = this.toFullPath(s);
816            return !!this.getRealFile(path);
817        }
818
819        getModifiedTime(s: string) {
820            const path = this.toFullPath(s);
821            const fsEntry = this.fs.get(path);
822            return (fsEntry && fsEntry.modifiedTime)!; // TODO: GH#18217
823        }
824
825        setModifiedTime(s: string, date: Date) {
826            const path = this.toFullPath(s);
827            const fsEntry = this.fs.get(path);
828            if (fsEntry) {
829                fsEntry.modifiedTime = date;
830                this.invokeFileAndFsWatches(fsEntry.fullPath, FileWatcherEventKind.Changed, fsEntry.modifiedTime);
831            }
832        }
833
834        readFile(s: string): string | undefined {
835            const fsEntry = this.getRealFile(this.toFullPath(s));
836            return fsEntry ? fsEntry.content : undefined;
837        }
838
839        getFileSize(s: string) {
840            const path = this.toFullPath(s);
841            const entry = this.fs.get(path)!;
842            if (isFsFile(entry)) {
843                return entry.fileSize ? entry.fileSize : entry.content.length;
844            }
845            return undefined!; // TODO: GH#18217
846        }
847
848        directoryExists(s: string) {
849            const path = this.toFullPath(s);
850            return !!this.getRealFolder(path);
851        }
852
853        getDirectories(s: string): string[] {
854            const path = this.toFullPath(s);
855            const folder = this.getRealFolder(path);
856            if (folder) {
857                return mapDefined(folder.entries, entry => this.isFsFolder(entry) ? getBaseFileName(entry.fullPath) : undefined);
858            }
859            Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder");
860        }
861
862        getJsDocNodeCheckedConfig(jsDocFileCheckInfo: FileCheckModuleInfo, sourceFilePath: string): JsDocNodeCheckConfig {
863            Debug.log(jsDocFileCheckInfo.fileNeedCheck.toString());
864            Debug.log(sourceFilePath);
865            return {
866                nodeNeedCheck: false,
867                checkConfig: [],
868            };
869        }
870
871        getFileCheckedModuleInfo?(sourceFilePath: string): FileCheckModuleInfo {
872            Debug.log(sourceFilePath);
873            return {
874                fileNeedCheck: false,
875                checkPayload: undefined,
876                currentFileName: "",
877            }
878        }
879
880        getJsDocNodeConditionCheckedResult?(jsDocFileCheckedInfo: FileCheckModuleInfo, jsDocs: JsDocTagInfo[]): ConditionCheckResult {
881            Debug.log(jsDocFileCheckedInfo.fileNeedCheck.toString());
882            Debug.log(jsDocs.toString());
883            return {
884                valid: true,
885                message: "",
886                type: DiagnosticCategory.Warning
887            };
888        }
889
890        readDirectory(path: string, extensions?: readonly string[], exclude?: readonly string[], include?: readonly string[], depth?: number): string[] {
891            return matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => {
892                const directories: string[] = [];
893                const files: string[] = [];
894                const folder = this.getRealFolder(this.toPath(dir));
895                if (folder) {
896                    folder.entries.forEach((entry) => {
897                        if (this.isFsFolder(entry)) {
898                            directories.push(getBaseFileName(entry.fullPath));
899                        }
900                        else if (this.isFsFile(entry)) {
901                            files.push(getBaseFileName(entry.fullPath));
902                        }
903                        else {
904                            Debug.fail("Unknown entry");
905                        }
906                    });
907                }
908                return { directories, files };
909            }, path => this.realpath(path));
910        }
911
912        createHash(s: string): string {
913            return `${generateDjb2Hash(s)}-${s}`;
914        }
915
916        createSHA256Hash(s: string): string {
917            return sys.createSHA256Hash!(s);
918        }
919
920        // TOOD: record and invoke callbacks to simulate timer events
921        setTimeout(callback: TimeOutCallback, ms: number, ...args: any[]) {
922            return this.timeoutCallbacks.register(callback, args, ms);
923        }
924
925        getNextTimeoutId() {
926            return this.timeoutCallbacks.getNextId();
927        }
928
929        clearTimeout(timeoutId: any): void {
930            this.timeoutCallbacks.unregister(timeoutId);
931        }
932
933        clearScreen(): void {
934            this.screenClears.push(this.output.length);
935        }
936
937        checkTimeoutQueueLengthAndRun(expected: number) {
938            this.checkTimeoutQueueLength(expected);
939            this.runQueuedTimeoutCallbacks();
940        }
941
942        checkTimeoutQueueLength(expected: number) {
943            const callbacksCount = this.timeoutCallbacks.count();
944            assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`);
945        }
946
947        runQueuedTimeoutCallbacks(timeoutId?: number) {
948            try {
949                this.timeoutCallbacks.invoke(timeoutId);
950            }
951            catch (e) {
952                if (e.message === this.exitMessage) {
953                    return;
954                }
955                throw e;
956            }
957        }
958
959        runQueuedImmediateCallbacks(checkCount?: number) {
960            if (checkCount !== undefined) {
961                assert.equal(this.immediateCallbacks.count(), checkCount);
962            }
963            this.immediateCallbacks.invoke();
964        }
965
966        setImmediate(callback: TimeOutCallback, ...args: any[]) {
967            return this.immediateCallbacks.register(callback, args);
968        }
969
970        clearImmediate(timeoutId: any): void {
971            this.immediateCallbacks.unregister(timeoutId);
972        }
973
974        createDirectory(directoryName: string): void {
975            const folder = this.toFsFolder(directoryName);
976
977            // base folder has to be present
978            const base = getDirectoryPath(folder.path);
979            const baseFolder = this.fs.get(base) as FsFolder;
980            Debug.assert(isFsFolder(baseFolder));
981
982            Debug.assert(!this.fs.get(folder.path));
983            this.addFileOrFolderInFolder(baseFolder, folder);
984        }
985
986        writeFile(path: string, content: string): void {
987            const file = this.toFsFile({ path, content });
988
989            // base folder has to be present
990            const base = getDirectoryPath(file.path);
991            const folder = Debug.checkDefined(this.getRealFolder(base));
992
993            if (folder.path === base) {
994                if (!this.fs.has(file.path)) {
995                    this.addFileOrFolderInFolder(folder, file);
996                }
997                else {
998                    this.modifyFile(path, content);
999                }
1000            }
1001            else {
1002                this.writeFile(this.realpath(path), content);
1003            }
1004        }
1005
1006        prependFile(path: string, content: string, options?: Partial<WatchInvokeOptions>): void {
1007            this.modifyFile(path, content + this.readFile(path), options);
1008        }
1009
1010        appendFile(path: string, content: string, options?: Partial<WatchInvokeOptions>): void {
1011            this.modifyFile(path, this.readFile(path) + content, options);
1012        }
1013
1014        write(message: string) {
1015            if (Debug.isDebugging) console.log(message);
1016            this.output.push(message);
1017        }
1018
1019        getOutput(): readonly string[] {
1020            return this.output;
1021        }
1022
1023        clearOutput() {
1024            clear(this.output);
1025            this.screenClears.length = 0;
1026        }
1027
1028        serializeOutput(baseline: string[]) {
1029            const output = this.getOutput();
1030            let start = 0;
1031            baseline.push("Output::");
1032            for (const screenClear of this.screenClears) {
1033                baselineOutputs(baseline, output, start, screenClear);
1034                start = screenClear;
1035                baseline.push(">> Screen clear");
1036            }
1037            baselineOutputs(baseline, output, start);
1038            baseline.push("");
1039            this.clearOutput();
1040        }
1041
1042        snap(): ESMap<Path, FSEntry> {
1043            const result = new Map<Path, FSEntry>();
1044            this.fs.forEach((value, key) => {
1045                const cloneValue = clone(value);
1046                if (isFsFolder(cloneValue)) {
1047                    cloneValue.entries = cloneValue.entries.map(clone) as SortedArray<FSEntry>;
1048                }
1049                result.set(key, cloneValue);
1050            });
1051
1052            return result;
1053        }
1054
1055        writtenFiles?: ESMap<Path, number>;
1056        diff(baseline: string[], base: ESMap<Path, FSEntry> = new Map()) {
1057            this.fs.forEach((newFsEntry, path) => {
1058                diffFsEntry(baseline, base.get(path), newFsEntry, this.inodes?.get(path), this.writtenFiles);
1059            });
1060            base.forEach((oldFsEntry, path) => {
1061                const newFsEntry = this.fs.get(path);
1062                if (!newFsEntry) {
1063                    diffFsEntry(baseline, oldFsEntry, newFsEntry, this.inodes?.get(path), this.writtenFiles);
1064                }
1065            });
1066            baseline.push("");
1067        }
1068
1069        serializeWatches(baseline: string[] = []) {
1070            serializeMultiMap(baseline, "PolledWatches", this.watchedFiles);
1071            baseline.push("");
1072            serializeMultiMap(baseline, "FsWatches", this.fsWatches);
1073            baseline.push("");
1074            serializeMultiMap(baseline, "FsWatchesRecursive", this.fsWatchesRecursive);
1075            baseline.push("");
1076            return baseline;
1077        }
1078
1079        realpath(s: string): string {
1080            const fullPath = this.toNormalizedAbsolutePath(s);
1081            const path = this.toPath(fullPath);
1082            if (getDirectoryPath(path) === path) {
1083                // Root
1084                return s;
1085            }
1086            const dirFullPath = this.realpath(getDirectoryPath(fullPath));
1087            const realFullPath = combinePaths(dirFullPath, getBaseFileName(fullPath));
1088            const fsEntry = this.fs.get(this.toPath(realFullPath))!;
1089            if (isFsSymLink(fsEntry)) {
1090                return this.realpath(fsEntry.symLink);
1091            }
1092
1093            // realpath supports non-existent files, so there may not be an fsEntry
1094            return fsEntry?.fullPath || realFullPath;
1095        }
1096
1097        readonly exitMessage = "System Exit";
1098        exitCode: number | undefined;
1099        readonly resolvePath = (s: string) => s;
1100        readonly getExecutingFilePath = () => this.executingFilePath;
1101        readonly getCurrentDirectory = () => this.currentDirectory;
1102        exit(exitCode?: number) {
1103            this.exitCode = exitCode;
1104            throw new Error(this.exitMessage);
1105        }
1106        getEnvironmentVariable(name: string) {
1107            return this.environmentVariables && this.environmentVariables.get(name) || "";
1108        }
1109    }
1110
1111    function diffFsFile(baseline: string[], fsEntry: FsFile, newInode: number | undefined) {
1112        baseline.push(`//// [${fsEntry.fullPath}]${inodeString(newInode)}\r\n${fsEntry.content}`, "");
1113    }
1114    function diffFsSymLink(baseline: string[], fsEntry: FsSymLink, newInode: number | undefined) {
1115        baseline.push(`//// [${fsEntry.fullPath}] symlink(${fsEntry.symLink})${inodeString(newInode)}`);
1116    }
1117    function inodeString(inode: number | undefined) {
1118        return inode !== undefined ? ` Inode:: ${inode}` : "";
1119    }
1120    function diffFsEntry(baseline: string[], oldFsEntry: FSEntry | undefined, newFsEntry: FSEntry | undefined, newInode: number | undefined, writtenFiles: ESMap<string, any> | undefined): void {
1121        const file = newFsEntry && newFsEntry.fullPath;
1122        if (isFsFile(oldFsEntry)) {
1123            if (isFsFile(newFsEntry)) {
1124                if (oldFsEntry.content !== newFsEntry.content) {
1125                    diffFsFile(baseline, newFsEntry, newInode);
1126                }
1127                else if (oldFsEntry.modifiedTime !== newFsEntry.modifiedTime) {
1128                    if (oldFsEntry.fullPath !== newFsEntry.fullPath) {
1129                        baseline.push(`//// [${file}] file was renamed from file ${oldFsEntry.fullPath}${inodeString(newInode)}`);
1130                    }
1131                    else if (writtenFiles && !writtenFiles.has(newFsEntry.path)) {
1132                        baseline.push(`//// [${file}] file changed its modified time${inodeString(newInode)}`);
1133                    }
1134                    else {
1135                        baseline.push(`//// [${file}] file written with same contents${inodeString(newInode)}`);
1136                    }
1137                }
1138            }
1139            else {
1140                baseline.push(`//// [${oldFsEntry.fullPath}] deleted`);
1141                if (isFsSymLink(newFsEntry)) {
1142                    diffFsSymLink(baseline, newFsEntry, newInode);
1143                }
1144            }
1145        }
1146        else if (isFsSymLink(oldFsEntry)) {
1147            if (isFsSymLink(newFsEntry)) {
1148                if (oldFsEntry.symLink !== newFsEntry.symLink) {
1149                    diffFsSymLink(baseline, newFsEntry, newInode);
1150                }
1151                else if (oldFsEntry.modifiedTime !== newFsEntry.modifiedTime) {
1152                    if (oldFsEntry.fullPath !== newFsEntry.fullPath) {
1153                        baseline.push(`//// [${file}] symlink was renamed from symlink ${oldFsEntry.fullPath}${inodeString(newInode)}`);
1154                    }
1155                    else if (writtenFiles && !writtenFiles.has(newFsEntry.path)) {
1156                        baseline.push(`//// [${file}] symlink changed its modified time${inodeString(newInode)}`);
1157                    }
1158                    else {
1159                        baseline.push(`//// [${file}] symlink written with same link${inodeString(newInode)}`);
1160                    }
1161                }
1162            }
1163            else {
1164                baseline.push(`//// [${oldFsEntry.fullPath}] deleted symlink`);
1165                if (isFsFile(newFsEntry)) {
1166                    diffFsFile(baseline, newFsEntry, newInode);
1167                }
1168            }
1169        }
1170        else if (isFsFile(newFsEntry)) {
1171            diffFsFile(baseline, newFsEntry, newInode);
1172        }
1173        else if (isFsSymLink(newFsEntry)) {
1174            diffFsSymLink(baseline, newFsEntry, newInode);
1175        }
1176    }
1177
1178    function serializeMultiMap<T>(baseline: string[], caption: string, multiMap: MultiMap<string, T>) {
1179        baseline.push(`${caption}::`);
1180        multiMap.forEach((values, key) => {
1181            baseline.push(`${key}:`);
1182            for (const value of values) {
1183                baseline.push(`  ${JSON.stringify(value)}`);
1184            }
1185        });
1186    }
1187
1188    function baselineOutputs(baseline: string[], output: readonly string[], start: number, end = output.length) {
1189        let baselinedOutput: string[] | undefined;
1190        for (let i = start; i < end; i++) {
1191            (baselinedOutput ||= []).push(output[i].replace(/Elapsed::\s[0-9]+(?:\.\d+)?ms/g, "Elapsed:: *ms"));
1192        }
1193        if (baselinedOutput) baseline.push(baselinedOutput.join(""));
1194    }
1195
1196    export type TestServerHostTrackingWrittenFiles = TestServerHost & { writtenFiles: ESMap<Path, number>; };
1197
1198    export function changeToHostTrackingWrittenFiles(inputHost: TestServerHost) {
1199        const host = inputHost as TestServerHostTrackingWrittenFiles;
1200        const originalWriteFile = host.writeFile;
1201        host.writtenFiles = new Map<Path, number>();
1202        host.writeFile = (fileName, content) => {
1203            originalWriteFile.call(host, fileName, content);
1204            const path = host.toFullPath(fileName);
1205            host.writtenFiles.set(path, (host.writtenFiles.get(path) || 0) + 1);
1206        };
1207        return host;
1208    }
1209    export const tsbuildProjectsLocation = "/user/username/projects";
1210    export function getTsBuildProjectFilePath(project: string, file: string) {
1211        return `${tsbuildProjectsLocation}/${project}/${file}`;
1212    }
1213
1214    export function getTsBuildProjectFile(project: string, file: string): File {
1215        return {
1216            path: getTsBuildProjectFilePath(project, file),
1217            content: Harness.IO.readFile(`${Harness.IO.getWorkspaceRoot()}/tests/projects/${project}/${file}`)!
1218        };
1219    }
1220}
1221