Partial deprecation of non-vfs TestServerHost

This commit is contained in:
Ron Buckton 2017-11-12 16:23:08 -08:00
parent 646c32cb09
commit 4fbc74ec26
8 changed files with 811 additions and 158 deletions

View File

@ -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;
}
}

339
src/harness/mocks.ts Normal file
View File

@ -0,0 +1,339 @@
/// <reference path="./core.ts" />
/// <reference path="./vfs.ts" />
// 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<any>;
}
export interface Timeout {
readonly kind: "timeout";
readonly callback: (...args: any[]) => void;
readonly args: ReadonlyArray<any>;
readonly due: number;
}
export interface Interval {
readonly kind: "interval";
readonly callback: (...args: any[]) => void;
readonly args: ReadonlyArray<any>;
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<Immediate>();
private _timeouts = new Set<Timeout>();
private _intervals = new Set<InternalInterval>();
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<string>, exclude?: ReadonlyArray<string>, include?: ReadonlyArray<string>, 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<string> {
return this._output;
}
public clearOutput() {
this._output.length = 0;
}
}
}

View File

@ -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",

View File

@ -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<string>;"
};
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<T> {}"
};
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<string>;");
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<T> {}");
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);
});
});

View File

@ -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}`;
}

View File

@ -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<string, VirtualEntry> | 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<VirtualSymlink>): VirtualEntry | undefined {
const entry = vfs.getEntry(target);
if (entry instanceof VirtualFileSymlink || entry instanceof VirtualDirectorySymlink) {

View File

@ -1,4 +1,5 @@
/// <reference path="harness.ts" />
/// <reference path="./mocks.ts" />
// TODO(rbuckton): Migrate this to use vfs.
@ -160,7 +161,7 @@ interface Array<T> {}`
checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories);
}
export function checkOutputContains(host: TestServerHost, expected: ReadonlyArray<string>) {
export function checkOutputContains(host: TestServerHost | mocks.MockServerHost, expected: ReadonlyArray<string>) {
const mapExpected = arrayToSet(expected);
const mapSeen = createMap<true>();
for (const f of host.getOutput()) {
@ -173,7 +174,7 @@ interface Array<T> {}`
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<string>) {
export function checkOutputDoesNotContain(host: TestServerHost | mocks.MockServerHost, expectedToBeAbsent: string[] | ReadonlyArray<string>) {
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<T> {}`
}
readonly getEnvironmentVariable = notImplemented;
}
}
}

View File

@ -1,4 +1,4 @@
// @lib: es5
// @lib: es2015
// @strict: true
// @target: es2015