namespace ts . TestFSWithWatch { export const libFile = { path : "/a/lib/lib.d.ts" , content : `/// <reference no-default-lib="true"/>
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> { length: number; [n: number]: T; }` } ; export const safeList = { path : "/safeList.json" as Path , content : JSON . stringify ( { commander : "commander" , express : "express" , jquery : "jquery" , lodash : "lodash" , moment : "moment" , chroma : "chroma-js" } ) } ; function getExecutingFilePathFromLibFile ( ) { return combinePaths ( getDirectoryPath ( libFile . path ) , "tsc.js" ) ; } export interface TestServerHostCreationParameters { useCaseSensitiveFileNames ? : boolean ; executingFilePath ? : string ; currentDirectory ? : string ; newLine ? : string ; windowsStyleRoot ? : string ; environmentVariables ? : ESMap < string , string > ; runWithoutRecursiveWatches ? : boolean ; runWithFallbackPolling ? : boolean ; } export function createWatchedSystem ( fileOrFolderList , params ? ) { return new TestServerHost ( false , fileOrFolderList , params ) ; } export function createServerHost ( fileOrFolderList , params ? ) { const host = new TestServerHost ( true , fileOrFolderList , params ) ; patchWriteFileEnsuringDirectory ( host ) ; return host ; } export interface File { path : string ; content : string ; fileSize ? : number ; } export interface Folder { path : string ; } export interface SymLink { /** Location of the symlink. */ path : string ; /** Relative path to the real file. */ symLink : string ; } export type FileOrFolderOrSymLink = File | Folder | SymLink ; export function isFile ( fileOrFolderOrSymLink ) { return isString ( ( fileOrFolderOrSymLink as File ) . content ) ; } export function isSymLink ( fileOrFolderOrSymLink ) { return isString ( ( fileOrFolderOrSymLink as SymLink ) . symLink ) ; } interface FSEntryBase { path : Path ; fullPath : string ; modifiedTime : Date ; } interface FsFile extends FSEntryBase { content : string ; fileSize ? : number ; } interface FsFolder extends FSEntryBase { entries : SortedArray < FSEntry > ; } interface FsSymLink extends FSEntryBase { symLink : string ; } type FSEntry = FsFile | FsFolder | FsSymLink ; function isFsFolder ( s ) { return ! ! s && isArray ( ( s as FsFolder ) . entries ) ; } function isFsFile ( s ) { return ! ! s && isString ( ( s as FsFile ) . content ) ; } function isFsSymLink ( s ) { return ! ! s && isString ( ( s as FsSymLink ) . symLink ) ; } function invokeWatcherCallbacks < T > ( callbacks , invokeCallback ) { if ( callbacks ) { const cbs = callbacks . slice ( ) ; for ( const cb of cbs ) { invokeCallback ( cb ) ; } } } function createWatcher < T > ( map , path , callback ) { map . add ( path , callback ) ; return { close : ( ) => map . remove ( path , callback ) } ; } export function getDiffInKeys < T > ( map , expectedKeys ) { if ( map . size === expectedKeys . length ) { return "" ; } const notInActual = [ ] ; const duplicates = [ ] ; const seen = new Map < string , true > ( ) ; forEach ( expectedKeys , expectedKey => { if ( seen . has ( expectedKey ) ) { duplicates . push ( expectedKey ) ; return ; } seen . set ( expectedKey , true ) ; if ( ! map . has ( expectedKey ) ) { notInActual . push ( expectedKey ) ; } } ) ; const inActualNotExpected = [ ] ; map . forEach ( ( _value , key ) => { if ( ! seen . has ( key ) ) { inActualNotExpected . push ( key ) ; } seen . set ( key , true ) ; } ) ; return `\n\nNotInActual: ${ notInActual }\nDuplicates: ${ duplicates }\nInActualButNotInExpected: ${ inActualNotExpected }` ; } export function verifyMapSize ( caption , map , expectedKeys ) { assert . equal ( map . size , expectedKeys . length , `${ caption }: incorrect size of map: Actual keys: ${ arrayFrom ( map . keys ( ) ) } Expected: ${ expectedKeys }${ getDiffInKeys ( map , expectedKeys ) }` ) ; } export type MapValueTester < T , U > = [ ESMap < string , U [ ] > | undefined , ( value : T ) => U ] ; export function checkMap < T , U = undefined > ( caption , actual , expectedKeys , valueTester ? ) ; export function checkMap < T , U = undefined > ( caption , actual , expectedKeys , eachKeyCount , valueTester ? ) ; export function checkMap < T > ( caption , actual , expectedKeys , eachKeyCount ) ; export function checkMap < T , U = undefined > ( caption , actual , expectedKeysMapOrArray , eachKeyCountOrValueTester ? , valueTester ? ) { const expectedKeys = isArray ( expectedKeysMapOrArray ) ? arrayToMap ( expectedKeysMapOrArray , s => s , ( ) => eachKeyCountOrValueTester as number ) : expectedKeysMapOrArray ; verifyMapSize ( caption , actual , isArray ( expectedKeysMapOrArray ) ? expectedKeysMapOrArray : arrayFrom ( expectedKeys . keys ( ) ) ) ; if ( ! isNumber ( eachKeyCountOrValueTester ) ) { valueTester = eachKeyCountOrValueTester ; } const [ expectedValues , valueMapper ] = valueTester || [ undefined , undefined ! ] ; expectedKeys . forEach ( ( count , name ) => { assert . isTrue ( actual . has ( name ) , `${ caption }: expected to contain ${ name }, actual keys: ${ arrayFrom ( actual . keys ( ) ) }` ) ; if ( ! isArray ( expectedKeysMapOrArray ) || eachKeyCountOrValueTester !== undefined ) { 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 ) ) }` ) ; if ( expectedValues ) { assert . deepEqual ( ( actual as MultiMap < string , T > ) . get ( name ) ! . map ( valueMapper ) , expectedValues . get ( name ) , `${ caption }:: expected values mismatch for ${ name }` ) ; } } } ) ; } export function checkArray ( caption , actual , expected ) { checkMap ( caption , arrayToMap ( actual , identity ) , expected , undefined ) ; } export function checkWatchedFiles ( host , expectedFiles , additionalInfo ? ) { checkMap ( `watchedFiles:: ${ additionalInfo || "" }::` , host . watchedFiles , expectedFiles , undefined ) ; } export interface WatchFileDetails { fileName : string ; pollingInterval : PollingInterval ; } export function checkWatchedFilesDetailed ( host , expectedFiles , expectedDetails ? ) ; export function checkWatchedFilesDetailed ( host , expectedFiles , eachFileWatchCount , expectedDetails ? ) ; export function checkWatchedFilesDetailed ( host , expectedFiles , eachFileWatchCountOrExpectedDetails ? , expectedDetails ? ) { if ( ! isNumber ( eachFileWatchCountOrExpectedDetails ) ) expectedDetails = eachFileWatchCountOrExpectedDetails ; if ( isArray ( expectedFiles ) ) { checkMap ( "watchedFiles" , host . watchedFiles , expectedFiles , eachFileWatchCountOrExpectedDetails as number , [ expectedDetails , ( { fileName , pollingInterval } ) => ( { fileName , pollingInterval } ) ] ) ; } else { checkMap ( "watchedFiles" , host . watchedFiles , expectedFiles , [ expectedDetails , ( { fileName , pollingInterval } ) => ( { fileName , pollingInterval } ) ] ) ; } } export function checkWatchedDirectories ( host , expectedDirectories , recursive ) { checkMap ( `watchedDirectories${ recursive ? " recursive" : "" }` , recursive ? host . fsWatchesRecursive : host . fsWatches , expectedDirectories , undefined ) ; } export interface WatchDirectoryDetails { directoryName : string ; fallbackPollingInterval : PollingInterval ; fallbackOptions : WatchOptions | undefined ; } export function checkWatchedDirectoriesDetailed ( host , expectedDirectories , recursive , expectedDetails ? ) ; export function checkWatchedDirectoriesDetailed ( host , expectedDirectories , eachDirectoryWatchCount , recursive , expectedDetails ? ) ; export function checkWatchedDirectoriesDetailed ( host , expectedDirectories , recursiveOrEachDirectoryWatchCount , recursiveOrExpectedDetails ? , expectedDetails ? ) { if ( typeof recursiveOrExpectedDetails !== "boolean" ) expectedDetails = recursiveOrExpectedDetails ; if ( isArray ( expectedDirectories ) ) { checkMap ( `fsWatches${ recursiveOrExpectedDetails ? " recursive" : "" }` , recursiveOrExpectedDetails as boolean ? host . fsWatchesRecursive : host . fsWatches , expectedDirectories , recursiveOrEachDirectoryWatchCount as number , [ expectedDetails , ( { directoryName , fallbackPollingInterval , fallbackOptions } ) => ( { directoryName , fallbackPollingInterval , fallbackOptions } ) ] ) ; } else { recursiveOrExpectedDetails = recursiveOrEachDirectoryWatchCount as boolean ; checkMap ( `fsWatches${ recursiveOrExpectedDetails ? " recursive" : "" }` , recursiveOrExpectedDetails ? host . fsWatchesRecursive : host . fsWatches , expectedDirectories , [ expectedDetails , ( { directoryName , fallbackPollingInterval , fallbackOptions } ) => ( { directoryName , fallbackPollingInterval , fallbackOptions } ) ] ) ; } } export function checkOutputContains ( host , expected ) { const mapExpected = new Set ( expected ) ; const mapSeen = new Set < string > ( ) ; for ( const f of host . getOutput ( ) ) { assert . isFalse ( mapSeen . has ( f ) , `Already found ${ f } in ${ JSON . stringify ( host . getOutput ( ) ) }` ) ; if ( mapExpected . has ( f ) ) { mapExpected . delete ( f ) ; mapSeen . add ( f ) ; } } assert . equal ( mapExpected . size , 0 , `Output has missing ${ JSON . stringify ( arrayFrom ( mapExpected . keys ( ) ) ) } in ${ JSON . stringify ( host . getOutput ( ) ) }` ) ; } export function checkOutputDoesNotContain ( host , expectedToBeAbsent ) { const mapExpectedToBeAbsent = new Set ( expectedToBeAbsent ) ; for ( const f of host . getOutput ( ) ) { assert . isFalse ( mapExpectedToBeAbsent . has ( f ) , `Contains ${ f } in ${ JSON . stringify ( host . getOutput ( ) ) }` ) ; } } class Callbacks { private map : TimeOutCallback [ ] = [ ] ; private nextId = 1 ; getNextId ( ) { return this . nextId ; } register ( cb , args ) { const timeoutId = this . nextId ; this . nextId ++ ; this . map [ timeoutId ] = cb . bind ( undefined , ... args ) ; return timeoutId ; } unregister ( id ) { if ( typeof id === "number" ) { delete this . map [ id ] ; } } count ( ) { let n = 0 ; for ( const _ in this . map ) { n ++ ; } return n ; } invoke ( invokeKey ? ) { if ( invokeKey ) { this . map [ invokeKey ] ( ) ; delete this . map [ invokeKey ] ; return ; } for ( const key in this . map ) { this . map [ key ] ( ) ; delete this . map [ key ] ; } } } type TimeOutCallback = ( ) => any ; export interface TestFileWatcher { cb : FileWatcherCallback ; fileName : string ; pollingInterval : PollingInterval ; } export interface TestFsWatcher { cb : FsWatchCallback ; directoryName : string ; fallbackPollingInterval : PollingInterval ; fallbackOptions : WatchOptions | undefined ; } export interface ReloadWatchInvokeOptions { /** Invokes the directory watcher for the parent instead of the file changed */ invokeDirectoryWatcherInsteadOfFileChanged : boolean ; /** When new file is created, do not invoke watches for it */ ignoreWatchInvokedWithTriggerAsFileCreate : boolean ; /** Invoke the file delete, followed by create instead of file changed */ invokeFileDeleteCreateAsPartInsteadOfChange : boolean ; } export enum Tsc_WatchFile { DynamicPolling = "DynamicPriorityPolling" , SingleFileWatcherPerName = "SingleFileWatcherPerName" } export enum Tsc_WatchDirectory { WatchFile = "RecursiveDirectoryUsingFsWatchFile" , NonRecursiveWatchDirectory = "RecursiveDirectoryUsingNonRecursiveWatchDirectory" , DynamicPolling = "RecursiveDirectoryUsingDynamicPriorityPolling" } const timeIncrements = 1000 ; export interface TestServerHostOptions { useCaseSensitiveFileNames : boolean ; executingFilePath : string ; currentDirectory : string ; fileOrFolderorSymLinkList : readonly FileOrFolderOrSymLink [ ] ; newLine ? : string ; useWindowsStylePaths ? : boolean ; environmentVariables ? : ESMap < string , string > ; } export class TestServerHost implements server . ServerHost , FormatDiagnosticsHost , ModuleResolutionHost { args : string [ ] = [ ] ; private readonly output : string [ ] = [ ] ; private fs : ESMap < Path , FSEntry > = new Map ( ) ; private time = timeIncrements ; getCanonicalFileName : ( s : string ) => string ; private toPath : ( f : string ) => Path ; private timeoutCallbacks = new Callbacks ( ) ; private immediateCallbacks = new Callbacks ( ) ; readonly screenClears : number [ ] = [ ] ; readonly watchedFiles = createMultiMap < Path , TestFileWatcher > ( ) ; readonly fsWatches = createMultiMap < Path , TestFsWatcher > ( ) ; readonly fsWatchesRecursive = createMultiMap < Path , TestFsWatcher > ( ) ; runWithFallbackPolling : boolean ; public readonly useCaseSensitiveFileNames : boolean ; public readonly newLine : string ; public readonly windowsStyleRoot ? : string ; private readonly environmentVariables ? : ESMap < string , string > ; private readonly executingFilePath : string ; private readonly currentDirectory : string ; public require : ( ( initialPath : string , moduleName : string ) => RequireResult ) | undefined ; public defaultWatchFileKind ? : ( ) => WatchFileKind | undefined ; watchFile : HostWatchFile ; watchDirectory : HostWatchDirectory ; constructor ( public withSafeList , fileOrFolderorSymLinkList , { useCaseSensitiveFileNames , executingFilePath , currentDirectory , newLine , windowsStyleRoot , environmentVariables , runWithoutRecursiveWatches , runWithFallbackPolling } = { } ) { this . useCaseSensitiveFileNames = ! ! useCaseSensitiveFileNames ; this . newLine = newLine || "\n" ; this . windowsStyleRoot = windowsStyleRoot ; this . environmentVariables = environmentVariables ; currentDirectory = currentDirectory || "/" ; this . getCanonicalFileName = createGetCanonicalFileName ( ! ! useCaseSensitiveFileNames ) ; this . toPath = s => toPath ( s , currentDirectory , this . getCanonicalFileName ) ; this . executingFilePath = this . getHostSpecificPath ( executingFilePath || getExecutingFilePathFromLibFile ( ) ) ; this . currentDirectory = this . getHostSpecificPath ( currentDirectory ) ; this . runWithFallbackPolling = ! ! runWithFallbackPolling ; const tscWatchFile = this . environmentVariables && this . environmentVariables . get ( "TSC_WATCHFILE" ) ; const tscWatchDirectory = this . environmentVariables && this . environmentVariables . get ( "TSC_WATCHDIRECTORY" ) ; const { watchFile , watchDirectory } = createSystemWatchFunctions ( { pollingWatchFile : tscWatchFile === Tsc_WatchFile . SingleFileWatcherPerName ? createSingleFileWatcherPerName ( this . watchFileWorker . bind ( this ) , this . useCaseSensitiveFileNames ) : this . watchFileWorker . bind ( this ) , getModifiedTime : this . getModifiedTime . bind ( this ) , setTimeout : this . setTimeout . bind ( this ) , clearTimeout : this . clearTimeout . bind ( this ) , fsWatch : this . fsWatch . bind ( this ) , fileExists : this . fileExists . bind ( this ) , useCaseSensitiveFileNames : this . useCaseSensitiveFileNames , getCurrentDirectory : this . getCurrentDirectory . bind ( this ) , fsSupportsRecursiveFsWatch : tscWatchDirectory ? false : ! runWithoutRecursiveWatches , directoryExists : this . directoryExists . bind ( this ) , getAccessibleSortedChildDirectories : path => this . getDirectories ( path ) , realpath : this . realpath . bind ( this ) , tscWatchFile , tscWatchDirectory , defaultWatchFileKind : ( ) => this . defaultWatchFileKind ?. ( ) , } ) ; this . watchFile = watchFile ; this . watchDirectory = watchDirectory ; this . reloadFS ( fileOrFolderorSymLinkList ) ; } writeOutputIsTTY ( ) { return true ; } getNewLine ( ) { return this . newLine ; } toNormalizedAbsolutePath ( s ) { return getNormalizedAbsolutePath ( s , this . currentDirectory ) ; } toFullPath ( s ) { return this . toPath ( this . toNormalizedAbsolutePath ( s ) ) ; } getHostSpecificPath ( s ) { if ( this . windowsStyleRoot && s . startsWith ( directorySeparator ) ) { return this . windowsStyleRoot + s . substring ( 1 ) ; } return s ; } now ( ) { this . time += timeIncrements ; return new Date ( this . time ) ; } private reloadFS ( fileOrFolderOrSymLinkList , options ? ) { Debug . assert ( this . fs . size === 0 ) ; fileOrFolderOrSymLinkList = fileOrFolderOrSymLinkList . concat ( this . withSafeList ? safeList : [ ] ) ; const filesOrFoldersToLoad = ! this . windowsStyleRoot ? fileOrFolderOrSymLinkList : fileOrFolderOrSymLinkList . map < FileOrFolderOrSymLink > ( f => { const result = clone ( f ) ; result . path = this . getHostSpecificPath ( f . path ) ; return result ; } ) ; for ( const fileOrDirectory of filesOrFoldersToLoad ) { const path = this . toFullPath ( fileOrDirectory . path ) ; const currentEntry = this . fs . get ( path ) ; if ( currentEntry ) { if ( isFsFile ( currentEntry ) ) { if ( isFile ( fileOrDirectory ) ) { if ( currentEntry . content !== fileOrDirectory . content ) { this . modifyFile ( fileOrDirectory . path , fileOrDirectory . content , options ) ; } } else { } } else if ( isFsSymLink ( currentEntry ) ) { } else { if ( isFile ( fileOrDirectory ) ) { } else { currentEntry . modifiedTime = this . now ( ) ; this . invokeFsWatches ( currentEntry . fullPath , "change" ) ; } } } else { this . ensureFileOrFolder ( fileOrDirectory , options && options . ignoreWatchInvokedWithTriggerAsFileCreate ) ; } } } modifyFile ( filePath , content , options ? ) { const path = this . toFullPath ( filePath ) ; const currentEntry = this . fs . get ( path ) ; if ( ! currentEntry || ! isFsFile ( currentEntry ) ) { throw new Error ( `file not present: ${ filePath }` ) ; } if ( options && options . invokeFileDeleteCreateAsPartInsteadOfChange ) { this . removeFileOrFolder ( currentEntry , returnFalse ) ; this . ensureFileOrFolder ( { path : filePath , content } ) ; } else { currentEntry . content = content ; currentEntry . modifiedTime = this . now ( ) ; this . fs . get ( getDirectoryPath ( currentEntry . path ) ) ! . modifiedTime = this . now ( ) ; if ( options && options . invokeDirectoryWatcherInsteadOfFileChanged ) { const directoryFullPath = getDirectoryPath ( currentEntry . fullPath ) ; this . invokeFileWatcher ( directoryFullPath , FileWatcherEventKind . Changed , true ) ; this . invokeFsWatchesCallbacks ( directoryFullPath , "rename" , currentEntry . fullPath ) ; this . invokeRecursiveFsWatches ( directoryFullPath , "rename" , currentEntry . fullPath ) ; } else { this . invokeFileAndFsWatches ( currentEntry . fullPath , FileWatcherEventKind . Changed ) ; } } } renameFile ( fileName , newFileName ) { const fullPath = getNormalizedAbsolutePath ( fileName , this . currentDirectory ) ; const path = this . toPath ( fullPath ) ; const file = this . fs . get ( path ) as FsFile ; Debug . assert ( ! ! file ) ; this . removeFileOrFolder ( file , returnFalse , true ) ; const newFullPath = getNormalizedAbsolutePath ( newFileName , this . currentDirectory ) ; const newFile = this . toFsFile ( { path : newFullPath , content : file . content } ) ; const newPath = newFile . path ; const basePath = getDirectoryPath ( path ) ; Debug . assert ( basePath !== path ) ; Debug . assert ( basePath === getDirectoryPath ( newPath ) ) ; const baseFolder = this . fs . get ( basePath ) as FsFolder ; this . addFileOrFolderInFolder ( baseFolder , newFile ) ; } renameFolder ( folderName , newFolderName ) { const fullPath = getNormalizedAbsolutePath ( folderName , this . currentDirectory ) ; const path = this . toPath ( fullPath ) ; const folder = this . fs . get ( path ) as FsFolder ; Debug . assert ( ! ! folder ) ; this . removeFileOrFolder ( folder , returnFalse , true ) ; const newFullPath = getNormalizedAbsolutePath ( newFolderName , this . currentDirectory ) ; const newFolder = this . toFsFolder ( newFullPath ) ; const newPath = newFolder . path ; const basePath = getDirectoryPath ( path ) ; Debug . assert ( basePath !== path ) ; Debug . assert ( basePath === getDirectoryPath ( newPath ) ) ; const baseFolder = this . fs . get ( basePath ) as FsFolder ; this . addFileOrFolderInFolder ( baseFolder , newFolder ) ; this . renameFolderEntries ( folder , newFolder ) ; } private renameFolderEntries ( oldFolder , newFolder ) { for ( const entry of oldFolder . entries ) { this . fs . delete ( entry . path ) ; this . invokeFileAndFsWatches ( entry . fullPath , FileWatcherEventKind . Deleted ) ; entry . fullPath = combinePaths ( newFolder . fullPath , getBaseFileName ( entry . fullPath ) ) ; entry . path = this . toPath ( entry . fullPath ) ; if ( newFolder !== oldFolder ) { newFolder . entries . push ( entry ) ; } this . fs . set ( entry . path , entry ) ; this . invokeFileAndFsWatches ( entry . fullPath , FileWatcherEventKind . Created ) ; if ( isFsFolder ( entry ) ) { this . renameFolderEntries ( entry , entry ) ; } } } ensureFileOrFolder ( fileOrDirectoryOrSymLink , ignoreWatchInvokedWithTriggerAsFileCreate ? , ignoreParentWatch ? ) { if ( isFile ( fileOrDirectoryOrSymLink ) ) { const file = this . toFsFile ( fileOrDirectoryOrSymLink ) ; if ( ! this . fs . get ( file . path ) ) { const baseFolder = this . ensureFolder ( getDirectoryPath ( file . fullPath ) , ignoreParentWatch ) ; this . addFileOrFolderInFolder ( baseFolder , file , ignoreWatchInvokedWithTriggerAsFileCreate ) ; } } else if ( isSymLink ( fileOrDirectoryOrSymLink ) ) { const symLink = this . toFsSymLink ( fileOrDirectoryOrSymLink ) ; Debug . assert ( ! this . fs . get ( symLink . path ) ) ; const baseFolder = this . ensureFolder ( getDirectoryPath ( symLink . fullPath ) , ignoreParentWatch ) ; this . addFileOrFolderInFolder ( baseFolder , symLink , ignoreWatchInvokedWithTriggerAsFileCreate ) ; } else { const fullPath = getNormalizedAbsolutePath ( fileOrDirectoryOrSymLink . path , this . currentDirectory ) ; this . ensureFolder ( getDirectoryPath ( fullPath ) , ignoreParentWatch ) ; this . ensureFolder ( fullPath , ignoreWatchInvokedWithTriggerAsFileCreate ) ; } } private ensureFolder ( fullPath , ignoreWatch ) { const path = this . toPath ( fullPath ) ; let folder = this . fs . get ( path ) as FsFolder ; if ( ! folder ) { folder = this . toFsFolder ( fullPath ) ; const baseFullPath = getDirectoryPath ( fullPath ) ; if ( fullPath !== baseFullPath ) { const baseFolder = this . ensureFolder ( baseFullPath , ignoreWatch ) ; this . addFileOrFolderInFolder ( baseFolder , folder , ignoreWatch ) ; } else { Debug . assert ( this . fs . size === 0 || ! ! this . windowsStyleRoot ) ; this . fs . set ( path , folder ) ; } } Debug . assert ( isFsFolder ( folder ) ) ; return folder ; } private addFileOrFolderInFolder ( folder , fileOrDirectory , ignoreWatch ? ) { if ( ! this . fs . has ( fileOrDirectory . path ) ) { insertSorted ( folder . entries , fileOrDirectory , ( a , b ) => compareStringsCaseSensitive ( getBaseFileName ( a . path ) , getBaseFileName ( b . path ) ) ) ; } folder . modifiedTime = this . now ( ) ; this . fs . set ( fileOrDirectory . path , fileOrDirectory ) ; if ( ignoreWatch ) { return ; } this . invokeFileAndFsWatches ( fileOrDirectory . fullPath , FileWatcherEventKind . Created ) ; this . invokeFileAndFsWatches ( folder . fullPath , FileWatcherEventKind . Changed ) ; } private removeFileOrFolder ( fileOrDirectory , isRemovableLeafFolder , isRenaming = false ) { const basePath = getDirectoryPath ( fileOrDirectory . path ) ; const baseFolder = this . fs . get ( basePath ) as FsFolder ; if ( basePath !== fileOrDirectory . path ) { Debug . assert ( ! ! baseFolder ) ; baseFolder . modifiedTime = this . now ( ) ; filterMutate ( baseFolder . entries , entry => entry !== fileOrDirectory ) ; } this . fs . delete ( fileOrDirectory . path ) ; if ( isFsFolder ( fileOrDirectory ) ) { Debug . assert ( fileOrDirectory . entries . length === 0 || isRenaming ) ; } this . invokeFileAndFsWatches ( fileOrDirectory . fullPath , FileWatcherEventKind . Deleted ) ; this . invokeFileAndFsWatches ( baseFolder . fullPath , FileWatcherEventKind . Changed ) ; if ( basePath !== fileOrDirectory . path && baseFolder . entries . length === 0 && isRemovableLeafFolder ( baseFolder ) ) { this . removeFileOrFolder ( baseFolder , isRemovableLeafFolder ) ; } } deleteFile ( filePath ) { const path = this . toFullPath ( filePath ) ; const currentEntry = this . fs . get ( path ) as FsFile ; Debug . assert ( isFsFile ( currentEntry ) ) ; this . removeFileOrFolder ( currentEntry , returnFalse ) ; } deleteFolder ( folderPath , recursive ? ) { const path = this . toFullPath ( folderPath ) ; const currentEntry = this . fs . get ( path ) as FsFolder ; Debug . assert ( isFsFolder ( currentEntry ) ) ; if ( recursive && currentEntry . entries . length ) { const subEntries = currentEntry . entries . slice ( ) ; subEntries . forEach ( fsEntry => { if ( isFsFolder ( fsEntry ) ) { this . deleteFolder ( fsEntry . fullPath , recursive ) ; } else { this . removeFileOrFolder ( fsEntry , returnFalse ) ; } } ) ; } this . removeFileOrFolder ( currentEntry , returnFalse ) ; } private watchFileWorker ( fileName , cb , pollingInterval ) { return createWatcher ( this . watchedFiles , this . toFullPath ( fileName ) , { fileName , cb , pollingInterval } ) ; } private fsWatch ( fileOrDirectory , _entryKind , cb , recursive , fallbackPollingInterval , fallbackOptions ) { return this . runWithFallbackPolling ? this . watchFile ( fileOrDirectory , createFileWatcherCallback ( cb ) , fallbackPollingInterval , fallbackOptions ) : createWatcher ( recursive ? this . fsWatchesRecursive : this . fsWatches , this . toFullPath ( fileOrDirectory ) , { directoryName : fileOrDirectory , cb , fallbackPollingInterval , fallbackOptions } ) ; } invokeFileWatcher ( fileFullPath , eventKind , useFileNameInCallback ? ) { invokeWatcherCallbacks ( this . watchedFiles . get ( this . toPath ( fileFullPath ) ) , ( { cb , fileName } ) => cb ( useFileNameInCallback ? fileName : fileFullPath , eventKind ) ) ; } private fsWatchCallback ( map , fullPath , eventName , entryFullPath ? ) { invokeWatcherCallbacks ( map . get ( this . toPath ( fullPath ) ) , ( { cb } ) => cb ( eventName , entryFullPath ? this . getRelativePathToDirectory ( fullPath , entryFullPath ) : "" ) ) ; } invokeFsWatchesCallbacks ( fullPath , eventName , entryFullPath ? ) { this . fsWatchCallback ( this . fsWatches , fullPath , eventName , entryFullPath ) ; } invokeFsWatchesRecursiveCallbacks ( fullPath , eventName , entryFullPath ? ) { this . fsWatchCallback ( this . fsWatchesRecursive , fullPath , eventName , entryFullPath ) ; } private getRelativePathToDirectory ( directoryFullPath , fileFullPath ) { return getRelativePathToDirectoryOrUrl ( directoryFullPath , fileFullPath , this . currentDirectory , this . getCanonicalFileName , false ) ; } private invokeRecursiveFsWatches ( fullPath , eventName , entryFullPath ? ) { this . invokeFsWatchesRecursiveCallbacks ( fullPath , eventName , entryFullPath ) ; const basePath = getDirectoryPath ( fullPath ) ; if ( this . getCanonicalFileName ( fullPath ) !== this . getCanonicalFileName ( basePath ) ) { this . invokeRecursiveFsWatches ( basePath , eventName , entryFullPath || fullPath ) ; } } private invokeFsWatches ( fullPath , eventName ) { this . invokeFsWatchesCallbacks ( fullPath , eventName ) ; this . invokeFsWatchesCallbacks ( getDirectoryPath ( fullPath ) , eventName , fullPath ) ; this . invokeRecursiveFsWatches ( fullPath , eventName ) ; } private invokeFileAndFsWatches ( fileOrFolderFullPath , eventKind ) { this . invokeFileWatcher ( fileOrFolderFullPath , eventKind ) ; this . invokeFsWatches ( fileOrFolderFullPath , eventKind === FileWatcherEventKind . Changed ? "change" : "rename" ) ; } private toFsEntry ( path ) { const fullPath = getNormalizedAbsolutePath ( path , this . currentDirectory ) ; return { path : this . toPath ( fullPath ) , fullPath , modifiedTime : this . now ( ) } ; } private toFsFile ( file ) { const fsFile = this . toFsEntry ( file . path ) as FsFile ; fsFile . content = file . content ; fsFile . fileSize = file . fileSize ; return fsFile ; } private toFsSymLink ( symLink ) { const fsSymLink = this . toFsEntry ( symLink . path ) as FsSymLink ; fsSymLink . symLink = getNormalizedAbsolutePath ( symLink . symLink , getDirectoryPath ( fsSymLink . fullPath ) ) ; return fsSymLink ; } private toFsFolder ( path ) { const fsFolder = this . toFsEntry ( path ) as FsFolder ; fsFolder . entries = [ ] as FSEntry [ ] as SortedArray < FSEntry > ; return fsFolder ; } private getRealFsEntry < T extends FSEntry > ( isFsEntry , path , fsEntry = this . fs . get ( path ) ! ) { if ( isFsEntry ( fsEntry ) ) { return fsEntry ; } if ( isFsSymLink ( fsEntry ) ) { return this . getRealFsEntry ( isFsEntry , this . toPath ( fsEntry . symLink ) ) ; } if ( fsEntry ) { return undefined ; } const realpath = this . toPath ( this . realpath ( path ) ) ; if ( path !== realpath ) { return this . getRealFsEntry ( isFsEntry , realpath ) ; } return undefined ; } private isFsFile ( fsEntry ) { return ! ! this . getRealFile ( fsEntry . path , fsEntry ) ; } private getRealFile ( path , fsEntry ? ) { return this . getRealFsEntry ( isFsFile , path , fsEntry ) ; } private isFsFolder ( fsEntry ) { return ! ! this . getRealFolder ( fsEntry . path , fsEntry ) ; } private getRealFolder ( path , fsEntry = this . fs . get ( path ) ) { return this . getRealFsEntry ( isFsFolder , path , fsEntry ) ; } fileExists ( s ) { const path = this . toFullPath ( s ) ; return ! ! this . getRealFile ( path ) ; } getModifiedTime ( s ) { const path = this . toFullPath ( s ) ; const fsEntry = this . fs . get ( path ) ; return ( fsEntry && fsEntry . modifiedTime ) ! ; } setModifiedTime ( s , date ) { const path = this . toFullPath ( s ) ; const fsEntry = this . fs . get ( path ) ; if ( fsEntry ) { fsEntry . modifiedTime = date ; this . invokeFileAndFsWatches ( fsEntry . fullPath , FileWatcherEventKind . Changed ) ; } } readFile ( s ) { const fsEntry = this . getRealFile ( this . toFullPath ( s ) ) ; return fsEntry ? fsEntry . content : undefined ; } getFileSize ( s ) { const path = this . toFullPath ( s ) ; const entry = this . fs . get ( path ) ! ; if ( isFsFile ( entry ) ) { return entry . fileSize ? entry . fileSize : entry . content . length ; } return undefined ! ; } directoryExists ( s ) { const path = this . toFullPath ( s ) ; return ! ! this . getRealFolder ( path ) ; } getDirectories ( s ) { const path = this . toFullPath ( s ) ; const folder = this . getRealFolder ( path ) ; if ( folder ) { return mapDefined ( folder . entries , entry => this . isFsFolder ( entry ) ? getBaseFileName ( entry . fullPath ) : undefined ) ; } Debug . fail ( folder ? "getDirectories called on file" : "getDirectories called on missing folder" ) ; return [ ] ; } readDirectory ( path , extensions ? , exclude ? , include ? , depth ? ) { return matchFiles ( path , extensions , exclude , include , this . useCaseSensitiveFileNames , this . getCurrentDirectory ( ) , depth , ( dir ) => { const directories = [ ] ; const files = [ ] ; const folder = this . getRealFolder ( this . toPath ( dir ) ) ; if ( folder ) { folder . entries . forEach ( ( entry ) => { if ( this . isFsFolder ( entry ) ) { directories . push ( getBaseFileName ( entry . fullPath ) ) ; } else if ( this . isFsFile ( entry ) ) { files . push ( getBaseFileName ( entry . fullPath ) ) ; } else { Debug . fail ( "Unknown entry" ) ; } } ) ; } return { directories , files } ; } , path => this . realpath ( path ) ) ; } createHash ( s ) { return `${ generateDjb2Hash ( s ) }-${ s }` ; } createSHA256Hash ( s ) { return sys . createSHA256Hash ! ( s ) ; } setTimeout ( callback , _time , ... args ) { return this . timeoutCallbacks . register ( callback , args ) ; } getNextTimeoutId ( ) { return this . timeoutCallbacks . getNextId ( ) ; } clearTimeout ( timeoutId ) { this . timeoutCallbacks . unregister ( timeoutId ) ; } clearScreen ( ) { this . screenClears . push ( this . output . length ) ; } checkTimeoutQueueLengthAndRun ( expected ) { this . checkTimeoutQueueLength ( expected ) ; this . runQueuedTimeoutCallbacks ( ) ; } checkTimeoutQueueLength ( expected ) { const callbacksCount = this . timeoutCallbacks . count ( ) ; assert . equal ( callbacksCount , expected , `expected ${ expected } timeout callbacks queued but found ${ callbacksCount }.` ) ; } runQueuedTimeoutCallbacks ( timeoutId ? ) { try { this . timeoutCallbacks . invoke ( timeoutId ) ; } catch ( e ) { if ( e . message === this . exitMessage ) { return ; } throw e ; } } runQueuedImmediateCallbacks ( checkCount ? ) { if ( checkCount !== undefined ) { assert . equal ( this . immediateCallbacks . count ( ) , checkCount ) ; } this . immediateCallbacks . invoke ( ) ; } setImmediate ( callback , _time , ... args ) { return this . immediateCallbacks . register ( callback , args ) ; } clearImmediate ( timeoutId ) { this . immediateCallbacks . unregister ( timeoutId ) ; } createDirectory ( directoryName ) { const folder = this . toFsFolder ( directoryName ) ; const base = getDirectoryPath ( folder . path ) ; const baseFolder = this . fs . get ( base ) as FsFolder ; Debug . assert ( isFsFolder ( baseFolder ) ) ; Debug . assert ( ! this . fs . get ( folder . path ) ) ; this . addFileOrFolderInFolder ( baseFolder , folder ) ; } writeFile ( path , content ) { const file = this . toFsFile ( { path , content } ) ; const base = getDirectoryPath ( file . path ) ; const folder = Debug . checkDefined ( this . getRealFolder ( base ) ) ; if ( folder . path === base ) { if ( ! this . fs . has ( file . path ) ) { this . addFileOrFolderInFolder ( folder , file ) ; } else { this . modifyFile ( path , content ) ; } } else { this . writeFile ( this . realpath ( path ) , content ) ; } } prependFile ( path , content , options ? ) { this . modifyFile ( path , content + this . readFile ( path ) , options ) ; } appendFile ( path , content , options ? ) { this . modifyFile ( path , this . readFile ( path ) + content , options ) ; } write ( message ) { this . output . push ( message ) ; } getOutput ( ) { return this . output ; } clearOutput ( ) { clear ( this . output ) ; this . screenClears . length = 0 ; } serializeOutput ( baseline ) { const output = this . getOutput ( ) ; let start = 0 ; baseline . push ( "Output::" ) ; for ( const screenClear of this . screenClears ) { baselineOutputs ( baseline , output , start , screenClear ) ; start = screenClear ; baseline . push ( ">> Screen clear" ) ; } baselineOutputs ( baseline , output , start ) ; baseline . push ( "" ) ; this . clearOutput ( ) ; } snap ( ) { const result = new Map < Path , FSEntry > ( ) ; this . fs . forEach ( ( value , key ) => { const cloneValue = clone ( value ) ; if ( isFsFolder ( cloneValue ) ) { cloneValue . entries = cloneValue . entries . map ( clone ) as SortedArray < FSEntry > ; } result . set ( key , cloneValue ) ; } ) ; return result ; } writtenFiles ? : ESMap < Path , number > ; diff ( baseline , base = new Map ( ) ) { this . fs . forEach ( newFsEntry => { diffFsEntry ( baseline , base . get ( newFsEntry . path ) , newFsEntry , this . writtenFiles ) ; } ) ; base . forEach ( oldFsEntry => { const newFsEntry = this . fs . get ( oldFsEntry . path ) ; if ( ! newFsEntry ) { diffFsEntry ( baseline , oldFsEntry , newFsEntry , this . writtenFiles ) ; } } ) ; baseline . push ( "" ) ; } serializeWatches ( baseline ) { serializeMultiMap ( baseline , "WatchedFiles" , this . watchedFiles , ( { fileName , pollingInterval } ) => ( { fileName , pollingInterval } ) ) ; baseline . push ( "" ) ; serializeMultiMap ( baseline , "FsWatches" , this . fsWatches , serializeTestFsWatcher ) ; baseline . push ( "" ) ; serializeMultiMap ( baseline , "FsWatchesRecursive" , this . fsWatchesRecursive , serializeTestFsWatcher ) ; baseline . push ( "" ) ; } realpath ( s ) { const fullPath = this . toNormalizedAbsolutePath ( s ) ; const path = this . toPath ( fullPath ) ; if ( getDirectoryPath ( path ) === path ) { return s ; } const dirFullPath = this . realpath ( getDirectoryPath ( fullPath ) ) ; const realFullPath = combinePaths ( dirFullPath , getBaseFileName ( fullPath ) ) ; const fsEntry = this . fs . get ( this . toPath ( realFullPath ) ) ! ; if ( isFsSymLink ( fsEntry ) ) { return this . realpath ( fsEntry . symLink ) ; } return fsEntry ?. fullPath || realFullPath ; } readonly exitMessage = "System Exit" ; exitCode : number | undefined ; readonly resolvePath = ( s ) => s ; readonly getExecutingFilePath = ( ) => this . executingFilePath ; readonly getCurrentDirectory = ( ) => this . currentDirectory ; exit ( exitCode ? ) { this . exitCode = exitCode ; throw new Error ( this . exitMessage ) ; } getEnvironmentVariable ( name ) { return this . environmentVariables && this . environmentVariables . get ( name ) || "" ; } } function diffFsFile ( baseline , fsEntry ) { baseline . push ( `//// [${ fsEntry . fullPath }]\r\n${ fsEntry . content }` , "" ) ; } function diffFsSymLink ( baseline , fsEntry ) { baseline . push ( `//// [${ fsEntry . fullPath }] symlink(${ fsEntry . symLink })` ) ; } function diffFsEntry ( baseline , oldFsEntry , newFsEntry , writtenFiles ) { const file = newFsEntry && newFsEntry . fullPath ; if ( isFsFile ( oldFsEntry ) ) { if ( isFsFile ( newFsEntry ) ) { if ( oldFsEntry . content !== newFsEntry . content ) { diffFsFile ( baseline , newFsEntry ) ; } else if ( oldFsEntry . modifiedTime !== newFsEntry . modifiedTime ) { if ( oldFsEntry . fullPath !== newFsEntry . fullPath ) { baseline . push ( `//// [${ file }] file was renamed from file ${ oldFsEntry . fullPath }` ) ; } else if ( writtenFiles && ! writtenFiles . has ( newFsEntry . path ) ) { baseline . push ( `//// [${ file }] file changed its modified time` ) ; } else { baseline . push ( `//// [${ file }] file written with same contents` ) ; } } } else { baseline . push ( `//// [${ oldFsEntry . fullPath }] deleted` ) ; if ( isFsSymLink ( newFsEntry ) ) { diffFsSymLink ( baseline , newFsEntry ) ; } } } else if ( isFsSymLink ( oldFsEntry ) ) { if ( isFsSymLink ( newFsEntry ) ) { if ( oldFsEntry . symLink !== newFsEntry . symLink ) { diffFsSymLink ( baseline , newFsEntry ) ; } else if ( oldFsEntry . modifiedTime !== newFsEntry . modifiedTime ) { if ( oldFsEntry . fullPath !== newFsEntry . fullPath ) { baseline . push ( `//// [${ file }] symlink was renamed from symlink ${ oldFsEntry . fullPath }` ) ; } else if ( writtenFiles && ! writtenFiles . has ( newFsEntry . path ) ) { baseline . push ( `//// [${ file }] symlink changed its modified time` ) ; } else { baseline . push ( `//// [${ file }] symlink written with same link` ) ; } } } else { baseline . push ( `//// [${ oldFsEntry . fullPath }] deleted symlink` ) ; if ( isFsFile ( newFsEntry ) ) { diffFsFile ( baseline , newFsEntry ) ; } } } else if ( isFsFile ( newFsEntry ) ) { diffFsFile ( baseline , newFsEntry ) ; } else if ( isFsSymLink ( newFsEntry ) ) { diffFsSymLink ( baseline , newFsEntry ) ; } } function serializeTestFsWatcher ( { directoryName , fallbackPollingInterval , fallbackOptions } ) { return { directoryName , fallbackPollingInterval , fallbackOptions : serializeWatchOptions ( fallbackOptions ) } ; } function serializeWatchOptions ( fallbackOptions ) { if ( ! fallbackOptions ) return undefined ; const { watchFile , watchDirectory , fallbackPolling , ... rest } = fallbackOptions ; return { watchFile : watchFile !== undefined ? WatchFileKind [ watchFile ] : undefined , watchDirectory : watchDirectory !== undefined ? WatchDirectoryKind [ watchDirectory ] : undefined , fallbackPolling : fallbackPolling !== undefined ? PollingWatchKind [ fallbackPolling ] : undefined , ... rest } ; } function serializeMultiMap < T , U > ( baseline , caption , multiMap , valueMapper ) { baseline . push ( `${ caption }::` ) ; multiMap . forEach ( ( values , key ) => { baseline . push ( `${ key }:` ) ; for ( const value of values ) { baseline . push ( `  ${ JSON . stringify ( valueMapper ( value ) ) }` ) ; } } ) ; } function baselineOutputs ( baseline , output , start , end = output . length ) { let baselinedOutput ; for ( let i = start ; i < end ; i ++ ) { ( baselinedOutput ||= [ ] ) . push ( output [ i ] . replace ( /Elapsed::\s[0-9]+(?:\.\d+)?ms/g , "Elapsed:: *ms" ) ) ; } if ( baselinedOutput ) baseline . push ( baselinedOutput . join ( "" ) ) ; } export type TestServerHostTrackingWrittenFiles = TestServerHost & { writtenFiles : ESMap < Path , number > ; } ; export function changeToHostTrackingWrittenFiles ( inputHost ) { const host = inputHost as TestServerHostTrackingWrittenFiles ; const originalWriteFile = host . writeFile ; host . writtenFiles = new Map < Path , number > ( ) ; host . writeFile = ( fileName , content ) => { originalWriteFile . call ( host , fileName , content ) ; const path = host . toFullPath ( fileName ) ; host . writtenFiles . set ( path , ( host . writtenFiles . get ( path ) || 0 ) + 1 ) ; } ; return host ; } export const tsbuildProjectsLocation = "/user/username/projects" ; export function getTsBuildProjectFilePath ( project , file ) { return `${ tsbuildProjectsLocation }/${ project }/${ file }` ; } export function getTsBuildProjectFile ( project , file ) { return { path : getTsBuildProjectFilePath ( project , file ) , content : Harness . IO . readFile ( `${ Harness . IO . getWorkspaceRoot ( ) }/tests/projects/${ project }/${ file }` ) ! } ; } }