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