From 4fbc74ec2615be13cc541fbbcf91fd8fdfdad855 Mon Sep 17 00:00:00 2001 From: Ron Buckton Date: Sun, 12 Nov 2017 16:23:08 -0800 Subject: [PATCH] Partial deprecation of non-vfs TestServerHost --- src/harness/core.ts | 72 ++++ src/harness/mocks.ts | 339 +++++++++++++++ src/harness/tsconfig.json | 5 +- .../unittests/reuseProgramStructure.ts | 137 +++--- src/harness/unittests/tscWatchMode.ts | 10 +- src/harness/vfs.ts | 396 +++++++++++++++--- src/harness/virtualFileSystemWithWatch.ts | 8 +- .../conformance/types/never/neverInference.ts | 2 +- 8 files changed, 811 insertions(+), 158 deletions(-) create mode 100644 src/harness/mocks.ts diff --git a/src/harness/core.ts b/src/harness/core.ts index c4f3a787816..10e5bb7a11b 100644 --- a/src/harness/core.ts +++ b/src/harness/core.ts @@ -479,4 +479,76 @@ namespace core { splitLinesWorker(text, lineStarts, /*lines*/ undefined, /*removeEmptyElements*/ false); return lineStarts; } + + // + // Cryptography + // + + const H = new Uint32Array(5); + const W = new Uint8Array(80); + const B = new Uint8Array(64); + const BLOCK_SIZE = 64; + + export function sha1(message: string): string { + let buffer = B; + const textSize = message.length; + const messageSize = textSize * 2; + const finalBlockSize = messageSize % BLOCK_SIZE; + const padSize = (finalBlockSize < BLOCK_SIZE - 8 - 1 ? BLOCK_SIZE : BLOCK_SIZE * 2) - finalBlockSize; + const byteLength = messageSize + padSize; + if (byteLength > BLOCK_SIZE) { + buffer = new Uint8Array(byteLength); + } + + const bufferView = new DataView(buffer.buffer); + for (let i = 0; i < textSize; ++i) { + bufferView.setUint16(i * 2, message.charCodeAt(i)); + } + + buffer[messageSize] = 0x80; + bufferView.setUint32(byteLength - 4, messageSize * 8); + H[0] = 0x67452301, H[1] = 0xefcdab89, H[2] = 0x98badcfe, H[3] = 0x10325476, H[4] = 0xc3d2e1f0; + for (let offset = 0; offset < byteLength; offset += BLOCK_SIZE) { + let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4]; + for (let i = 0; i < 80; ++i) { + if (i < 16) { + const x = offset + i * 4; + W[i] = buffer[x] << 24 | buffer[x + 1] << 16 | buffer[x + 2] << 8 | buffer[x + 3]; + } + else { + const x = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16]; + W[i] = (x << 1 | x >>> 31) >>> 0; + } + + let t = (a << 5 | a >>> 27) >>> 0 + e + W[i]; + if (i < 20) { + t += ((b & c) | (~b & d)) + 0x5A827999; + } + else if (i < 40) { + t += (b ^ c ^ d) + 0x6ED9EBA1; + } + else if (i < 60) { + t += ((b & c) | (b & d) | (c & d)) + 0x8F1BBCDC; + } + else { + t += (b ^ c ^ d) + 0xCA62C1D6; + } + + e = d, d = c, c = (b << 30 | b >>> 2) >>> 0, b = a, a = t; + } + + H[0] += a, H[1] += b, H[2] += c, H[3] += d, H[4] += e; + } + + for (let i = 0; i < 5; ++i) { + bufferView.setUint32(i * 4, H[i]); + } + + let result = ""; + for (let i = 0; i < 20; ++i) { + result += (buffer[i] < 16 ? "0" : "") + buffer[i].toString(16); + } + + return result; + } } \ No newline at end of file diff --git a/src/harness/mocks.ts b/src/harness/mocks.ts new file mode 100644 index 00000000000..051fdc094a4 --- /dev/null +++ b/src/harness/mocks.ts @@ -0,0 +1,339 @@ +/// +/// + +// NOTE: The contents of this file are all exported from the namespace 'mocks'. This is to +// support the eventual conversion of harness into a modular system. + +namespace mocks { + const MAX_INT32 = 2 ** 31 - 1; + + export interface Immediate { + readonly kind: "immediate"; + readonly callback: (...args: any[]) => void; + readonly args: ReadonlyArray; + } + + export interface Timeout { + readonly kind: "timeout"; + readonly callback: (...args: any[]) => void; + readonly args: ReadonlyArray; + readonly due: number; + } + + export interface Interval { + readonly kind: "interval"; + readonly callback: (...args: any[]) => void; + readonly args: ReadonlyArray; + readonly due: number; + readonly interval: number; + } + + export type Timer = Immediate | Timeout | Interval; + + interface InternalInterval extends Interval { + due: number; + } + + /** + * Programmatic control over timers. + */ + export class Timers { + public static readonly MAX_DEPTH = MAX_INT32; + private _immediates = new Set(); + private _timeouts = new Set(); + private _intervals = new Set(); + private _time: number; + + constructor(startTime = Date.now()) { + this._time = startTime; + + // bind each timer method so that it can be detached from this instance. + this.setImmediate = this.setImmediate.bind(this); + this.clearImmedate = this.clearImmedate.bind(this); + this.setTimeout = this.setTimeout.bind(this); + this.clearTimeout = this.clearImmedate.bind(this); + this.setInterval = this.setInterval.bind(this); + this.clearInterval = this.clearInterval.bind(this); + } + + /** + * Get the current time. + */ + public get time(): number { + return this._time; + } + + public getPending(kind: "immediate", ms?: number): Immediate[]; + public getPending(kind: "timeout", ms?: number): Timeout[]; + public getPending(kind: "interval", ms?: number): Interval[]; + public getPending(kind?: Timer["kind"], ms?: number): Timer[]; + public getPending(kind?: Timer["kind"], ms = 0) { + if (ms < 0) throw new TypeError("Argument 'ms' out of range."); + const pending: Timer[] = []; + if (!kind || kind === "immediate") this.appendImmediates(pending); + if (!kind || kind === "timeout") this.appendDueTimeouts(pending, this._time + ms); + if (!kind || kind === "interval") this.appendDueIntervals(pending, this._time + ms, /*expand*/ false); + return core.stableSort(pending, compareTimers); + } + + /** + * Advance the current time and trigger callbacks, returning the number of callbacks triggered. + * @param ms The number of milliseconds to advance. + * @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing. + * - Use `0` (default) to disable processing of nested `setImmediate` calls. + * - Use `Timer.NO_MAX_DEPTH` to continue processing all nested `setImmediate` calls. + */ + public advance(ms: number, maxDepth = 0): number { + if (ms < 0) throw new TypeError("Argument 'ms' out of range."); + if (maxDepth < 0) throw new TypeError("Argument 'maxDepth' out of range."); + this._time += ms; + return this.executePending(maxDepth); + } + + /** + * Execute any pending timers, returning the number of timers triggered. + * @param maxDepth The maximum depth for nested `setImmediate` calls to continue processing. + * - Use `0` (default) to disable processing of nested `setImmediate` calls. + * - Use `Timer.NO_MAX_DEPTH` to continue processing all nested `setImmediate` calls. + */ + public executePending(maxDepth = 0): number { + if (maxDepth < 0) throw new TypeError("Argument 'maxDepth' out of range."); + const pending: Timer[] = []; + this.appendImmediates(pending); + this.appendDueTimeouts(pending, this._time); + this.appendDueIntervals(pending, this._time, /*expand*/ true); + let count = this.execute(pending); + for (let depth = 0; depth < maxDepth && this._immediates.size > 0; depth++) { + pending.length = 0; + this.appendImmediates(pending); + count += this.execute(pending); + } + return count; + } + + public setImmediate(callback: (...args: any[]) => void, ...args: any[]): any { + const timer: Immediate = { kind: "immediate", callback, args }; + this._immediates.add(timer); + return timer; + } + + public clearImmedate(timerId: any): void { + this._immediates.delete(timerId); + } + + public setTimeout(callback: (...args: any[]) => void, timeout: number, ...args: any[]): any { + if (timeout < 0) timeout = 0; + const due = this._time + timeout; + const timer: Timeout = { kind: "timeout", callback, args, due }; + this._timeouts.add(timer); + return timer; + } + + public clearTimeout(timerId: any): void { + this._timeouts.delete(timerId); + } + + public setInterval(callback: (...args: any[]) => void, interval: number, ...args: any[]): any { + if (interval < 0) interval = 0; + const due = this._time + interval; + const timer: Interval = { kind: "interval", callback, args, due, interval }; + this._intervals.add(timer); + return timer; + } + + public clearInterval(timerId: any): void { + this._intervals.delete(timerId); + } + + private appendImmediates(pending: Timer[]) { + this._immediates.forEach(timer => { + pending.push(timer); + }); + } + + private appendDueTimeouts(timers: Timer[], dueTime: number) { + this._timeouts.forEach(timer => { + if (timer.due <= dueTime) { + timers.push(timer); + } + }); + } + + private appendDueIntervals(timers: Timer[], dueTime: number, expand: boolean) { + this._intervals.forEach(timer => { + while (timer.due <= dueTime) { + timers.push(timer); + if (!expand) break; + timer.due += timer.interval; + } + }); + } + + private execute(timers: Timer[]) { + for (const timer of core.stableSort(timers, compareTimers)) { + switch (timer.kind) { + case "immediate": this._immediates.delete(timer); break; + case "timeout": this._timeouts.delete(timer); break; + } + const { callback, args } = timer; + callback(...args); + } + return timers.length; + } + } + + function compareTimers(a: Immediate | Timeout, b: Immediate | Timeout) { + return (a.kind === "immediate" ? -1 : a.due) - (b.kind === "immediate" ? -1 : b.due); + } + + export class MockServerHost implements ts.server.ServerHost, ts.FormatDiagnosticsHost { + public readonly exitMessage = "System Exit"; + public readonly timers = new Timers(); + public readonly vfs: vfs.VirtualFileSystem; + public exitCode: number; + + private readonly _output: string[] = []; + private readonly _executingFilePath: string; + private readonly _getCanonicalFileName: (file: string) => string; + + constructor(vfs: vfs.VirtualFileSystem, executingFilePath = "/.ts/tsc.js", newLine = "\n") { + this.vfs = vfs; + this.useCaseSensitiveFileNames = vfs.useCaseSensitiveFileNames; + this.newLine = newLine; + this._executingFilePath = executingFilePath; + this._getCanonicalFileName = ts.createGetCanonicalFileName(this.useCaseSensitiveFileNames); + } + + // #region DirectoryStructureHost members + public readonly newLine: string; + public readonly useCaseSensitiveFileNames: boolean; + + public write(message: string) { + this._output.push(message); + } + + public readFile(path: string) { + return this.vfs.readFile(path); + } + + public writeFile(path: string, data: string): void { + this.vfs.writeFile(path, data); + } + + public fileExists(path: string) { + return this.vfs.fileExists(path); + } + + public directoryExists(path: string) { + return this.vfs.directoryExists(path); + } + + public createDirectory(path: string): void { + this.vfs.addDirectory(path); + } + + public getCurrentDirectory() { + return this.vfs.currentDirectory; + } + + public getDirectories(path: string) { + return this.vfs.getDirectoryNames(path); + } + + public readDirectory(path: string, extensions?: ReadonlyArray, exclude?: ReadonlyArray, include?: ReadonlyArray, depth?: number): string[] { + return ts.matchFiles(path, extensions, exclude, include, this.useCaseSensitiveFileNames, this.vfs.currentDirectory, depth, path => { + return this.vfs.getAccessibleFileSystemEntries(path); + }); + } + + public exit(exitCode?: number) { + this.exitCode = exitCode; + throw new Error("System exit"); + } + // #endregion DirectoryStructureHost members + + // #region System members + public readonly args: string[] = []; + + public getFileSize(path: string) { + const stats = this.vfs.getStats(path); + return stats && stats.isFile() ? stats.size : 0; + } + + public watchFile(path: string, cb: ts.FileWatcherCallback) { + return this.vfs.watchFile(path, (path, change) => { + cb(path, change === "added" ? ts.FileWatcherEventKind.Created : + change === "removed" ? ts.FileWatcherEventKind.Deleted : + ts.FileWatcherEventKind.Changed); + }); + } + + public watchDirectory(path: string, cb: ts.DirectoryWatcherCallback, recursive: boolean): ts.FileWatcher { + return this.vfs.watchDirectory(path, cb, recursive); + } + + public resolvePath(path: string) { + return vpath.resolve(this.vfs.currentDirectory, path); + } + + public getExecutingFilePath() { + return this._executingFilePath; + } + + public getModifiedTime(path: string) { + const stats = this.vfs.getStats(path); + return stats && stats.mtime; + } + + public createHash(data: string): string { + return core.sha1(data); + } + + public realpath(path: string) { + const entry = this.vfs.getRealEntry(this.vfs.getEntry(path)); + return entry && entry.path; + } + + public getEnvironmentVariable(_name: string): string | undefined { + return undefined; + } + + // TOOD: record and invoke callbacks to simulate timer events + public setTimeout(callback: (...args: any[]) => void, timeout: number, ...args: any[]) { + return this.timers.setTimeout(callback, timeout, ...args); + } + + public clearTimeout(timeoutId: any): void { + this.timers.clearTimeout(timeoutId); + } + // #endregion System members + + // #region FormatDiagnosticsHost members + public getNewLine() { + return this.newLine; + } + + public getCanonicalFileName(fileName: string) { + return this._getCanonicalFileName(fileName); + } + // #endregion FormatDiagnosticsHost members + + // #region ServerHost members + public setImmediate(callback: (...args: any[]) => void, ...args: any[]): any { + return this.timers.setImmediate(callback, args); + } + + public clearImmediate(timeoutId: any): void { + this.timers.clearImmedate(timeoutId); + } + // #endregion ServerHost members + + public getOutput(): ReadonlyArray { + return this._output; + } + + public clearOutput() { + this._output.length = 0; + } + } +} \ No newline at end of file diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index ee0032e1218..11e03438090 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -73,7 +73,7 @@ "../services/codefixes/disableJsDiagnostics.ts", "harness.ts", - + "core.ts", "utils.ts", "events.ts", @@ -81,7 +81,8 @@ "vpath.ts", "vfs.ts", "compiler.ts", - + "mocks.ts", + "virtualFileSystemWithWatch.ts", "sourceMapRecorder.ts", "harnessLanguageService.ts", diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index fdccc8a7795..7e61f188b10 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -871,9 +871,6 @@ namespace ts { }); }); - import TestSystem = ts.TestFSWithWatch.TestServerHost; - type FileOrFolder = ts.TestFSWithWatch.FileOrFolder; - import createTestSystem = ts.TestFSWithWatch.createWatchedSystem; import libFile = ts.TestFSWithWatch.libFile; describe("isProgramUptoDate should return true when there is no change in compiler options and", () => { @@ -897,7 +894,7 @@ namespace ts { return JSON.parse(JSON.stringify(filesOrOptions)); } - function createWatchingSystemHost(host: TestSystem) { + function createWatchingSystemHost(host: ts.System) { return ts.createWatchingSystemHost(/*pretty*/ undefined, host); } @@ -917,48 +914,29 @@ namespace ts { verifyProgramIsUptoDate(program, fileNames, options); } - function verifyProgram(files: FileOrFolder[], rootFiles: string[], options: CompilerOptions, configFile: string) { - const watchingSystemHost = createWatchingSystemHost(createTestSystem(files)); + function verifyProgram(vfs: vfs.VirtualFileSystem, rootFiles: string[], options: CompilerOptions, configFile: string) { + const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost(vfs)); verifyProgramWithoutConfigFile(watchingSystemHost, rootFiles, options); verifyProgramWithConfigFile(watchingSystemHost, configFile); } it("has empty options", () => { - const file1: FileOrFolder = { - path: "/a/b/file1.ts", - content: "let x = 1" - }; - const file2: FileOrFolder = { - path: "/a/b/file2.ts", - content: "let y = 1" - }; - const configFile: FileOrFolder = { - path: "/a/b/tsconfig.json", - content: "{}" - }; - verifyProgram([file1, file2, libFile, configFile], [file1.path, file2.path], {}, configFile.path); + const fs = new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true); + const file1 = fs.addFile("/a/b/file1.ts", "let x = 1"); + const file2 = fs.addFile("/a/b/file2.ts", "let y = 1"); + const configFile = fs.addFile("/a/b/tsconfig.json", "{}"); + fs.addFile(libFile.path, libFile.content); + verifyProgram(fs, [file1.path, file2.path], {}, configFile.path); }); it("has lib specified in the options", () => { const compilerOptions: CompilerOptions = { lib: ["es5", "es2015.promise"] }; - const app: FileOrFolder = { - path: "/src/app.ts", - content: "var x: Promise;" - }; - const configFile: FileOrFolder = { - path: "/src/tsconfig.json", - content: JSON.stringify({ compilerOptions }) - }; - const es5Lib: FileOrFolder = { - path: "/compiler/lib.es5.d.ts", - content: "declare const eval: any" - }; - const es2015Promise: FileOrFolder = { - path: "/compiler/lib.es2015.promise.d.ts", - content: "declare class Promise {}" - }; - - verifyProgram([app, configFile, es5Lib, es2015Promise], [app.path], compilerOptions, configFile.path); + const fs = new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true); + const app = fs.addFile("/src/app.ts", "var x: Promise;"); + const configFile = fs.addFile("/src/tsconfig.json", JSON.stringify({ compilerOptions })); + fs.addFile("/compiler/lib.es5.d.ts", "declare const eval: any;"); + fs.addFile("/compiler/lib.es2015.promise.d.ts", "declare class Promise {}"); + verifyProgram(fs, [app.path], compilerOptions, configFile.path); }); it("has paths specified in the options", () => { @@ -972,31 +950,23 @@ namespace ts { ] } }; - const app: FileOrFolder = { - path: "/src/packages/framework/app.ts", - content: 'import classc from "module1/lib/file1";\ - import classD from "module3/file3";\ - let x = new classc();\ - let y = new classD();' - }; - const module1: FileOrFolder = { - path: "/src/packages/mail/data/module1/lib/file1.ts", - content: 'import classc from "module2/file2";export default classc;', - }; - const module2: FileOrFolder = { - path: "/src/packages/mail/data/module1/lib/module2/file2.ts", - content: 'class classc { method2() { return "hello"; } }\nexport default classc', - }; - const module3: FileOrFolder = { - path: "/src/packages/styles/module3/file3.ts", - content: "class classD { method() { return 10; } }\nexport default classD;" - }; - const configFile: FileOrFolder = { - path: "/src/tsconfig.json", - content: JSON.stringify({ compilerOptions }) - }; - - verifyProgram([app, module1, module2, module3, libFile, configFile], [app.path], compilerOptions, configFile.path); + const fs = new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true); + const app = fs.addFile("/src/packages/framework/app.ts", + `import classC from "module1/lib/file1";\n` + + `import classD from "module3/file3";\n` + + `let x = new classC();\n` + + `let y = new classD();`); + fs.addFile("/src/packages/mail/data/module1/lib/file1.ts", + `import classC from "module2/file2";\n` + + `export default classC;`); + fs.addFile("/src/packages/mail/data/module1/lib/module2/file2.ts", + `class classC { method2() { return "hello"; } }\n` + + `export default classC;`); + fs.addFile("/src/packages/styles/module3/file3.ts", + `class classD { method() { return 10; } }\n` + + `export default classD;`); + const configFile = fs.addFile("/src/tsconfig.json", JSON.stringify({ compilerOptions })); + verifyProgram(fs, [app.path], compilerOptions, configFile.path); }); it("has include paths specified in tsconfig file", () => { @@ -1010,31 +980,24 @@ namespace ts { ] } }; - const app: FileOrFolder = { - path: "/src/packages/framework/app.ts", - content: 'import classc from "module1/lib/file1";\ - import classD from "module3/file3";\ - let x = new classc();\ - let y = new classD();' - }; - const module1: FileOrFolder = { - path: "/src/packages/mail/data/module1/lib/file1.ts", - content: 'import classc from "module2/file2";export default classc;', - }; - const module2: FileOrFolder = { - path: "/src/packages/mail/data/module1/lib/module2/file2.ts", - content: 'class classc { method2() { return "hello"; } }\nexport default classc', - }; - const module3: FileOrFolder = { - path: "/src/packages/styles/module3/file3.ts", - content: "class classD { method() { return 10; } }\nexport default classD;" - }; - const configFile: FileOrFolder = { - path: "/src/tsconfig.json", - content: JSON.stringify({ compilerOptions, include: ["packages/**/ *.ts"] }) - }; - - const watchingSystemHost = createWatchingSystemHost(createTestSystem([app, module1, module2, module3, libFile, configFile])); + const fs = new vfs.VirtualFileSystem("/", /*useCaseSensitiveFileNames*/ true); + fs.addFile("/src/packages/framework/app.ts", + `import classC from "module1/lib/file1";\n` + + `import classD from "module3/file3";\n` + + `let x = new classC();\n` + + `let y = new classD();`); + fs.addFile("/src/packages/mail/data/module1/lib/file1.ts", + `import classC from "module2/file2";\n` + + `export default classC;`); + fs.addFile("/src/packages/mail/data/module1/lib/module2/file2.ts", + `class classC { method2() { return "hello"; } }\n` + + `export default classC;`); + fs.addFile("/src/packages/styles/module3/file3.ts", + `class classD { method() { return 10; } }\n` + + `export default classD;`); + const configFile = fs.addFile("/src/tsconfig.json", + JSON.stringify({ compilerOptions, include: ["packages/**/ *.ts"] })); + const watchingSystemHost = createWatchingSystemHost(new mocks.MockServerHost(fs)); verifyProgramWithConfigFile(watchingSystemHost, configFile.path); }); }); diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 4e2d63cec90..e173df04abc 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -22,7 +22,7 @@ namespace ts.tscWatch { checkFileNames(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); } - function createWatchingSystemHost(system: WatchedSystem) { + function createWatchingSystemHost(system: ts.System) { return ts.createWatchingSystemHost(/*pretty*/ undefined, system); } @@ -30,22 +30,22 @@ namespace ts.tscWatch { return ts.parseConfigFile(configFileName, {}, watchingSystemHost.system, watchingSystemHost.reportDiagnostic, watchingSystemHost.reportWatchDiagnostic); } - function createWatchModeWithConfigFile(configFilePath: string, host: WatchedSystem) { + function createWatchModeWithConfigFile(configFilePath: string, host: ts.System) { const watchingSystemHost = createWatchingSystemHost(host); const configFileResult = parseConfigFile(configFilePath, watchingSystemHost); return ts.createWatchModeWithConfigFile(configFileResult, {}, watchingSystemHost); } - function createWatchModeWithoutConfigFile(fileNames: string[], host: WatchedSystem, options: CompilerOptions = {}) { + function createWatchModeWithoutConfigFile(fileNames: string[], host: ts.System, options: CompilerOptions = {}) { const watchingSystemHost = createWatchingSystemHost(host); return ts.createWatchModeWithoutConfigFile(fileNames, options, watchingSystemHost); } - function getEmittedLineForMultiFileOutput(file: FileOrFolder, host: WatchedSystem) { + function getEmittedLineForMultiFileOutput(file: FileOrFolder, host: ts.System) { return `TSFILE: ${file.path.replace(".ts", ".js")}${host.newLine}`; } - function getEmittedLineForSingleFileOutput(filename: string, host: WatchedSystem) { + function getEmittedLineForSingleFileOutput(filename: string, host: ts.System) { return `TSFILE: ${filename}${host.newLine}`; } diff --git a/src/harness/vfs.ts b/src/harness/vfs.ts index 7d31ae3df96..0fcaef05443 100644 --- a/src/harness/vfs.ts +++ b/src/harness/vfs.ts @@ -9,6 +9,11 @@ // support the eventual conversion of harness into a modular system. namespace vfs { + const S_IFMT = 0xf000; + const S_IFLNK = 0xa000; + const S_IFREG = 0x8000; + const S_IFDIR = 0x4000; + export interface PathMappings { [path: string]: string; } @@ -348,7 +353,7 @@ namespace vfs { */ public readFile(path: string): string | undefined { const file = this.getFile(vpath.resolve(this.currentDirectory, path)); - return file && file.content; + return file && file.readContent(); } /** @@ -357,9 +362,7 @@ namespace vfs { public writeFile(path: string, content: string): void { path = vpath.resolve(this.currentDirectory, path); const file = this.getFile(path) || this.addFile(path); - if (file) { - file.content = content; - } + if (file) file.writeContent(content); } /** @@ -376,6 +379,35 @@ namespace vfs { return this.getEntry(path) instanceof VirtualFile; } + public rename(oldpath: string, newpath: string) { + oldpath = vpath.resolve(this.currentDirectory, oldpath); + newpath = vpath.resolve(this.currentDirectory, newpath); + return this.root.replaceEntry(newpath, this.getEntry(oldpath)); + } + + /** + * Get file stats + */ + public getStats(path: string, options?: { noFollowSymlinks?: boolean }) { + let entry = this.getEntry(path); + if (entry && !(options && options.noFollowSymlinks)) { + entry = this.getRealEntry(entry); + } + return entry && entry.getStats(); + } + + public setStats(path: string, atime: number | Date, mtime: number | Date, options?: { noFollowSymlinks?: boolean }) { + let entry = this.getEntry(path); + if (entry && !(options && options.noFollowSymlinks)) { + entry = this.getRealEntry(entry); + } + if (entry && !entry.isReadOnly) { + entry.setStats(atime, mtime); + return true; + } + return false; + } + /** * If an entry is a symbolic link, gets the resolved target of the link. Otherwise, returns the entry. */ @@ -425,6 +457,39 @@ namespace vfs { return this.root.getDirectory(vpath.resolve(this.currentDirectory, path), options); } + public getEntries(path: string, options: { recursive?: boolean, pattern?: RegExp, kind: "file" }): VirtualFile[]; + public getEntries(path: string, options: { recursive?: boolean, pattern?: RegExp, kind: "directory" }): VirtualDirectory[]; + public getEntries(path: string, options?: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" }): VirtualEntry[]; + public getEntries(path: string, options?: { recursive?: boolean, pattern?: RegExp, kind?: "file" | "directory" }) { + const dir = this.root.getDirectory(vpath.resolve(this.currentDirectory, path)); + return dir ? dir.getEntries(options) : []; + } + + public getDirectories(path: string, options?: { recursive?: boolean, pattern?: RegExp }) { + const dir = this.root.getDirectory(vpath.resolve(this.currentDirectory, path)); + return dir ? dir.getDirectories(options) : []; + } + + public getFiles(path: string, options?: { recursive?: boolean, pattern?: RegExp }) { + const dir = this.root.getDirectory(vpath.resolve(this.currentDirectory, path)); + return dir ? dir.getFiles(options) : []; + } + + public getEntryNames(path: string, options?: { recursive?: boolean, qualified?: boolean, pattern?: RegExp, kind?: "file" | "directory" }) { + const dir = this.root.getDirectory(vpath.resolve(this.currentDirectory, path)); + return dir ? dir.getEntryNames(options) : []; + } + + public getDirectoryNames(path: string, options?: { recursive?: boolean, qualified?: boolean, pattern?: RegExp }) { + const dir = this.root.getDirectory(vpath.resolve(this.currentDirectory, path)); + return dir ? dir.getDirectoryNames(options) : []; + } + + public getFileNames(path: string, options?: { recursive?: boolean, qualified?: boolean, pattern?: RegExp }) { + const dir = this.root.getDirectory(vpath.resolve(this.currentDirectory, path)); + return dir ? dir.getFileNames(options) : []; + } + /** * Gets the accessible file system entries from a path relative to the current directory. */ @@ -587,17 +652,29 @@ namespace vfs { } export abstract class VirtualFileSystemEntry extends VirtualFileSystemObject { + private static _nextId = 1; private _path: string; + private _name: string; + private _parent: VirtualDirectory | undefined; private _metadata: core.Metadata; + private _atimeMS: number = Date.now(); + private _mtimeMS: number = Date.now(); + private _ctimeMS: number = Date.now(); + private _birthtimeMS: number = Date.now(); + + public readonly id = VirtualFileSystemEntry._nextId++; + + constructor(parent: VirtualDirectory | undefined, name: string) { + super(); + this._parent = parent; + this._name = name; + } /** * Gets the name of this entry. */ - public readonly name: string; - - constructor(name: string) { - super(); - this.name = name; + public get name(): string { + return this._name; } /** @@ -608,10 +685,19 @@ namespace vfs { return this.parent.fileSystem; } + /** + * Gets the root directory for this entry. + */ + public get root(): VirtualDirectory | undefined { + return this.parent.root; + } + /** * Gets the parent directory for this entry. */ - public abstract get parent(): VirtualDirectory | undefined; + public get parent(): VirtualDirectory | undefined { + return this._parent; + } /** * Gets the entry that this entry shadows. @@ -665,6 +751,27 @@ namespace vfs { */ public abstract shadow(parent: VirtualDirectory): VirtualEntry; + public abstract getStats(): VirtualStats; + + public setStats(atime: number | Date, mtime: number | Date) { + this.writePreamble(); + this._atimeMS = typeof atime === "object" ? atime.getTime() : + atime < 0 ? Date.now() : + atime; + this._mtimeMS = typeof mtime === "object" ? mtime.getTime() : + mtime < 0 ? Date.now() : + mtime; + this._ctimeMS = Date.now(); + } + + protected static _setNameUnsafe(entry: VirtualFileSystemEntry, name: string) { + entry._name = name; + } + + protected static _setParentUnsafe(entry: VirtualFileSystemEntry, parent: VirtualDirectory) { + entry._parent = parent; + } + protected shadowPreamble(parent: VirtualDirectory): void { this.checkShadowParent(parent); this.checkShadowFileSystem(parent.fileSystem); @@ -681,6 +788,30 @@ namespace vfs { fileSystem = fileSystem.shadowRoot; } } + + protected getStatsCore(mode: number, size: number) { + return new VirtualStats( + this.root.id, + this.id, + mode, + size, + this._atimeMS, + this._mtimeMS, + this._ctimeMS, + this._birthtimeMS); + } + + protected updateAccessTime() { + if (!this.isReadOnly) { + this._atimeMS = Date.now(); + } + } + + protected updateModificationTime() { + if (!this.isReadOnly) { + this._mtimeMS = Date.now(); + } + } } export interface VirtualDirectory { @@ -721,15 +852,13 @@ namespace vfs { export class VirtualDirectory extends VirtualFileSystemEntry { protected _shadowRoot: VirtualDirectory | undefined; - private _parent: VirtualDirectory; private _entries: core.KeyedCollection | undefined; private _resolver: FileSystemResolver | undefined; private _onChildFileSystemChange: (path: string, change: FileSystemChange) => void; constructor(parent: VirtualDirectory | undefined, name: string, resolver?: FileSystemResolver) { - super(name); + super(parent, name); if (parent === undefined && !(this instanceof VirtualRoot)) throw new TypeError(); - this._parent = parent; this._entries = undefined; this._resolver = resolver; this._shadowRoot = undefined; @@ -737,10 +866,10 @@ namespace vfs { } /** - * Gets the container for this entry. + * Gets the root directory for this entry. */ - public get parent(): VirtualDirectory | undefined { - return this._parent; + public get root(): VirtualDirectory | undefined { + return this.parent instanceof VirtualRoot ? this : undefined; } /** @@ -911,7 +1040,6 @@ namespace vfs { * Adds a directory (and all intermediate directories) for a path relative to this directory. */ public addDirectory(path: string, resolver?: FileSystemResolver): VirtualDirectory | undefined { - this.writePreamble(); const components = this.parsePath(path); const directory = this.walkContainers(components, /*create*/ true); return directory && directory.addOwnDirectory(components[components.length - 1], resolver); @@ -921,7 +1049,6 @@ namespace vfs { * Adds a file (and all intermediate directories) for a path relative to this directory. */ public addFile(path: string, content?: FileSystemResolver | ContentResolver | string, options?: { overwrite?: boolean }): VirtualFile | undefined { - this.writePreamble(); const components = this.parsePath(path); const directory = this.walkContainers(components, /*create*/ true); return directory && directory.addOwnFile(components[components.length - 1], content, options); @@ -940,7 +1067,6 @@ namespace vfs { */ public addSymlink(path: string, target: string | VirtualEntry): VirtualSymlink | undefined; public addSymlink(path: string, target: string | VirtualEntry): VirtualSymlink | undefined { - this.writePreamble(); const targetEntry = typeof target === "string" ? this.fileSystem.getEntry(vpath.resolve(this.path, target)) : target; if (targetEntry === undefined) return undefined; const components = this.parsePath(path); @@ -952,7 +1078,6 @@ namespace vfs { * Removes a directory (and all of its contents) at a path relative to this directory. */ public removeDirectory(path: string): boolean { - this.writePreamble(); const components = this.parsePath(path); const directory = this.walkContainers(components, /*create*/ false); return directory ? directory.removeOwnDirectory(components[components.length - 1]) : false; @@ -962,13 +1087,17 @@ namespace vfs { * Removes a file at a path relative to this directory. */ public removeFile(path: string): boolean { - this.writePreamble(); - this.writePreamble(); const components = this.parsePath(path); const directory = this.walkContainers(components, /*create*/ false); return directory ? directory.removeOwnFile(components[components.length - 1]) : false; } + public replaceEntry(path: string, entry: VirtualEntry) { + const components = this.parsePath(path); + const directory = this.walkContainers(components, /*create*/ false); + return directory ? directory.replaceOwnEntry(components[components.length - 1], entry) : false; + } + /** * Creates a shadow copy of this directory. Changes made to the shadow do not affect * this directory. @@ -980,6 +1109,10 @@ namespace vfs { return shadow; } + public getStats() { + return super.getStatsCore(S_IFDIR, 0); + } + protected makeReadOnlyCore(): void { if (this._entries) { this._entries.forEach(entry => entry.makeReadOnly()); @@ -1026,69 +1159,97 @@ namespace vfs { return undefined; } + this.writePreamble(); const entry = new VirtualDirectory(this, name, resolver); - this.getOwnEntries().set(entry.name, entry); - this.emit("childAdded", entry); - entry.emit("fileSystemChange", entry.path, "added"); - entry.addListener("fileSystemChange", this._onChildFileSystemChange); + this.addOwnEntry(entry); return entry; } protected addOwnFile(name: string, content?: FileSystemResolver | ContentResolver | string, options: { overwrite?: boolean } = {}): VirtualFile | undefined { const existing = this.getOwnEntry(name); - if (existing) { - if (!options.overwrite || !(existing instanceof VirtualFile)) { - return undefined; - } + if (existing && (!options.overwrite || !(existing instanceof VirtualFile))) { + return undefined; + } + this.writePreamble(); + if (existing) { // Remove the existing entry this.getOwnEntries().delete(name); } const entry = new VirtualFile(this, name, content); - this.getOwnEntries().set(entry.name, entry); - this.emit("childAdded", entry); - entry.emit("fileSystemChange", entry.path, "added"); - entry.addListener("fileSystemChange", this._onChildFileSystemChange); + this.addOwnEntry(entry); return entry; } protected addOwnSymlink(name: string, target: VirtualEntry): VirtualSymlink | undefined { if (this.getOwnEntry(name)) return undefined; + this.writePreamble(); const entry = target instanceof VirtualFile ? new VirtualFileSymlink(this, name, target.path) : new VirtualDirectorySymlink(this, name, target.path); - this.getOwnEntries().set(entry.name, entry); - this.emit("childAdded", entry); - entry.emit("fileSystemChange", entry.path, "added"); - entry.addListener("fileSystemChange", this._onChildFileSystemChange); + this.addOwnEntry(entry); return entry; } protected removeOwnDirectory(name: string) { - const entries = this.getOwnEntries(); - const entry = entries.get(name); + const entry = this.getOwnEntries().get(name); if (entry instanceof VirtualDirectory) { - entries.delete(name); - this.emit("childRemoved", entry); - this.emit("fileSystemChange", entry.path, "removed"); - entry.removeListener("fileSystemChange", this._onChildFileSystemChange); + this.writePreamble(); + this.removeOwnEntry(entry); return true; } return false; } protected removeOwnFile(name: string) { - const entries = this.getOwnEntries(); - const entry = entries.get(name); + const entry = this.getOwnEntries().get(name); if (entry instanceof VirtualFile) { - entries.delete(name); - this.emit("childRemoved", entry); - this.emit("fileSystemChange", entry.path, "removed"); - entry.removeListener("fileSystemChange", this._onChildFileSystemChange); + this.writePreamble(); + this.removeOwnEntry(entry); return true; } return false; } + protected replaceOwnEntry(name: string, entry: VirtualEntry) { + const existing = this.getOwnEntry(name); + // cannot replace yourself + // cannot move a file or directory into a read-only container. + if (entry === existing || + this.isReadOnly) { + return false; + } + else if (entry instanceof VirtualDirectory) { + // cannot move a directory on top of a file. + // cannot move a directory on top of a non-empty directory. + // cannot move a directory if its parent is read-only + // cannot move a directory underneath itself. + if (existing instanceof VirtualFile || + existing instanceof VirtualDirectory && existing.getEntries().length > 0 || + entry.parent.isReadOnly || + vpath.beneath(entry.path, vpath.combine(this.path, name), !this.fileSystem.useCaseSensitiveFileNames)) { + return false; + } + } + else if (entry instanceof VirtualFile) { + // cannot move a file on top of a directory. + // cannot move a file if its parent is read-only. + if (existing instanceof VirtualDirectory || + entry.parent.isReadOnly) { + return false; + } + } + + // delete any existing file or directory + if (existing) this.removeOwnEntry(existing); + + // move and rename the entry + entry.parent.removeOwnEntry(entry); + VirtualFileSystemEntry._setNameUnsafe(entry, name); + VirtualFileSystemEntry._setParentUnsafe(entry, this); + this.addOwnEntry(entry); + return true; + } + private parsePath(path: string) { return vpath.parse(vpath.normalize(path)); } @@ -1126,6 +1287,25 @@ namespace vfs { return this.getOwnDirectory(name) || this.addOwnDirectory(name); } + private addOwnEntry(entry: VirtualEntry) { + this.getOwnEntries().set(entry.name, entry); + this.updateAccessTime(); + this.updateModificationTime(); + this.emit("childAdded", entry); + entry.emit("fileSystemChange", entry.path, "added"); + entry.addListener("fileSystemChange", this._onChildFileSystemChange); + } + + private removeOwnEntry(entry: VirtualEntry) { + const entries = this.getOwnEntries(); + entries.delete(entry.name); + this.updateAccessTime(); + this.updateModificationTime(); + this.emit("childRemoved", entry); + this.emit("fileSystemChange", entry.path, "removed"); + entry.removeListener("fileSystemChange", this._onChildFileSystemChange); + } + private onChildFileSystemChange(path: string, change: FileSystemChange) { this.emit("fileSystemChange", path, change); } @@ -1195,6 +1375,21 @@ namespace vfs { return shadow; } + public readTargetPath() { + const targetPath = this.targetPath; + if (targetPath) this.updateAccessTime(); + return targetPath; + } + + public writeTargetPath(targetPath: string) { + this.targetPath = targetPath; + if (targetPath) this.updateModificationTime(); + } + + public getStats() { + return super.getStatsCore(S_IFLNK, this.targetPath.length); + } + protected addOwnDirectory(name: string, resolver?: FileSystemResolver): VirtualDirectory | undefined { const target = this.target; const child = target && target.addDirectory(name, resolver); @@ -1338,6 +1533,10 @@ namespace vfs { return this._fileSystem; } + public get root(): VirtualDirectory | undefined { + return undefined; + } + public get path(): string { return ""; } @@ -1396,27 +1595,18 @@ namespace vfs { export class VirtualFile extends VirtualFileSystemEntry { protected _shadowRoot: VirtualFile | undefined; - private _parent: VirtualDirectory; private _content: string | undefined; private _contentWasSet: boolean; private _resolver: FileSystemResolver | ContentResolver | undefined; constructor(parent: VirtualDirectory, name: string, content?: FileSystemResolver | ContentResolver | string) { - super(name); - this._parent = parent; + super(parent, name); this._content = typeof content === "string" ? content : undefined; this._resolver = typeof content !== "string" ? content : undefined; this._shadowRoot = undefined; this._contentWasSet = this._content !== undefined; } - /** - * Gets the parent directory for this entry. - */ - public get parent(): VirtualDirectory { - return this._parent; - } - /** * Gets the entry that this entry shadows. */ @@ -1470,6 +1660,27 @@ namespace vfs { return shadow; } + /** + * Reads the content and updates the file's access time. + */ + public readContent() { + const content = this.content; + if (content) this.updateAccessTime(); + return content; + } + + /** + * Writes the provided content and updates the file's modification time. + */ + public writeContent(content: string) { + this.content = content; + if (content) this.updateModificationTime(); + } + + public getStats() { + return super.getStatsCore(S_IFREG, this.content ? this.content.length : 0); + } + protected makeReadOnlyCore(): void { /*ignored*/ } } @@ -1548,6 +1759,29 @@ namespace vfs { return shadow; } + public readContent() { + return this.content; + } + + public writeContent(content: string) { + this.content = content; + } + + public readTargetPath() { + const targetPath = this.targetPath; + if (targetPath) this.updateAccessTime(); + return targetPath; + } + + public writeTargetPath(targetPath: string) { + this.targetPath = targetPath; + if (targetPath) this.updateModificationTime(); + } + + public getStats() { + return super.getStatsCore(S_IFLNK, this.targetPath.length); + } + private resolveTarget() { if (!this._target) { const entry = findTarget(this.fileSystem, this.targetPath); @@ -1598,6 +1832,48 @@ namespace vfs { protected onTargetFileSystemChange() { /* views do not propagate file system events */ } } + export class VirtualStats { + public readonly dev: number; + public readonly ino: number; + public readonly mode: number; + public readonly size: number; + public readonly atimeMS: number; + public readonly mtimeMS: number; + public readonly ctimeMS: number; + public readonly birthtimeMS: number; + public readonly atime: Date; + public readonly mtime: Date; + public readonly ctime: Date; + public readonly birthtime: Date; + constructor(dev: number, ino: number, mode: number, size: number, + atimeMS: number, mtimeMS: number, ctimeMS: number, birthtimeMS: number) { + this.dev = dev; + this.ino = ino; + this.mode = mode; + this.size = size; + this.atimeMS = atimeMS; + this.mtimeMS = mtimeMS; + this.ctimeMS = ctimeMS; + this.birthtimeMS = birthtimeMS; + this.atime = new Date(atimeMS + 0.5); + this.mtime = new Date(mtimeMS + 0.5); + this.ctime = new Date(ctimeMS + 0.5); + this.birthtime = new Date(birthtimeMS + 0.5); + } + + public isFile() { + return (this.mode & S_IFMT) === S_IFREG; + } + + public isDirectory() { + return (this.mode & S_IFMT) === S_IFDIR; + } + + public isSymbolicLink() { + return (this.mode & S_IFMT) === S_IFLNK; + } + } + function findTarget(vfs: VirtualFileSystem, target: string, set?: Set): VirtualEntry | undefined { const entry = vfs.getEntry(target); if (entry instanceof VirtualFileSymlink || entry instanceof VirtualDirectorySymlink) { diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts index 1238e7d437e..c1d99edb8ed 100644 --- a/src/harness/virtualFileSystemWithWatch.ts +++ b/src/harness/virtualFileSystemWithWatch.ts @@ -1,4 +1,5 @@ /// +/// // TODO(rbuckton): Migrate this to use vfs. @@ -160,7 +161,7 @@ interface Array {}` checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); } - export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray) { + export function checkOutputContains(host: TestServerHost | mocks.MockServerHost, expected: ReadonlyArray) { const mapExpected = arrayToSet(expected); const mapSeen = createMap(); for (const f of host.getOutput()) { @@ -173,7 +174,7 @@ interface Array {}` assert.equal(mapExpected.size, 0, `Output has missing ${JSON.stringify(flatMapIter(mapExpected.keys(), key => key))} in ${JSON.stringify(host.getOutput())}`); } - export function checkOutputDoesNotContain(host: TestServerHost, expectedToBeAbsent: string[] | ReadonlyArray) { + export function checkOutputDoesNotContain(host: TestServerHost | mocks.MockServerHost, 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())}`); @@ -686,4 +687,5 @@ interface Array {}` } readonly getEnvironmentVariable = notImplemented; } -} + +} \ No newline at end of file diff --git a/tests/cases/conformance/types/never/neverInference.ts b/tests/cases/conformance/types/never/neverInference.ts index 6570101c56e..11e55c2efcf 100644 --- a/tests/cases/conformance/types/never/neverInference.ts +++ b/tests/cases/conformance/types/never/neverInference.ts @@ -1,4 +1,4 @@ -// @lib: es5 +// @lib: es2015 // @strict: true // @target: es2015