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