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