diff --git a/src/harness/fakes.ts b/src/harness/fakes.ts index 125f0ec8804..5c23bedfbe0 100644 --- a/src/harness/fakes.ts +++ b/src/harness/fakes.ts @@ -99,6 +99,12 @@ namespace fakes { ? _vfs : new vfs.VirtualFileSystem(currentDirectory, useCaseSensitiveFileNames); + if (this.vfs.isReadOnly) { + this.vfs = this.vfs.shadow(); + } + + this.vfs.addDirectory(this.vfs.currentDirectory); + this.useCaseSensitiveFileNames = this.vfs.useCaseSensitiveFileNames; this.newLine = newLine; this._executingFilePath = executingFilePath; @@ -115,6 +121,7 @@ namespace fakes { dos ? FakeServerHost.dosLibPath : FakeServerHost.libPath, FakeServerHost.libContent); } + } // #region DirectoryStructureHost members @@ -293,5 +300,42 @@ namespace fakes { } } } + + // expectations + public checkOutputContains(expected: Iterable) { + const mapExpected = new Set(expected); + const mapSeen = new Set(); + for (const f of this._output) { + assert.isFalse(mapSeen.has(f), `Already found ${f} in ${JSON.stringify(this._output)}`); + if (mapExpected.has(f)) { + mapExpected.delete(f); + mapSeen.add(f); + } + } + assert.equal(mapExpected.size, 0, `Output is missing ${JSON.stringify(ts.flatMap(Array.from(mapExpected.keys()), key => key))} in ${JSON.stringify(this._output)}`); + } + + public checkOutputDoesNotContain(notExpected: Iterable) { + const mapExpectedToBeAbsent = new Set(notExpected); + for (const f of this._output) { + assert.isFalse(mapExpectedToBeAbsent.has(f), `Contains ${f} in ${JSON.stringify(this._output)}`); + } + } + + public checkWatchedFiles(expected: Iterable) { + return checkSortedSet(this.vfs.watchedFiles, expected); + } + + public checkWatchedDirectories(expected: Iterable, recursive = false) { + return checkSortedSet(recursive ? this.vfs.watchedRecursiveDirectories : this.vfs.watchedNonRecursiveDirectories, expected); + } + } + + function checkSortedSet(set: ReadonlySet, values: Iterable) { + const array = Array.from(values); + assert.strictEqual(set.size, array.length, `Actual: ${Array.from(set)}, expected: ${array}.`); + for (const value of array) { + assert.isTrue(set.has(value)); + } } } \ No newline at end of file diff --git a/src/harness/tsconfig.json b/src/harness/tsconfig.json index cf533cd03dc..c3ead3a0557 100644 --- a/src/harness/tsconfig.json +++ b/src/harness/tsconfig.json @@ -84,7 +84,6 @@ "compiler.ts", "fakes.ts", - "virtualFileSystemWithWatch.ts", "sourceMapRecorder.ts", "harnessLanguageService.ts", "fourslash.ts", diff --git a/src/harness/unittests/compileOnSave.ts b/src/harness/unittests/compileOnSave.ts index 84c488770db..0ca2867121f 100644 --- a/src/harness/unittests/compileOnSave.ts +++ b/src/harness/unittests/compileOnSave.ts @@ -6,8 +6,6 @@ namespace ts.projectSystem { const nullCancellationToken = server.nullCancellationToken; - import libFile = ts.TestFSWithWatch.libFile; - import FileOrFolder = ts.TestFSWithWatch.FileOrFolder; function createTestTypingsInstaller(host: server.ServerHost) { return new TestTypingsInstaller("/a/data/", /*throttleLimit*/5, host); @@ -16,7 +14,7 @@ namespace ts.projectSystem { describe("CompileOnSave affected list", () => { interface ProjectFileList { projectFileName: string; - files: (string | FileOrFolder | vfs.VirtualFile)[]; + files: (string | vfs.VirtualFile)[]; } function sendAffectedFileRequestAndCheckResult(session: server.Session, args: server.protocol.FileRequestArgs, expectedFileList: ProjectFileList | ProjectFileList[]) { @@ -64,14 +62,13 @@ namespace ts.projectSystem { describe("for configured projects", () => { it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -111,14 +108,13 @@ namespace ts.projectSystem { }); it("should be up-to-date with the reference map changes", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -178,14 +174,13 @@ namespace ts.projectSystem { }); it("should be up-to-date with changes made in non-open files", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -213,14 +208,13 @@ namespace ts.projectSystem { }); it("should be up-to-date with deleted files", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -247,14 +241,13 @@ namespace ts.projectSystem { }); it("should be up-to-date with newly created files", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -281,11 +274,10 @@ namespace ts.projectSystem { }); it("should detect changes in non-root files", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ "compileOnSave": true, "files": ["${file1Consumer1.path}"] }`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -325,14 +317,13 @@ namespace ts.projectSystem { }); it("should return all files if a global file changed shape", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); const globalFile3 = host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); const moduleFile2 = host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -354,14 +345,13 @@ namespace ts.projectSystem { }); it("should return empty array if CompileOnSave is not enabled", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{}`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -373,14 +363,13 @@ namespace ts.projectSystem { }); it("should save when compileOnSave is enabled in base tsconfig.json", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ "extends": "/a/tsconfig.json" }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); const file1Consumer2 = host.vfs.addFile("/a/b/file1Consumer2.ts", `import {Foo} from "./moduleFile1"; let z = 10;`); host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); host.vfs.addFile("/a/b/moduleFile2.ts", `export var Foo4 = 10;`); - host.vfs.addFile(libFile.path, libFile.content); host.vfs.addFile("/a/tsconfig.json", `{ "compileOnSave": true }`); const session = createSession(host, createTestTypingsInstaller(host)); @@ -393,11 +382,10 @@ namespace ts.projectSystem { }); it("should always return the file itself if '--isolatedModules' is specified", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ "compileOnSave": true, "compilerOptions": { "isolatedModules": true } }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -418,11 +406,10 @@ namespace ts.projectSystem { }); it("should always return the file itself if '--out' or '--outFile' is specified", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ "compileOnSave": true, "compilerOptions": { "module": "system", "outFile": "/a/b/out.js" } }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -443,13 +430,12 @@ namespace ts.projectSystem { }); it("should return cascaded affected file list", () => { - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{ compileOnSave": true }`); const moduleFile1 = host.vfs.addFile("/a/b/moduleFile1.ts", `export function Foo() { };`); const file1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1.ts", `import {Foo} from "./moduleFile1"; export var y = 10;`); const file1Consumer1Consumer1 = host.vfs.addFile("/a/b/file1Consumer1Consumer1.ts", `import {y} from "./file1Consumer1";`); host.vfs.addFile("/a/b/globalFile3.ts", `interface GlobalFoo { age: number }`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host, createTestTypingsInstaller(host)); @@ -574,11 +560,10 @@ namespace ts.projectSystem { }); it("should emit specified file", () => { - const host = new fakes.FakeServerHost({ newLine: "\r\n" }); + const host = new fakes.FakeServerHost({ lib: true, newLine: "\r\n" }); const file1 = host.vfs.addFile("/a/b/f1.ts", `export function Foo() { return 10; }`); const file2 = host.vfs.addFile("/a/b/f2.ts", `import {Foo} from "./f1"; let y = Foo();`); const configFile = host.vfs.addFile("/a/b/tsconfig.json", `{}`); - host.vfs.addFile(libFile.path, libFile.content); const typingsInstaller = createTestTypingsInstaller(host); const session = createSession(host, { typingsInstaller }); @@ -595,12 +580,11 @@ namespace ts.projectSystem { it("shoud not emit js files in external projects", () => { const externalProjectName = "/a/b/externalproject"; - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const file1 = host.vfs.addFile("/a/b/file1.ts", `consonle.log('file1');`); // file2 has errors. The emit should not be blocked. const file2 = host.vfs.addFile("/a/b/file2.js", `console.log'file2');`); const file3 = host.vfs.addFile("/a/b/file3.js", `console.log('file3');`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host); const projectService = session.getProjectService(); @@ -628,9 +612,8 @@ namespace ts.projectSystem { it("should use project root as current directory so that compile on save results in correct file mapping", () => { const externalProjectName = "/root/TypeScriptProject3/TypeScriptProject3/TypeScriptProject3.csproj"; - const host = new fakes.FakeServerHost(); + const host = new fakes.FakeServerHost({ lib: true }); const file1 = host.vfs.addFile("/root/TypeScriptProject3/TypeScriptProject3/Foo.ts", `consonle.log('file1');`); - host.vfs.addFile(libFile.path, libFile.content); const session = createSession(host); const projectService = session.getProjectService(); diff --git a/src/harness/unittests/extractTestHelpers.ts b/src/harness/unittests/extractTestHelpers.ts index 93ea2642675..7f6e8acb6b5 100644 --- a/src/harness/unittests/extractTestHelpers.ts +++ b/src/harness/unittests/extractTestHelpers.ts @@ -99,19 +99,6 @@ namespace ts { getCurrentDirectory: notImplemented, }; - function createServerHost(files: ts.TestFSWithWatch.FileOrFolder[], options?: Partial) { - const host = new fakes.FakeServerHost(options); - for (const file of files) { - if (isString(file.content)) { - host.vfs.writeFile(file.path, file.content); - } - else { - host.vfs.addDirectory(file.path); - } - } - return host; - } - export function testExtractSymbol(caption: string, text: string, baselineFolder: string, description: DiagnosticMessage, includeLib?: boolean) { const t = extractTest(text); const selectionRange = t.ranges.get("selection"); @@ -168,7 +155,8 @@ namespace ts { } function makeProgram(f: {path: string, content: string }, includeLib?: boolean) { - const host = createServerHost(includeLib ? [f, ts.TestFSWithWatch.libFile] : [f]); // libFile is expensive to parse repeatedly - only test when required + const host = new fakes.FakeServerHost({ lib: includeLib }); // libFile is expensive to parse repeatedly - only test when required + host.vfs.writeFile(f.path, f.content); const projectService = projectSystem.createProjectService(host); projectService.openClientFile(f.path); const program = projectService.inferredProjects[0].getLanguageService().getProgram(); @@ -188,15 +176,12 @@ namespace ts { if (!selectionRange) { throw new Error(`Test ${caption} does not specify selection range`); } - const f = { - path: "/a.ts", - content: t.source - }; - const host = ts.TestFSWithWatch.createServerHost([f, ts.TestFSWithWatch.libFile]); + const host = new fakes.FakeServerHost({ lib: true }); + host.vfs.writeFile("/a.ts", t.source); const projectService = projectSystem.createProjectService(host); - projectService.openClientFile(f.path); + projectService.openClientFile("/a.ts"); const program = projectService.inferredProjects[0].getLanguageService().getProgram(); - const sourceFile = program.getSourceFile(f.path); + const sourceFile = program.getSourceFile("/a.ts"); const context: RefactorContext = { cancellationToken: { throwIfCancellationRequested: noop, isCancellationRequested: returnFalse }, newLineCharacter, diff --git a/src/harness/unittests/reuseProgramStructure.ts b/src/harness/unittests/reuseProgramStructure.ts index 7219c58937b..d43cb03ca96 100644 --- a/src/harness/unittests/reuseProgramStructure.ts +++ b/src/harness/unittests/reuseProgramStructure.ts @@ -872,8 +872,6 @@ namespace ts { }); }); - import libFile = ts.TestFSWithWatch.libFile; - describe("isProgramUptoDate should return true when there is no change in compiler options and", () => { function verifyProgramIsUptoDate( program: Program, @@ -926,7 +924,7 @@ namespace ts { 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); + fs.addFile(fakes.FakeServerHost.libPath, fakes.FakeServerHost.libContent); verifyProgram(fs, [file1.path, file2.path], {}, configFile.path); }); diff --git a/src/harness/unittests/telemetry.ts b/src/harness/unittests/telemetry.ts index b33274da9ad..372cd5ebe87 100644 --- a/src/harness/unittests/telemetry.ts +++ b/src/harness/unittests/telemetry.ts @@ -1,48 +1,56 @@ /// /// +/// namespace ts.projectSystem { - import FileOrFolder = ts.TestFSWithWatch.FileOrFolder; - describe("project telemetry", () => { it("does nothing for inferred project", () => { - const file = makeFile("/a.js"); - const et = new TestServerEventManager([file]); - et.service.openClientFile(file.path); + const host = new fakes.FakeServerHost(); + host.vfs.writeFile("/a.js", ""); + const et = new TestServerEventManager(host); + et.service.openClientFile("/a.js"); et.hasZeroEvent(ts.server.ProjectInfoTelemetryEvent); }); it("only sends an event once", () => { - const file = makeFile("/a/a.ts"); - const file2 = makeFile("/b.ts"); - const tsconfig = makeFile("/a/tsconfig.json", {}); + const host = new fakes.FakeServerHost(); + host.vfs.writeFile("/a/a.ts", ``); + host.vfs.writeFile("/b.ts", ``); + host.vfs.writeFile("/a/tsconfig.json", `{}`); - const et = new TestServerEventManager([file, file2, tsconfig]); - et.service.openClientFile(file.path); - et.assertProjectInfoTelemetryEvent({}, tsconfig.path); + const et = new TestServerEventManager(host); + et.service.openClientFile("/a/a.ts"); + et.assertProjectInfoTelemetryEvent({}, "/a/tsconfig.json"); - et.service.closeClientFile(file.path); + et.service.closeClientFile("/a/a.ts"); checkNumberOfProjects(et.service, { configuredProjects: 1 }); - et.service.openClientFile(file2.path); + et.service.openClientFile("/b.ts"); checkNumberOfProjects(et.service, { inferredProjects: 1 }); et.hasZeroEvent(ts.server.ProjectInfoTelemetryEvent); - et.service.openClientFile(file.path); + et.service.openClientFile("/a/a.ts"); checkNumberOfProjects(et.service, { configuredProjects: 1, inferredProjects: 1 }); et.hasZeroEvent(ts.server.ProjectInfoTelemetryEvent); }); it("counts files by extension", () => { - const files = ["ts.ts", "tsx.tsx", "moo.ts", "dts.d.ts", "jsx.jsx", "js.js", "badExtension.badExtension"].map(f => makeFile(`/src/${f}`)); - const notIncludedFile = makeFile("/bin/ts.js"); const compilerOptions: ts.CompilerOptions = { allowJs: true }; - const tsconfig = makeFile("/tsconfig.json", { compilerOptions, include: ["src"] }); + const host = new fakes.FakeServerHost(); + host.vfs.writeFile("/src/ts.ts", ``); + host.vfs.writeFile("/src/tsx.tsx", ``); + host.vfs.writeFile("/src/moo.ts", ``); + host.vfs.writeFile("/src/dts.d.ts", ``); + host.vfs.writeFile("/src/jsx.jsx", ``); + host.vfs.writeFile("/src/js.js", ``); + host.vfs.writeFile("/src/badExtension.badExtension", ``); + host.vfs.writeFile("/bin/ts.js", ``); + host.vfs.writeFile("/tsconfig.json", JSON.stringify({ compilerOptions, include: ["src"] })); - const et = new TestServerEventManager([...files, notIncludedFile, tsconfig]); - et.service.openClientFile(files[0].path); + const et = new TestServerEventManager(host); + et.service.openClientFile("/src/ts.ts"); et.assertProjectInfoTelemetryEvent({ fileStats: { ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }, compilerOptions, @@ -51,8 +59,10 @@ namespace ts.projectSystem { }); it("works with external project", () => { - const file1 = makeFile("/a.ts"); - const et = new TestServerEventManager([file1]); + const host = new fakes.FakeServerHost(); + host.vfs.writeFile("/a.ts", ``); + + const et = new TestServerEventManager(host); const compilerOptions: ts.server.protocol.CompilerOptions = { strict: true }; const projectFileName = "/hunter2/foo.csproj"; @@ -61,7 +71,7 @@ namespace ts.projectSystem { // TODO: Apparently compilerOptions is mutated, so have to repeat it here! et.assertProjectInfoTelemetryEvent({ - projectId: Harness.mockHash("/hunter2/foo.csproj"), + projectId: core.sha1("/hunter2/foo.csproj"), compilerOptions: { strict: true }, compileOnSave: true, // These properties can't be present for an external project, so they are undefined instead of false. @@ -83,7 +93,7 @@ namespace ts.projectSystem { function open(): void { et.service.openExternalProject({ - rootFiles: toExternalFiles([file1.path]), + rootFiles: toExternalFiles(["/a.ts"]), options: compilerOptions, projectFileName, }); @@ -92,8 +102,6 @@ namespace ts.projectSystem { }); it("does not expose paths", () => { - const file = makeFile("/a.ts"); - const compilerOptions: ts.CompilerOptions = { project: "", outFile: "hunter2.js", @@ -148,10 +156,13 @@ namespace ts.projectSystem { lib: ["es6", "dom"], }; (compilerOptions as any).unknownCompilerOption = "hunter2"; // These are always ignored. - const tsconfig = makeFile("/tsconfig.json", { compilerOptions, files: ["/a.ts"] }); - const et = new TestServerEventManager([file, tsconfig]); - et.service.openClientFile(file.path); + const host = new fakes.FakeServerHost(); + host.vfs.writeFile("/a.ts", ``); + host.vfs.writeFile("/tsconfig.json", JSON.stringify({ compilerOptions, files: ["/a.ts"] })); + + const et = new TestServerEventManager(host); + et.service.openClientFile("/a.ts"); et.assertProjectInfoTelemetryEvent({ compilerOptions: safeCompilerOptions, @@ -160,18 +171,19 @@ namespace ts.projectSystem { }); it("sends telemetry for extends, files, include, exclude, and compileOnSave", () => { - const file = makeFile("/hunter2/a.ts"); - const tsconfig = makeFile("/tsconfig.json", { + const host = new fakes.FakeServerHost(); + host.vfs.writeFile("/hunter2/a.ts", ``); + host.vfs.writeFile("/tsconfig.json", JSON.stringify({ compilerOptions: {}, extends: "hunter2.json", files: ["hunter2/a.ts"], include: ["hunter2"], exclude: ["hunter2"], compileOnSave: true, - }); + })); - const et = new TestServerEventManager([tsconfig, file]); - et.service.openClientFile(file.path); + const et = new TestServerEventManager(host); + et.service.openClientFile("/hunter2/a.ts"); et.assertProjectInfoTelemetryEvent({ extends: true, files: true, @@ -190,8 +202,9 @@ namespace ts.projectSystem { }; it("sends telemetry for typeAcquisition settings", () => { - const file = makeFile("/a.js"); - const jsconfig = makeFile("/jsconfig.json", { + const host = new fakes.FakeServerHost(); + host.vfs.writeFile("/a.js", ``); + host.vfs.writeFile("/jsconfig.json", JSON.stringify({ compilerOptions: {}, typeAcquisition: { enable: true, @@ -199,11 +212,11 @@ namespace ts.projectSystem { include: ["hunter2", "hunter3"], exclude: [], }, - }); - const et = new TestServerEventManager([jsconfig, file]); - et.service.openClientFile(file.path); + })); + const et = new TestServerEventManager(host); + et.service.openClientFile("/a.js"); et.assertProjectInfoTelemetryEvent({ - projectId: Harness.mockHash("/jsconfig.json"), + projectId: core.sha1("/jsconfig.json"), fileStats: fileStats({ js: 1 }), compilerOptions: autoJsCompilerOptions, typeAcquisition: { @@ -216,14 +229,16 @@ namespace ts.projectSystem { }); it("detects whether language service was disabled", () => { - const file = makeFile("/a.js"); - const tsconfig = makeFile("/jsconfig.json", {}); - const et = new TestServerEventManager([tsconfig, file]); + const host = new fakes.FakeServerHost(); + host.vfs.writeFile("/a.js", ``); + host.vfs.writeFile("/jsconfig.json", `{}`); + + const et = new TestServerEventManager(host); et.host.getFileSize = () => server.maxProgramSizeForNonTsFiles + 1; - et.service.openClientFile(file.path); + et.service.openClientFile("/a.js"); et.getEvent(server.ProjectLanguageServiceStateEvent); et.assertProjectInfoTelemetryEvent({ - projectId: Harness.mockHash("/jsconfig.json"), + projectId: core.sha1("/jsconfig.json"), fileStats: fileStats({ js: 1 }), compilerOptions: autoJsCompilerOptions, configFileName: "jsconfig.json", @@ -236,8 +251,4 @@ namespace ts.projectSystem { }); }); }); - - function makeFile(path: string, content: {} = ""): FileOrFolder { - return { path, content: isString(content) ? "" : JSON.stringify(content) }; - } } diff --git a/src/harness/unittests/tscWatchMode.ts b/src/harness/unittests/tscWatchMode.ts index 7719b2c9937..bbf24189fed 100644 --- a/src/harness/unittests/tscWatchMode.ts +++ b/src/harness/unittests/tscWatchMode.ts @@ -1,8 +1,8 @@ -/// -/// -/// +/// +/// /// /// +/// namespace ts.tscWatch { import theory = utils.theory; @@ -10,11 +10,7 @@ namespace ts.tscWatch { import Arg = typemock.Arg; import Times = typemock.Times; - import checkFileNames = ts.TestFSWithWatch.checkFileNames; - import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; - import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; - import checkOutputContains = ts.TestFSWithWatch.checkOutputContains; - import checkOutputDoesNotContain = ts.TestFSWithWatch.checkOutputDoesNotContain; + import checkFileNames = utils.checkFileNames; export function checkProgramActualFiles(program: Program, expectedFiles: string[]) { checkFileNames(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); @@ -197,8 +193,8 @@ namespace ts.tscWatch { checkProgramActualFiles(watch(), ["/a/b/c/f1.ts", fakes.FakeServerHost.libPath, "/a/b/d/f2.ts"]); checkProgramRootFiles(watch(), ["/a/b/c/f1.ts", "/a/b/d/f2.ts"]); - checkWatchedFiles(host, ["/a/b/tsconfig.json", "/a/b/c/f1.ts", "/a/b/d/f2.ts", fakes.FakeServerHost.libPath]); - checkWatchedDirectories(host, ["/a/b", "/a/b/node_modules/@types"], /*recursive*/ true); + host.checkWatchedFiles(["/a/b/tsconfig.json", "/a/b/c/f1.ts", "/a/b/d/f2.ts", fakes.FakeServerHost.libPath]); + host.checkWatchedDirectories(["/a/b", "/a/b/node_modules/@types"], /*recursive*/ true); }); // TODO: if watching for config file creation @@ -211,7 +207,7 @@ namespace ts.tscWatch { host.vfs.addFile("/a/b/tsconfig.json", `{}`); const watch = createWatchModeWithConfigFile("/a/b/tsconfig.json", host); - checkWatchedDirectories(host, ["/a/b", "/a/b/node_modules/@types"], /*recursive*/ true); + host.checkWatchedDirectories(["/a/b", "/a/b/node_modules/@types"], /*recursive*/ true); checkProgramRootFiles(watch(), ["/a/b/commonFile1.ts"]); // add a new ts file @@ -767,11 +763,11 @@ namespace ts.tscWatch { function checkAffectedFiles(host: fakes.FakeServerHost, affectedFiles: ReadonlyArray, unaffectedFiles?: ReadonlyArray) { affectedFiles = getEmittedLines(affectedFiles, host, formatOutputFile); - checkOutputContains(host, affectedFiles); + host.checkOutputContains(affectedFiles); if (unaffectedFiles) { unaffectedFiles = getEmittedLines(unaffectedFiles, host, formatOutputFile); unaffectedFiles = mapDefined(unaffectedFiles, line => contains(affectedFiles, line) ? undefined : line); - checkOutputDoesNotContain(host, unaffectedFiles); + host.checkOutputDoesNotContain(unaffectedFiles); } host.clearOutput(); } diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index af6c99d7d89..3e7ebbcf546 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1,5 +1,4 @@ -/// -/// +/// /// /// /// @@ -12,12 +11,14 @@ namespace ts.projectSystem { import protocol = server.protocol; import CommandNames = server.CommandNames; - import FileOrFolder = ts.TestFSWithWatch.FileOrFolder; - import checkFileNames = ts.TestFSWithWatch.checkFileNames; - import libFile = ts.TestFSWithWatch.libFile; - import checkWatchedFiles = ts.TestFSWithWatch.checkWatchedFiles; - import checkWatchedDirectories = ts.TestFSWithWatch.checkWatchedDirectories; - import safeList = ts.TestFSWithWatch.safeList; + interface FileOrFolder { + path: string; + content?: string; + } + + import checkFileNames = utils.checkFileNames; + + const libFile = { path: fakes.FakeServerHost.libPath, content: fakes.FakeServerHost.libContent }; export const customTypesMap = { path: "/typesMap.json", @@ -68,7 +69,7 @@ namespace ts.projectSystem { installTypingHost: server.ServerHost, readonly typesRegistry = createMap(), log?: TI.Log) { - super(installTypingHost, globalTypingsCacheLocation, safeList.path, customTypesMap.path, throttleLimit, log); + super(installTypingHost, globalTypingsCacheLocation, fakes.FakeServerHost.safeListPath, customTypesMap.path, throttleLimit, log); } protected postExecActions: PostExecAction[] = []; @@ -146,8 +147,8 @@ namespace ts.projectSystem { readonly session: TestSession; readonly service: server.ProjectService; readonly host: fakes.FakeServerHost; - constructor(files: FileOrFolder[]) { - this.host = createServerHost(files); + constructor(host: fakes.FakeServerHost) { + this.host = host; this.session = createSession(this.host, { canUseEvents: true, eventHandler: event => this.events.push(event), @@ -189,7 +190,7 @@ namespace ts.projectSystem { assertProjectInfoTelemetryEvent(partial: Partial, configFile?: string): void { assert.deepEqual(this.getEvent(ts.server.ProjectInfoTelemetryEvent), { - projectId: Harness.mockHash(configFile || "/tsconfig.json"), + projectId: core.sha1(configFile || "/tsconfig.json"), fileStats: fileStats({ ts: 1 }), compilerOptions: {}, extends: false, @@ -517,6 +518,37 @@ namespace ts.projectSystem { return host; } + 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}`; + } + + 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)}`); + } + describe("tsserverProjectSystem", () => { describe("general", () => { const commonFile1: FileOrFolder = { @@ -545,9 +577,9 @@ namespace ts.projectSystem { checkFileNames("inferred project", project.getFileNames(), ["/a/b/c/app.ts", fakes.FakeServerHost.libPath, "/a/b/c/module.d.ts"]); const configFileLocations = ["/a/b/c/", "/a/b/", "/a/", "/"]; const configFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]); - checkWatchedFiles(host, configFiles.concat(fakes.FakeServerHost.libPath, "/a/b/c/module.d.ts")); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, ["/a/b/c", combinePaths("/a/b/c/", nodeModulesAtTypes)], /*recursive*/ true); + host.checkWatchedFiles(configFiles.concat(fakes.FakeServerHost.libPath, "/a/b/c/module.d.ts")); + host.checkWatchedDirectories([], /*recursive*/ false); + host.checkWatchedDirectories(["/a/b/c", combinePaths("/a/b/c/", nodeModulesAtTypes)], /*recursive*/ true); }); it("can handle tsconfig file name with difference casing", () => { @@ -608,9 +640,9 @@ namespace ts.projectSystem { checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath, file2.path, configFile.path]); checkProjectRootFiles(project, [file1.path, file2.path]); // watching all files except one that was open - checkWatchedFiles(host, [configFile.path, file2.path, fakes.FakeServerHost.libPath]); + host.checkWatchedFiles([configFile.path, file2.path, fakes.FakeServerHost.libPath]); const configFileDirectory = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); + host.checkWatchedDirectories([configFileDirectory, combinePaths(configFileDirectory, nodeModulesAtTypes)], /*recursive*/ true); }); it("create configured project with the file list", () => { @@ -648,8 +680,8 @@ namespace ts.projectSystem { checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath, file2.path, configFile.path]); checkProjectRootFiles(project, [file1.path, file2.path]); // watching all files except one that was open - checkWatchedFiles(host, [configFile.path, file2.path, fakes.FakeServerHost.libPath]); - checkWatchedDirectories(host, [getDirectoryPath(configFile.path)], /*recursive*/ false); + host.checkWatchedFiles([configFile.path, file2.path, fakes.FakeServerHost.libPath]); + host.checkWatchedDirectories([getDirectoryPath(configFile.path)], /*recursive*/ false); }); it("add and then remove a config file in a folder with loose files", () => { @@ -670,14 +702,14 @@ namespace ts.projectSystem { checkNumberOfInferredProjects(projectService, 2); const configFileLocations = ["/", "/a/", "/a/b/"]; const watchedFiles = flatMap(configFileLocations, location => [location + "tsconfig.json", location + "jsconfig.json"]).concat(fakes.FakeServerHost.libPath); - checkWatchedFiles(host, watchedFiles); + host.checkWatchedFiles(watchedFiles); // Add a tsconfig file host.vfs.addFile(configFile.path, configFile.content); host.checkTimeoutQueueLengthAndRun(1); checkNumberOfInferredProjects(projectService, 1); checkNumberOfConfiguredProjects(projectService, 1); - checkWatchedFiles(host, watchedFiles); + host.checkWatchedFiles(watchedFiles); // remove the tsconfig file host.vfs.removeFile(configFile.path); @@ -687,7 +719,7 @@ namespace ts.projectSystem { checkNumberOfInferredProjects(projectService, 2); checkNumberOfConfiguredProjects(projectService, 0); - checkWatchedFiles(host, watchedFiles); + host.checkWatchedFiles(watchedFiles); }); it("add new files to a configured project without file list", () => { @@ -699,7 +731,7 @@ namespace ts.projectSystem { const projectService = createProjectService(host); projectService.openClientFile(commonFile1.path); const configFileDir = getDirectoryPath(configFile.path); - checkWatchedDirectories(host, [configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); + host.checkWatchedDirectories([configFileDir, combinePaths(configFileDir, nodeModulesAtTypes)], /*recursive*/ true); checkNumberOfConfiguredProjects(projectService, 1); const project = configuredProjectAt(projectService, 0); @@ -2118,7 +2150,7 @@ namespace ts.projectSystem { projectService.openExternalProject({ projectFileName, options: {}, rootFiles: [{ fileName: file1.path, scriptKind: ScriptKind.JS, hasMixedContent: true }] }); checkNumberOfProjects(projectService, { externalProjects: 1 }); - checkWatchedFiles(host, [fakes.FakeServerHost.libPath]); // watching the "missing" lib file + host.checkWatchedFiles([fakes.FakeServerHost.libPath]); // watching the "missing" lib file const project = projectService.externalProjects[0]; @@ -2762,11 +2794,11 @@ namespace ts.projectSystem { const project = projectService.configuredProjects.get(configFile.path); assert.isDefined(project); checkProjectActualFiles(project, map(files, file => file.path)); - checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); + host.checkWatchedFiles(mapDefined(files, file => file === file1 ? undefined : file.path)); + host.checkWatchedDirectories([], /*recursive*/ false); const watchedRecursiveDirectories = ["/a/b/node_modules/@types"]; watchedRecursiveDirectories.push("/a/b"); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + host.checkWatchedDirectories(watchedRecursiveDirectories, /*recursive*/ true); files.push(file2); host.vfs.writeFile(file2.path, file2.content); @@ -2775,18 +2807,18 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); - checkWatchedFiles(host, mapDefined(files, file => file === file1 ? undefined : file.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + host.checkWatchedFiles(mapDefined(files, file => file === file1 ? undefined : file.path)); + host.checkWatchedDirectories([], /*recursive*/ false); + host.checkWatchedDirectories(watchedRecursiveDirectories, /*recursive*/ true); // On next file open the files file2a should be closed and not watched any more projectService.openClientFile(file2.path); checkNumberOfProjects(projectService, { configuredProjects: 1 }); assert.strictEqual(projectService.configuredProjects.get(configFile.path), project); checkProjectActualFiles(project, mapDefined(files, file => file === file2a ? undefined : file.path)); - checkWatchedFiles(host, [fakes.FakeServerHost.libPath, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + host.checkWatchedFiles([fakes.FakeServerHost.libPath, configFile.path]); + host.checkWatchedDirectories([], /*recursive*/ false); + host.checkWatchedDirectories(watchedRecursiveDirectories, /*recursive*/ true); }); @@ -2823,11 +2855,11 @@ namespace ts.projectSystem { const project = projectService.configuredProjects.get(configFile.path); assert.isDefined(project); checkProjectActualFiles(project, [file1.path, fakes.FakeServerHost.libPath, module1.path, module2.path, configFile.path]); - checkWatchedFiles(host, [fakes.FakeServerHost.libPath, module1.path, module2.path, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); + host.checkWatchedFiles([fakes.FakeServerHost.libPath, module1.path, module2.path, configFile.path]); + host.checkWatchedDirectories([], /*recursive*/ false); const watchedRecursiveDirectories = getTypeRootsFromLocation(root + "/a/b/src"); watchedRecursiveDirectories.push(`${root}/a/b/src`, `${root}/a/b/node_modules`); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + host.checkWatchedDirectories(watchedRecursiveDirectories, /*recursive*/ true); }); }); @@ -3632,10 +3664,9 @@ namespace ts.projectSystem { path: "/a/b/node_modules/@types/node/index.d.ts", content: "declare var process: any" }; - const cwd = { - path: "/a/c" - }; - const host = createServerHost([f1, config, node, cwd], { vfs: { currentDirectory: cwd.path } }); + const host = createServerHost([f1, config, node], { vfs: { currentDirectory: "/a/c" } }); + host.vfs.addDirectory("/a/c"); + const projectService = createProjectService(host); projectService.openClientFile(f1.path); projectService.checkNumberOfProjects({ configuredProjects: 1 }); @@ -3769,7 +3800,8 @@ namespace ts.projectSystem { } }` }; - const serverEventManager = new TestServerEventManager([file, configFile]); + const host = createServerHost([file, configFile]); + const serverEventManager = new TestServerEventManager(host); openFilesForSession([file], serverEventManager.session); serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path); }); @@ -3785,7 +3817,8 @@ namespace ts.projectSystem { "compilerOptions": {} }` }; - const serverEventManager = new TestServerEventManager([file, configFile]); + const host = createServerHost([file, configFile]); + const serverEventManager = new TestServerEventManager(host); openFilesForSession([file], serverEventManager.session); serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path); }); @@ -3802,7 +3835,8 @@ namespace ts.projectSystem { }` }; - const serverEventManager = new TestServerEventManager([file, configFile]); + const host = createServerHost([file, configFile]); + const serverEventManager = new TestServerEventManager(host); openFilesForSession([file], serverEventManager.session); serverEventManager.checkSingleConfigFileDiagEvent(configFile.path, file.path); @@ -3842,7 +3876,8 @@ namespace ts.projectSystem { "files": ["app.ts"] }` }; - const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); + const host = createServerHost([file, file2, libFile, configFile]); + const serverEventManager = new TestServerEventManager(host); openFilesForSession([file2], serverEventManager.session); serverEventManager.hasZeroEvent("configFileDiag"); }); @@ -3863,7 +3898,8 @@ namespace ts.projectSystem { }` }; - const serverEventManager = new TestServerEventManager([file, file2, libFile, configFile]); + const host = createServerHost([file, file2, libFile, configFile]); + const serverEventManager = new TestServerEventManager(host); openFilesForSession([file2], serverEventManager.session); serverEventManager.hasZeroEvent("configFileDiag"); }); @@ -4710,18 +4746,18 @@ namespace ts.projectSystem { service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); checkNumberOfProjects(service, { configuredProjects: 1 }); assert.isDefined(service.configuredProjects.get(configFile.path)); - checkWatchedFiles(host, [fakes.FakeServerHost.libPath, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); + host.checkWatchedFiles([fakes.FakeServerHost.libPath, configFile.path]); + host.checkWatchedDirectories([], /*recursive*/ false); const typeRootLocations = getTypeRootsFromLocation(configFileLocation); - checkWatchedDirectories(host, typeRootLocations.concat(configFileLocation), /*recursive*/ true); + host.checkWatchedDirectories(typeRootLocations.concat(configFileLocation), /*recursive*/ true); // Delete config file - should create inferred project and not configured project host.vfs.removeFile(configFile.path); host.runQueuedTimeoutCallbacks(); checkNumberOfProjects(service, { inferredProjects: 1 }); - checkWatchedFiles(host, [fakes.FakeServerHost.libPath, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, typeRootLocations, /*recursive*/ true); + host.checkWatchedFiles([fakes.FakeServerHost.libPath, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + host.checkWatchedDirectories([], /*recursive*/ false); + host.checkWatchedDirectories(typeRootLocations, /*recursive*/ true); }); it("should use projectRootPath when searching for inferred project again 2", () => { @@ -4744,18 +4780,18 @@ namespace ts.projectSystem { service.openClientFile(f1.path, /*fileContent*/ undefined, /*scriptKind*/ undefined, projectDir); checkNumberOfProjects(service, { configuredProjects: 1 }); assert.isDefined(service.configuredProjects.get(configFile.path)); - checkWatchedFiles(host, [fakes.FakeServerHost.libPath, configFile.path]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); + host.checkWatchedFiles([fakes.FakeServerHost.libPath, configFile.path]); + host.checkWatchedDirectories([], /*recursive*/ false); + host.checkWatchedDirectories(getTypeRootsFromLocation(configFileLocation).concat(configFileLocation), /*recursive*/ true); // Delete config file - should create inferred project with project root path set host.vfs.removeFile(configFile.path); host.runQueuedTimeoutCallbacks(); checkNumberOfProjects(service, { inferredProjects: 1 }); assert.equal(service.inferredProjects[0].projectRootPath, projectDir); - checkWatchedFiles(host, [fakes.FakeServerHost.libPath, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, getTypeRootsFromLocation(projectDir), /*recursive*/ true); + host.checkWatchedFiles([fakes.FakeServerHost.libPath, configFile.path, `${configFileLocation}/jsconfig.json`, `${projectDir}/tsconfig.json`, `${projectDir}/jsconfig.json`]); + host.checkWatchedDirectories([], /*recursive*/ false); + host.checkWatchedDirectories(getTypeRootsFromLocation(projectDir), /*recursive*/ true); }); }); @@ -5305,7 +5341,7 @@ namespace ts.projectSystem { function verifyCalledOnEachEntry(callback: CalledMaps, expectedKeys: Map) { const calledMap = calledMaps[callback]; - ts.TestFSWithWatch.verifyMapSize(callback, calledMap, arrayFrom(expectedKeys.keys())); + verifyMapSize(callback, calledMap, arrayFrom(expectedKeys.keys())); expectedKeys.forEach((called, name) => { assert.isTrue(calledMap.has(name), `${callback} is expected to contain ${name}, actual keys: ${arrayFrom(calledMap.keys())}`); assert.equal(calledMap.get(name).length, called, `${callback} is expected to be called ${called} times with ${name}. Actual entry: ${calledMap.get(name)}`); @@ -5676,9 +5712,9 @@ namespace ts.projectSystem { function verifyProjectAndWatchedDirectories() { checkProjectActualFiles(project, map(projectFiles, f => f.path)); - checkWatchedFiles(host, mapDefined(projectFiles, getFilePathIfNotOpen)); - checkWatchedDirectories(host, watchingRecursiveDirectories, /*recursive*/ true); - checkWatchedDirectories(host, [], /*recursive*/ false); + host.checkWatchedFiles(mapDefined(projectFiles, getFilePathIfNotOpen)); + host.checkWatchedDirectories(watchingRecursiveDirectories, /*recursive*/ true); + host.checkWatchedDirectories([], /*recursive*/ false); } } @@ -5867,9 +5903,9 @@ namespace ts.projectSystem { checkProjectActualFiles(project, projectFilePaths); const filesWatched = filter(projectFilePaths, p => p !== app.path); - checkWatchedFiles(host, filesWatched); - checkWatchedDirectories(host, typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); - checkWatchedDirectories(host, [], /*recursive*/ false); + host.checkWatchedFiles(filesWatched); + host.checkWatchedDirectories(typeRootDirectories.concat(recursiveWatchedDirectories), /*recursive*/ true); + host.checkWatchedDirectories([], /*recursive*/ false); } } @@ -6398,8 +6434,8 @@ namespace ts.projectSystem { function verifyProject() { checkProjectActualFiles(project, map(projectFiles, file => file.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, watchedRecursiveDirectories, /*recursive*/ true); + host.checkWatchedDirectories([], /*recursive*/ false); + host.checkWatchedDirectories(watchedRecursiveDirectories, /*recursive*/ true); } } @@ -6526,9 +6562,9 @@ namespace ts.projectSystem { const project = projectService.configuredProjects.get(configFile.path); assert.isDefined(project); checkProjectActualFiles(project, files.map(f => f.path)); - checkWatchedFiles(host, mapDefined(files, f => f === libFile ? windowsStyleLibFilePath : f === file1 ? undefined : f.path)); - checkWatchedDirectories(host, [], /*recursive*/ false); - checkWatchedDirectories(host, [ + host.checkWatchedFiles(mapDefined(files, f => f === libFile ? windowsStyleLibFilePath : f === file1 ? undefined : f.path)); + host.checkWatchedDirectories([], /*recursive*/ false); + host.checkWatchedDirectories([ root + "project", root + "project/node_modules/@types" ].concat(useProjectAtRoot ? [] : [root + nodeModulesAtTypes]), /*recursive*/ true); diff --git a/src/harness/unittests/typingsInstaller.ts b/src/harness/unittests/typingsInstaller.ts index 82d102d4218..91d1748ef39 100644 --- a/src/harness/unittests/typingsInstaller.ts +++ b/src/harness/unittests/typingsInstaller.ts @@ -13,7 +13,6 @@ namespace ts.projectSystem { import TI = server.typingsInstaller; import validatePackageName = JsTyping.validatePackageName; import PackageNameValidationResult = JsTyping.PackageNameValidationResult; - import FileOrFolder = ts.TestFSWithWatch.FileOrFolder; interface InstallerParams { globalTypingsCacheLocation?: string; @@ -45,15 +44,10 @@ namespace ts.projectSystem { } } - function executeCommand(self: Installer, host: fakes.FakeServerHost, installedTypings: string[] | string, typingFiles: FileOrFolder[], cb: TI.RequestCompletedAction): void { + function executeCommand(self: Installer, host: fakes.FakeServerHost, installedTypings: string[] | string, typingFiles: { path: string, content: string }[], cb: TI.RequestCompletedAction): void { self.addPostExecAction(installedTypings, success => { for (const file of typingFiles) { - if (typeof file.content === "string") { - host.vfs.addFile(file.path, file.content, { overwrite: true }); - } - else { - host.vfs.addDirectory(file.path); - } + host.vfs.writeFile(file.path, file.content); } cb(success); }); @@ -629,7 +623,7 @@ namespace ts.projectSystem { checkNumberOfProjects(projectService, { configuredProjects: 1 }); const p = configuredProjectAt(projectService, 0); checkProjectActualFiles(p, [app.path, jsconfig.path]); - ts.TestFSWithWatch.checkWatchedFiles(host, [jsconfig.path, "/bower_components", "/node_modules", "/.ts/lib.d.ts"]); + host.checkWatchedFiles([jsconfig.path, "/bower_components", "/node_modules", "/.ts/lib.d.ts"]); installer.installAll(/*expectedCount*/ 1); diff --git a/src/harness/utils.ts b/src/harness/utils.ts index 961aafd09a5..ebea8c7f5f3 100644 --- a/src/harness/utils.ts +++ b/src/harness/utils.ts @@ -103,4 +103,11 @@ namespace utils { } return indentation; } + + export function checkFileNames(caption: string, actualFileNames: ReadonlyArray, expectedFileNames: string[]) { + assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${expectedFileNames}, got ${actualFileNames}`); + for (const f of expectedFileNames) { + assert.isTrue(actualFileNames.indexOf(f) >= 0, `${caption}: expected to find ${f} in ${actualFileNames}`); + } + } } \ No newline at end of file diff --git a/src/harness/virtualFileSystemWithWatch.ts b/src/harness/virtualFileSystemWithWatch.ts deleted file mode 100644 index 70785d0cb4d..00000000000 --- a/src/harness/virtualFileSystemWithWatch.ts +++ /dev/null @@ -1,791 +0,0 @@ -/// -/// - -// TODO(rbuckton): Migrate this to use vfs. - -namespace ts.TestFSWithWatch { - export const libFile: FileOrFolder = { - path: "/.ts/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 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; - } - - interface FSEntry { - path: Path; - fullPath: string; - } - - interface File extends FSEntry { - content: string; - fileSize?: number; - } - - interface Folder extends FSEntry { - entries: FSEntry[]; - } - - 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 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 ${expectedFileNames}, got ${actualFileNames}`); - for (const f of expectedFileNames) { - assert.isTrue(contains(actualFileNames, f), `${caption}: expected to find ${f} in ${actualFileNames}`); - } - } - - function checkSortedSet(set: ReadonlySet, values: ReadonlyArray) { - assert.strictEqual(set.size, values.length, `Actual: ${Array.from(set)}, expected: ${values}.`); - for (const value of values) { - assert.isTrue(set.has(value)); - } - } - - export function checkWatchedFiles(host: TestServerHost | fakes.FakeServerHost, expectedFiles: string[]) { - if (host instanceof fakes.FakeServerHost) { - return checkSortedSet(host.vfs.watchedFiles, expectedFiles); - } - - checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles); - } - - export function checkWatchedDirectories(host: TestServerHost | fakes.FakeServerHost, expectedDirectories: string[], recursive = false) { - if (host instanceof fakes.FakeServerHost) { - return checkSortedSet(recursive ? host.vfs.watchedRecursiveDirectories : host.vfs.watchedNonRecursiveDirectories, expectedDirectories); - } - checkMapKeys(`watchedDirectories${recursive ? " recursive" : ""}`, recursive ? host.watchedDirectoriesRecursive : host.watchedDirectories, expectedDirectories); - } - - export function checkOutputContains(host: TestServerHost | fakes.FakeServerHost, 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(flatMapIter(mapExpected.keys(), key => key))} in ${JSON.stringify(host.getOutput())}`); - } - - export function checkOutputDoesNotContain(host: TestServerHost | fakes.FakeServerHost, 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; - } - - function createFileMatcher(host: TestServerHost, path: string) { - path = host.getCanonicalFileName(host.getHostSpecificPath(path)); - return (file: FileOrFolder) => host.getCanonicalFileName(host.getHostSpecificPath(file.path)) === path && isString(file.content); - } - - function createFolderMatcher(host: TestServerHost, path: string) { - path = host.getCanonicalFileName(host.getHostSpecificPath(path)); - return (file: FileOrFolder) => host.getCanonicalFileName(host.getHostSpecificPath(file.path)) === path && !isString(file.content); - } - - export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost { - 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(); - - readonly watchedDirectories = createMultiMap(); - readonly watchedDirectoriesRecursive = createMultiMap(); - readonly watchedFiles = createMultiMap(); - private readonly executingFilePath: string; - private readonly currentDirectory: string; - public files: FileOrFolder[] = []; - - // temporary vfs shim - public vfs = { - watchFiles: true, - watchDirectories: true, - addFile: (path: string, content: string, options?: { overwrite?: boolean }) => { - let file = this.files.find(createFileMatcher(this, path)); - if (file) { - if (!(options && options.overwrite)) return undefined; - file.content = content; - } - else { - file = { path, content }; - this.files.push(file); - } - this.reloadFS(this.files); - return file; - }, - writeFile: (path: string, content: string) => { - let file = this.files.find(createFileMatcher(this, path)); - if (file) { - file.content = content; - } - else { - file = { path, content }; - this.files.push(file); - } - this.reloadFS(this.files); - }, - getFile: (path: string) => { - return this.files.find(createFileMatcher(this, path)); - }, - removeFile: (path: string) => { - const index = this.files.findIndex(createFileMatcher(this, path)); - if (index >= 0) { - ts.orderedRemoveItemAt(this.files, index); - this.reloadFS(this.files); - } - }, - rename: (oldpath: string, newpath: string) => { - if (this.vfs.getFile(oldpath)) { - const oldIndex = this.files.findIndex(createFileMatcher(this, oldpath)); - if (oldIndex >= 0) { - const oldItem = this.files[oldIndex]; - ts.orderedRemoveItemAt(this.files, oldIndex); - const newIndex = this.files.findIndex(createFileMatcher(this, newpath)); - if (newIndex >= 0) { - ts.orderedRemoveItemAt(this.files, newIndex); - } - this.files.push({ path: newpath, content: oldItem.content }); - } - } - else { - for (let i = 0; i < this.files.length; i++) { - const file = this.files[i]; - if (vpath.beneath(oldpath, file.path, !this.useCaseSensitiveFileNames)) { - this.files[i] = { - path: vpath.combine(newpath, vpath.relative(oldpath, file.path, !this.useCaseSensitiveFileNames)), - content: file.content - }; - } - } - } - this.reloadFS(this.files); - }, - addDirectory: (path: string) => { - let file = this.files.find(createFolderMatcher(this, path)); - if (!file) { - file = { path }; - this.files.push(file); - this.reloadFS(this.files); - } - return file; - }, - removeDirectory: (path: string) => { - const index = this.files.findIndex(createFolderMatcher(this, path)); - if (index >= 0) { - ts.orderedRemoveItemAt(this.files, index); - this.reloadFS(this.files); - } - } - }; - - 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; - if (this.files !== fileOrFolderList) { - this.files = 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 - } - } - 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) || 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 { - 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, ignoreWatch?: boolean) { - folder.entries.push(fileOrDirectory); - this.fs.set(fileOrDirectory.path, fileOrDirectory); - - if (ignoreWatch) { - return; - } - if (isFile(fileOrDirectory)) { - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Created); - } - this.invokeDirectoryWatcher(folder.fullPath, fileOrDirectory.fullPath); - } - - private removeFileOrFolder(fileOrDirectory: File | Folder, 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)) { - this.invokeFileWatcher(fileOrDirectory.fullPath, FileWatcherEventKind.Deleted); - } - else { - Debug.assert(fileOrDirectory.entries.length === 0 || isRenaming); - if (this.vfs.watchDirectories) { - 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) { - if (!this.vfs.watchFiles) return; - const callbacks = this.watchedFiles.get(this.toPath(fileFullPath)); - invokeWatcherCallbacks(callbacks, ({ cb, fileName }) => cb(fileName, 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) { - if (!this.vfs.watchDirectories) return; - 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) { - if (!this.vfs.watchDirectories) return; - 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 toFolder(path: string): Folder { - const fullPath = getNormalizedAbsolutePath(path, this.currentDirectory); - return { - path: this.toPath(fullPath), - entries: [], - fullPath - }; - } - - fileExists(s: string) { - const path = this.toFullPath(s); - return isFile(this.fs.get(path)); - } - - readFile(s: string) { - const fsEntry = this.fs.get(this.toFullPath(s)); - return isFile(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 isFolder(this.fs.get(path)); - } - - getDirectories(s: string) { - const path = this.toFullPath(s); - const folder = this.fs.get(path); - if (isFolder(folder)) { - return mapDefined(folder.entries, entry => 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(this.toNormalizedAbsolutePath(path), extensions, exclude, include, this.useCaseSensitiveFileNames, this.getCurrentDirectory(), depth, (dir) => { - const directories: string[] = []; - const files: string[] = []; - const dirEntry = this.fs.get(this.toPath(dir)); - if (isFolder(dirEntry)) { - dirEntry.entries.forEach((entry) => { - if (isFolder(entry)) { - directories.push(getBaseFileName(entry.fullPath)); - } - else if (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); - } - - 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); - } - - 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); - } - - 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; - } - -} \ No newline at end of file diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 0d98d798c0e..e81ddd5e325 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -537,6 +537,10 @@ namespace ts.server { } } + updateTypingsForProject(response: SetTypings | InvalidateCachedTypings): void; + /** @internal */ + // tslint:disable-next-line:unified-signatures + updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes): void; updateTypingsForProject(response: SetTypings | InvalidateCachedTypings | BeginInstallTypes | EndInstallTypes): void { const project = this.findProject(response.projectName); if (!project) {