From 0b963489a755140328f5a45bea1992fbf74c71fb Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Mon, 5 Feb 2018 22:27:20 -0800 Subject: [PATCH] Revert changes to unit tests --- src/harness/assert.ts | 117 ++++ src/harness/virtualFileSystemWithWatch.ts | 778 ++++++++++++++++++++++ 2 files changed, 895 insertions(+) create mode 100644 src/harness/assert.ts create mode 100644 src/harness/virtualFileSystemWithWatch.ts diff --git a/src/harness/assert.ts b/src/harness/assert.ts new file mode 100644 index 00000000000..6e3be2b6e12 --- /dev/null +++ b/src/harness/assert.ts @@ -0,0 +1,117 @@ +/// +declare namespace Chai { + interface ChaiStatic { + util: UtilStatic; + } + + interface UtilStatic { + objDisplay(obj: any): string; + } + + interface AssertStatic { + sameMembers(actual: Iterable, expected: Iterable, message?: string): void; + notSameMembers(actual: Iterable, expected: Iterable, message?: string): void; + includeMembersOnce(actual: Iterable, expected: Iterable, message?: string): void; + includeMembers(actual: Iterable, expected: Iterable, message?: string): void; + notIncludeMembers(actual: Iterable, expected: Iterable, message?: string): void; + } +} + +// patch assert.sameMembers to support other iterables +_chai.use((chai: Chai.ChaiStatic, util: Chai.UtilStatic) => { + function isIdenticalTo(left: Iterable, right: Iterable) { + if (!(left instanceof core.SortedSet) && right instanceof core.SortedSet) { + [left, right] = [right, left]; + } + + const set = asReadonlySet(left); + const seen = set instanceof core.SortedSet ? new core.SortedSet(set.comparer) : new Set(); + const iterator = Array.isArray(right) ? ts.arrayIterator(right) : right[Symbol.iterator](); + for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) { + if (!set.has(value)) return false; + seen.add(value); + } + return set.size === seen.size; + } + + function isSubsetOf(subset: Iterable, superset: Iterable, unique?: boolean) { + const set = asReadonlySet(superset); + const seen = unique ? set instanceof core.SortedSet ? new core.SortedSet(set.comparer) : new Set() : undefined; + const iterator = Array.isArray(subset) ? ts.arrayIterator(subset) : subset[Symbol.iterator](); + for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) { + if (!set.has(value)) return false; + if (seen) { + if (seen.has(value)) return false; + seen.add(value); + } + } + return true; + } + + function overlaps(left: Iterable, right: Iterable) { + if (!(left instanceof core.SortedSet) && right instanceof core.SortedSet) { + [left, right] = [right, left]; + } + + const set = asReadonlySet(left); + const iterator = Array.isArray(right) ? ts.arrayIterator(right) : right[Symbol.iterator](); + for (let { value, done } = iterator.next(); !done; { value, done } = iterator.next()) { + if (set.has(value)) return true; + } + return false; + } + + function asReadonlySet(iterable: Iterable): ReadonlySet { + return iterable instanceof Set ? iterable : + iterable instanceof core.SortedSet ? iterable : + new Set(iterable); + } + + function asReadonlyArray(iterable: Iterable): ReadonlyArray { + return Array.isArray(iterable) ? iterable : Array.from(iterable); + } + + // patch `assert.sameMembers` to support any iterable + chai.assert.sameMembers = (actual: Iterable, expected: Iterable, message?: string) => { + if (!isIdenticalTo(actual, expected)) { + actual = asReadonlyArray(actual); + expected = asReadonlyArray(expected); + assert.fail(actual, expected, message || `expected ${util.objDisplay(actual)} to have the same members as ${util.objDisplay(expected)}`); + } + }; + + // patch `assert.notSameMembers` to support any iterable + chai.assert.notSameMembers = (actual: Iterable, expected: Iterable, message?: string) => { + if (isIdenticalTo(actual, expected)) { + actual = asReadonlyArray(actual); + expected = asReadonlyArray(expected); + assert.fail(actual, expected, message || `expected ${util.objDisplay(actual)} to not have the same members as ${util.objDisplay(expected)}`); + } + }; + + chai.assert.includeMembersOnce = (actual: Iterable, expected: Iterable, message?: string) => { + if (!isSubsetOf(expected, actual, /*unique*/ true)) { + actual = asReadonlyArray(actual); + expected = asReadonlyArray(expected); + assert.fail(actual, expected, message || `expected ${util.objDisplay(actual)} to include the members of ${util.objDisplay(expected)} only once`); + } + }; + + // patch `assert.includeMembers` to support any iterable + chai.assert.includeMembers = (actual: Iterable, expected: Iterable, message?: string) => { + if (!isSubsetOf(expected, actual)) { + actual = asReadonlyArray(actual); + expected = asReadonlyArray(expected); + assert.fail(actual, expected, message || `expected ${util.objDisplay(actual)} to include the members of ${util.objDisplay(expected)}`); + } + }; + + // patch `assert.notIncludeMembers` to support any iterable + chai.assert.notIncludeMembers = (actual: Iterable, expected: Iterable, message?: string) => { + if (overlaps(expected, actual)) { + actual = asReadonlyArray(actual); + expected = asReadonlyArray(expected); + assert.fail(actual, expected, message || `expected ${util.objDisplay(actual)} to not include the members of ${util.objDisplay(expected)}`); + } + }; +}); \ No newline at end of file diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts new file mode 100644 index 00000000000..381917d71c7 --- /dev/null +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -0,0 +1,778 @@ +/// + +namespace ts.TestFSWithWatch { + export const libFile: FileOrFolder = { + path: "/a/lib/lib.d.ts", + content: `/// +interface Boolean {} +interface Function {} +interface IArguments {} +interface Number { toExponential: any; } +interface Object {} +interface RegExp {} +interface String { charAt: any; } +interface Array {}` + }; + + export const safeList = { + path: "/safeList.json", + content: JSON.stringify({ + commander: "commander", + express: "express", + jquery: "jquery", + lodash: "lodash", + moment: "moment", + chroma: "chroma-js" + }) + }; + + function getExecutingFilePathFromLibFile(): string { + return combinePaths(getDirectoryPath(libFile.path), "tsc.js"); + } + + interface TestServerHostCreationParameters { + useCaseSensitiveFileNames?: boolean; + executingFilePath?: string; + currentDirectory?: string; + newLine?: string; + useWindowsStylePaths?: boolean; + } + + export function createWatchedSystem(fileOrFolderList: ReadonlyArray, params?: TestServerHostCreationParameters): TestServerHost { + if (!params) { + params = {}; + } + const host = new TestServerHost(/*withSafelist*/ false, + params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, + params.executingFilePath || getExecutingFilePathFromLibFile(), + params.currentDirectory || "/", + fileOrFolderList, + params.newLine, + params.useWindowsStylePaths); + return host; + } + + export function createServerHost(fileOrFolderList: ReadonlyArray, params?: TestServerHostCreationParameters): TestServerHost { + if (!params) { + params = {}; + } + const host = new TestServerHost(/*withSafelist*/ true, + params.useCaseSensitiveFileNames !== undefined ? params.useCaseSensitiveFileNames : false, + params.executingFilePath || getExecutingFilePathFromLibFile(), + params.currentDirectory || "/", + fileOrFolderList, + params.newLine, + params.useWindowsStylePaths); + return host; + } + + export interface FileOrFolder { + path: string; + content?: string; + fileSize?: number; + symLink?: string; + } + + interface FSEntry { + path: Path; + fullPath: string; + } + + interface File extends FSEntry { + content: string; + fileSize?: number; + } + + interface Folder extends FSEntry { + entries: FSEntry[]; + } + + interface SymLink extends FSEntry { + symLink: string; + } + + function isFolder(s: FSEntry): s is Folder { + return s && isArray((s).entries); + } + + function isFile(s: FSEntry): s is File { + return s && isString((s).content); + } + + function isSymLink(s: FSEntry): s is SymLink { + return s && isString((s).symLink); + } + + function invokeWatcherCallbacks(callbacks: T[], invokeCallback: (cb: T) => void): void { + if (callbacks) { + // The array copy is made to ensure that even if one of the callback removes the callbacks, + // we dont miss any callbacks following it + const cbs = callbacks.slice(); + for (const cb of cbs) { + invokeCallback(cb); + } + } + } + + function getDiffInKeys(map: Map, expectedKeys: ReadonlyArray) { + if (map.size === expectedKeys.length) { + return ""; + } + const notInActual: string[] = []; + const duplicates: string[] = []; + const seen = createMap(); + forEach(expectedKeys, expectedKey => { + if (seen.has(expectedKey)) { + duplicates.push(expectedKey); + return; + } + seen.set(expectedKey, true); + if (!map.has(expectedKey)) { + notInActual.push(expectedKey); + } + }); + const inActualNotExpected: string[] = []; + 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: string, map: Map, expectedKeys: ReadonlyArray) { + assert.equal(map.size, expectedKeys.length, `${caption}: incorrect size of map: Actual keys: ${arrayFrom(map.keys())} Expected: ${expectedKeys}${getDiffInKeys(map, expectedKeys)}`); + } + + function checkMapKeys(caption: string, map: Map, expectedKeys: ReadonlyArray) { + verifyMapSize(caption, map, expectedKeys); + for (const name of expectedKeys) { + assert.isTrue(map.has(name), `${caption} is expected to contain ${name}, actual keys: ${arrayFrom(map.keys())}`); + } + } + + export function checkFileNames(caption: string, actualFileNames: ReadonlyArray, expectedFileNames: string[]) { + assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected:\r\n${expectedFileNames.join("\r\n")}\r\ngot: ${actualFileNames.join("\r\n")}`); + for (const f of expectedFileNames) { + assert.equal(true, contains(actualFileNames, f), `${caption}: expected to find ${f} in ${actualFileNames}`); + } + } + + export function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) { + checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); + } + + export function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[], recursive = false) { + checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); + } + + export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray) { + const mapExpected = arrayToSet(expected); + const mapSeen = createMap(); + for (const f of host.getOutput()) { + assert.isUndefined(mapSeen.get(f), `Already found ${f} in ${JSON.stringify(host.getOutput())}`); + if (mapExpected.has(f)) { + mapExpected.delete(f); + mapSeen.set(f, true); + } + } + assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(arrayFrom(mapExpected.keys()))} in ${JSON.stringify(host.getOutput())}`); + } + + export function checkOutputDoesNotContain(host: TestServerHost, expectedToBeAbsent: string[] | ReadonlyArray) { + const mapExpectedToBeAbsent = arrayToSet(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: any[]) => void, args: any[]) { + const timeoutId = this.nextId; + this.nextId++; + this.map[timeoutId] = cb.bind(/*this*/ undefined, ...args); + return timeoutId; + } + + unregister(id: any) { + if (typeof id === "number") { + delete this.map[id]; + } + } + + count() { + let n = 0; + for (const _ in this.map) { + n++; + } + return n; + } + + invoke(invokeKey?: number) { + if (invokeKey) { + this.map[invokeKey](); + delete this.map[invokeKey]; + return; + } + + // Note: invoking a callback may result in new callbacks been queued, + // so do not clear the entire callback list regardless. Only remove the + // ones we have invoked. + for (const key in this.map) { + this.map[key](); + delete this.map[key]; + } + } + } + + type TimeOutCallback = () => any; + + export interface TestFileWatcher { + cb: FileWatcherCallback; + fileName: string; + } + + export interface TestDirectoryWatcher { + cb: DirectoryWatcherCallback; + directoryName: string; + } + + export interface ReloadWatchInvokeOptions { + invokeDirectoryWatcherInsteadOfFileChanged: boolean; + ignoreWatchInvokedWithTriggerAsFileCreate: boolean; + } + + export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost, ModuleResolutionHost { + args: string[] = []; + + private readonly output: string[] = []; + + private fs: Map = createMap(); + getCanonicalFileName: (s: string) => string; + private toPath: (f: string) => Path; + private timeoutCallbacks = new Callbacks(); + private immediateCallbacks = new Callbacks(); + private screenClears = 0; + + readonly watchedDirectories = createMultiMap(); + readonly watchedDirectoriesRecursive = createMultiMap(); + readonly watchedFiles = createMultiMap(); + private readonly executingFilePath: string; + private readonly currentDirectory: string; + + constructor(public withSafeList: boolean, public useCaseSensitiveFileNames: boolean, executingFilePath: string, currentDirectory: string, fileOrFolderList: ReadonlyArray, public readonly newLine = "\n", public readonly useWindowsStylePath?: boolean) { + this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName); + this.executingFilePath = this.getHostSpecificPath(executingFilePath); + this.currentDirectory = this.getHostSpecificPath(currentDirectory); + this.reloadFS(fileOrFolderList); + } + + getNewLine() { + return this.newLine; + } + + toNormalizedAbsolutePath(s: string) { + return getNormalizedAbsolutePath(s, this.currentDirectory); + } + + toFullPath(s: string) { + return this.toPath(this.toNormalizedAbsolutePath(s)); + } + + getHostSpecificPath(s: string) { + if (this.useWindowsStylePath && s.startsWith(directorySeparator)) { + return "c:/" + s.substring(1); + } + return s; + } + + reloadFS(fileOrFolderList: ReadonlyArray, options?: Partial) { + const mapNewLeaves = createMap(); + const isNewFs = this.fs.size === 0; + fileOrFolderList = fileOrFolderList.concat(this.withSafeList ? safeList : []); + const filesOrFoldersToLoad: ReadonlyArray = !this.useWindowsStylePath ? fileOrFolderList : + fileOrFolderList.map(f => { + const result = clone(f); + result.path = this.getHostSpecificPath(f.path); + return result; + }); + for (const fileOrDirectory of filesOrFoldersToLoad) { + const path = this.toFullPath(fileOrDirectory.path); + mapNewLeaves.set(path, true); + // If its a change + const currentEntry = this.fs.get(path); + if (currentEntry) { + if (isFile(currentEntry)) { + if (isString(fileOrDirectory.content)) { + // Update file + if (currentEntry.content !== fileOrDirectory.content) { + currentEntry.content = fileOrDirectory.content; + if (options && options.invokeDirectoryWatcherInsteadOfFileChanged) { + this.invokeDirectoryWatcher(getDirectoryPath(currentEntry.fullPath), currentEntry.fullPath); + } + else { + this.invokeFileWatcher(currentEntry.fullPath, FileWatcherEventKind.Changed); + } + } + } + else { + // TODO: Changing from file => folder/Symlink + } + } + else if (isSymLink(currentEntry)) { + // TODO: update symlinks + } + else { + // Folder + if (isString(fileOrDirectory.content)) { + // TODO: Changing from folder => file + } + else { + // Folder update: Nothing to do. + } + } + } + else { + this.ensureFileOrFolder(fileOrDirectory, options && options.ignoreWatchInvokedWithTriggerAsFileCreate); + } + } + + if (!isNewFs) { + this.fs.forEach((fileOrDirectory, path) => { + // If this entry is not from the new file or folder + if (!mapNewLeaves.get(path)) { + // Leaf entries that arent in new list => remove these + if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory) || isFolder(fileOrDirectory) && fileOrDirectory.entries.length === 0) { + this.removeFileOrFolder(fileOrDirectory, folder => !mapNewLeaves.get(folder.path)); + } + } + }); + } + } + + renameFolder(folderName: string, newFolderName: string) { + const fullPath = getNormalizedAbsolutePath(folderName, this.currentDirectory); + const path = this.toPath(fullPath); + const folder = this.fs.get(path) as Folder; + Debug.assert(!!folder); + + // Only remove the folder + this.removeFileOrFolder(folder, returnFalse, /*isRenaming*/ true); + + // Add updated folder with new folder name + const newFullPath = getNormalizedAbsolutePath(newFolderName, this.currentDirectory); + const newFolder = this.toFolder(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 Folder; + this.addFileOrFolderInFolder(baseFolder, newFolder); + + // Invoke watches for files in the folder as deleted (from old path) + for (const entry of folder.entries) { + Debug.assert(isFile(entry)); + this.fs.delete(entry.path); + this.invokeFileWatcher(entry.fullPath, FileWatcherEventKind.Deleted); + + entry.fullPath = combinePaths(newFullPath, getBaseFileName(entry.fullPath)); + entry.path = this.toPath(entry.fullPath); + newFolder.entries.push(entry); + this.fs.set(entry.path, entry); + this.invokeFileWatcher(entry.fullPath, FileWatcherEventKind.Created); + } + } + + ensureFileOrFolder(fileOrDirectory: FileOrFolder, ignoreWatchInvokedWithTriggerAsFileCreate?: boolean) { + if (isString(fileOrDirectory.content)) { + const file = this.toFile(fileOrDirectory); + Debug.assert(!this.fs.get(file.path)); + const baseFolder = this.ensureFolder(getDirectoryPath(file.fullPath)); + this.addFileOrFolderInFolder(baseFolder, file, ignoreWatchInvokedWithTriggerAsFileCreate); + } + else if (isString(fileOrDirectory.symLink)) { + const symLink = this.toSymLink(fileOrDirectory); + Debug.assert(!this.fs.get(symLink.path)); + const baseFolder = this.ensureFolder(getDirectoryPath(symLink.fullPath)); + this.addFileOrFolderInFolder(baseFolder, symLink, ignoreWatchInvokedWithTriggerAsFileCreate); + } + else { + const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); + this.ensureFolder(fullPath); + } + } + + private ensureFolder(fullPath: string): Folder { + const path = this.toPath(fullPath); + let folder = this.fs.get(path) as Folder; + if (!folder) { + folder = this.toFolder(fullPath); + const baseFullPath = getDirectoryPath(fullPath); + if (fullPath !== baseFullPath) { + // Add folder in the base folder + const baseFolder = this.ensureFolder(baseFullPath); + this.addFileOrFolderInFolder(baseFolder, folder); + } + else { + // root folder + Debug.assert(this.fs.size === 0); + this.fs.set(path, folder); + } + } + Debug.assert(isFolder(folder)); + return folder; + } + + private addFileOrFolderInFolder(folder: Folder, fileOrDirectory: File | Folder | SymLink, ignoreWatch?: boolean) { + folder.entries.push(fileOrDirectory); + this.fs.set(fileOrDirectory.path, fileOrDirectory); + + if (ignoreWatch) { + return; + } + if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory)) { + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); + } + this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath); + } + + private removeFileOrFolder(fileOrDirectory: File | Folder | SymLink, isRemovableLeafFolder: (folder: Folder) => boolean, isRenaming?: boolean) { + const basePath = getDirectoryPath(fileOrDirectory.path); + const baseFolder = this.fs.get(basePath) as Folder; + if (basePath !== fileOrDirectory.path) { + Debug.assert(!!baseFolder); + filterMutate(baseFolder.entries, entry => entry !== fileOrDirectory); + } + this.fs.delete(fileOrDirectory.path); + + if (isFile(fileOrDirectory) || isSymLink(fileOrDirectory)) { + this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); + } + else { + Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming); + const relativePath = this.getRelativePathToDirectory(fileOrDirectory.fullPath, fileOrDirectory.fullPath); + // Invoke directory and recursive directory watcher for the folder + // Here we arent invoking recursive directory watchers for the base folders + // since that is something we would want to do for both file as well as folder we are deleting + invokeWatcherCallbacks(this.watchedDirectories.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath)); + invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(fileOrDirectory.path), cb => this.directoryCallback(cb, relativePath)); + } + + if (basePath !== fileOrDirectory.path) { + if (baseFolder.entries.length === 0 && isRemovableLeafFolder(baseFolder)) { + this.removeFileOrFolder(baseFolder, isRemovableLeafFolder); + } + else { + this.invokeRecursiveDirectoryWatcher(baseFolder.fullPath, fileOrDirectory.fullPath); + } + } + } + + private invokeFileWatcher(fileFullPath: string, eventKind: FileWatcherEventKind) { + const callbacks = this.watchedFiles.get(this.toPath(fileFullPath)); + invokeWatcherCallbacks(callbacks, ({ cb }) => cb(fileFullPath, eventKind)); + } + + private getRelativePathToDirectory(directoryFullPath: string, fileFullPath: string) { + return getRelativePathToDirectoryOrUrl(directoryFullPath, fileFullPath, this.currentDirectory, this.getCanonicalFileName, /*isAbsolutePathAnUrl*/ false); + } + + /** + * This will call the directory watcher for the folderFullPath and recursive directory watchers for this and base folders + */ + private invokeDirectoryWatcher(folderFullPath: string, fileName: string) { + const relativePath = this.getRelativePathToDirectory(folderFullPath, fileName); + invokeWatcherCallbacks(this.watchedDirectories.get(this.toPath(folderFullPath)), cb => this.directoryCallback(cb, relativePath)); + this.invokeRecursiveDirectoryWatcher(folderFullPath, fileName); + } + + private directoryCallback({ cb, directoryName }: TestDirectoryWatcher, relativePath: string) { + cb(combinePaths(directoryName, relativePath)); + } + + /** + * This will call the recursive directory watcher for this directory as well as all the base directories + */ + private invokeRecursiveDirectoryWatcher(fullPath: string, fileName: string) { + const relativePath = this.getRelativePathToDirectory(fullPath, fileName); + invokeWatcherCallbacks(this.watchedDirectoriesRecursive.get(this.toPath(fullPath)), cb => this.directoryCallback(cb, relativePath)); + const basePath = getDirectoryPath(fullPath); + if (this.getCanonicalFileName(fullPath) !== this.getCanonicalFileName(basePath)) { + this.invokeRecursiveDirectoryWatcher(basePath, fileName); + } + } + + private toFile(fileOrDirectory: FileOrFolder): File { + const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); + return { + path: this.toPath(fullPath), + content: fileOrDirectory.content, + fullPath, + fileSize: fileOrDirectory.fileSize + }; + } + + private toSymLink(fileOrDirectory: FileOrFolder): SymLink { + const fullPath = getNormalizedAbsolutePath(fileOrDirectory.path, this.currentDirectory); + return { + path: this.toPath(fullPath), + fullPath, + symLink: getNormalizedAbsolutePath(fileOrDirectory.symLink, getDirectoryPath(fullPath)) + }; + } + + private toFolder(path: string): Folder { + const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); + return { + path: this.toPath(fullPath), + entries: [], + fullPath + }; + } + + private getRealFsEntry(isFsEntry: (fsEntry: FSEntry) => fsEntry is T, path: Path, fsEntry = this.fs.get(path)): T | undefined { + if (isFsEntry(fsEntry)) { + return fsEntry; + } + + if (isSymLink(fsEntry)) { + return this.getRealFsEntry(isFsEntry, this.toPath(fsEntry.symLink)); + } + + if (fsEntry) { + // This fs entry is something else + return undefined; + } + + const realpath = this.realpath(path); + if (path !== realpath) { + return this.getRealFsEntry(isFsEntry, realpath as Path); + } + + return undefined; + } + + private isFile(fsEntry: FSEntry) { + return !!this.getRealFile(fsEntry.path, fsEntry); + } + + private getRealFile(path: Path, fsEntry?: FSEntry): File | undefined { + return this.getRealFsEntry(isFile, path, fsEntry); + } + + private isFolder(fsEntry: FSEntry) { + return !!this.getRealFolder(fsEntry.path, fsEntry); + } + + private getRealFolder(path: Path, fsEntry = this.fs.get(path)): Folder | undefined { + return this.getRealFsEntry(isFolder, path, fsEntry); + } + + fileExists(s: string) { + const path = this.toFullPath(s); + return !!this.getRealFile(path); + } + + readFile(s: string): string { + const fsEntry = this.getRealFile(this.toFullPath(s)); + return fsEntry ? fsEntry.content : undefined; + } + + getFileSize(s: string) { + const path = this.toFullPath(s); + const entry = this.fs.get(path); + if (isFile(entry)) { + return entry.fileSize ? entry.fileSize : entry.content.length; + } + return undefined; + } + + directoryExists(s: string) { + const path = this.toFullPath(s); + return !!this.getRealFolder(path); + } + + getDirectories(s: string): string[] { + const path = this.toFullPath(s); + const folder = this.getRealFolder(path); + if (folder) { + return mapDefined(folder.entries, entry => this.isFolder(entry) ? getBaseFileName(entry.fullPath) : undefined); + } + Debug.fail(folder ? "getDirectories called on file" : "getDirectories called on missing folder"); + return []; + } + + readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { + return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => { + const directories: string[] = []; + const files: string[] = []; + const folder = this.getRealFolder(this.toPath(dir)); + if (folder) { + folder.entries.forEach((entry) => { + if (this.isFolder(entry)) { + directories.push(getBaseFileName(entry.fullPath)); + } + else if (this.isFile(entry)) { + files.push(getBaseFileName(entry.fullPath)); + } + else { + Debug.fail("Unknown entry"); + } + }); + } + return { directories, files }; + }); + } + + watchDirectory(directoryName: string, cb: DirectoryWatcherCallback, recursive: boolean): FileWatcher { + const path = this.toFullPath(directoryName); + const map = recursive ? this.watchedDirectoriesRecursive : this.watchedDirectories; + const callback: TestDirectoryWatcher = { + cb, + directoryName + }; + map.add(path, callback); + return { + close: () => map.remove(path, callback) + }; + } + + createHash(s: string): string { + return Harness.mockHash(s); + } + + watchFile(fileName: string, cb: FileWatcherCallback) { + const path = this.toFullPath(fileName); + const callback: TestFileWatcher = { fileName, cb }; + this.watchedFiles.add(path, callback); + return { close: () => this.watchedFiles.remove(path, callback) }; + } + + // TOOD: record and invoke callbacks to simulate timer events + setTimeout(callback: TimeOutCallback, _time: number, ...args: any[]) { + return this.timeoutCallbacks.register(callback, args); + } + + getNextTimeoutId() { + return this.timeoutCallbacks.getNextId(); + } + + clearTimeout(timeoutId: any): void { + this.timeoutCallbacks.unregister(timeoutId); + } + + clearScreen(): void { + this.screenClears += 1; + } + + checkTimeoutQueueLengthAndRun(expected: number) { + this.checkTimeoutQueueLength(expected); + this.runQueuedTimeoutCallbacks(); + } + + checkTimeoutQueueLength(expected: number) { + const callbacksCount = this.timeoutCallbacks.count(); + assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`); + } + + runQueuedTimeoutCallbacks(timeoutId?: number) { + try { + this.timeoutCallbacks.invoke(timeoutId); + } + catch (e) { + if (e.message === this.existMessage) { + return; + } + throw e; + } + } + + runQueuedImmediateCallbacks() { + this.immediateCallbacks.invoke(); + } + + setImmediate(callback: TimeOutCallback, _time: number, ...args: any[]) { + return this.immediateCallbacks.register(callback, args); + } + + clearImmediate(timeoutId: any): void { + this.immediateCallbacks.unregister(timeoutId); + } + + checkScreenClears(expected: number): void { + assert.equal(this.screenClears, expected); + } + + createDirectory(directoryName: string): void { + const folder = this.toFolder(directoryName); + + // base folder has to be present + const base = getDirectoryPath(folder.path); + const baseFolder = this.fs.get(base) as Folder; + Debug.assert(isFolder(baseFolder)); + + Debug.assert(!this.fs.get(folder.path)); + this.addFileOrFolderInFolder(baseFolder, folder); + } + + writeFile(path: string, content: string): void { + const file = this.toFile({ path, content }); + + // base folder has to be present + const base = getDirectoryPath(file.path); + const folder = this.fs.get(base) as Folder; + Debug.assert(isFolder(folder)); + + this.addFileOrFolderInFolder(folder, file); + } + + write(message: string) { + this.output.push(message); + } + + getOutput(): ReadonlyArray { + return this.output; + } + + clearOutput() { + clear(this.output); + } + + realpath(s: string): string { + const fullPath = this.toNormalizedAbsolutePath(s); + const path = this.toPath(fullPath); + if (getDirectoryPath(path) === path) { + // Root + return s; + } + const dirFullPath = this.realpath(getDirectoryPath(fullPath)); + const realFullPath = combinePaths(dirFullPath, getBaseFileName(fullPath)); + const fsEntry = this.fs.get(this.toPath(realFullPath)); + if (isSymLink(fsEntry)) { + return this.realpath(fsEntry.symLink); + } + + return realFullPath; + } + + readonly existMessage = "System Exit"; + exitCode: number; + readonly resolvePath = (s: string) => s; + readonly getExecutingFilePath = () => this.executingFilePath; + readonly getCurrentDirectory = () => this.currentDirectory; + exit(exitCode?: number) { + this.exitCode = exitCode; + throw new Error(this.existMessage); + } + readonly getEnvironmentVariable = notImplemented; + } +}