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