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