Merge pull request #9002 from zhengbli/moreTsserverTests

Add tests for tsserver project system
This commit is contained in:
Zhengbo Li 2016-06-07 21:56:09 -07:00
commit cdf4cded15
4 changed files with 300 additions and 22 deletions

View File

@ -2,7 +2,7 @@
namespace ts {
export type FileWatcherCallback = (fileName: string, removed?: boolean) => void;
export type DirectoryWatcherCallback = (directoryName: string) => void;
export type DirectoryWatcherCallback = (fileName: string) => void;
export interface WatchedFile {
fileName: string;
callback: FileWatcherCallback;

View File

@ -268,7 +268,7 @@ namespace ts.server {
}
removeRoot(info: ScriptInfo) {
if (!this.filenameToScript.contains(info.path)) {
if (this.filenameToScript.contains(info.path)) {
this.filenameToScript.remove(info.path);
this.roots = copyListRemovingItem(info, this.roots);
this.resolvedModuleNames.remove(info.path);

View File

@ -2919,6 +2919,7 @@ namespace ts {
const changesInCompilationSettingsAffectSyntax = oldSettings &&
(oldSettings.target !== newSettings.target ||
oldSettings.module !== newSettings.module ||
oldSettings.moduleResolution !== newSettings.moduleResolution ||
oldSettings.noResolve !== newSettings.noResolve ||
oldSettings.jsx !== newSettings.jsx ||
oldSettings.allowJs !== newSettings.allowJs);

View File

@ -107,6 +107,32 @@ namespace ts {
}
}
function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) {
assert.equal(projectService.configuredProjects.length, expected, `expected ${expected} configured project(s)`);
}
function checkNumberOfInferredProjects(projectService: server.ProjectService, expected: number) {
assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`);
}
function checkWatchedFiles(host: TestServerHost, expectedFiles: string[]) {
checkMapKeys("watchedFiles", host.watchedFiles, expectedFiles);
}
function checkWatchedDirectories(host: TestServerHost, expectedDirectories: string[]) {
checkMapKeys("watchedDirectories", host.watchedDirectories, expectedDirectories);
}
function checkConfiguredProjectActualFiles(project: server.Project, expectedFiles: string[]) {
checkFileNames("configuredProjects project, actualFileNames", project.getFileNames(), expectedFiles);
}
function checkConfiguredProjectRootFiles(project: server.Project, expectedFiles: string[]) {
checkFileNames("configuredProjects project, rootFileNames", project.getRootFiles(), expectedFiles);
}
type TimeOutCallback = () => any;
class TestServerHost implements server.ServerHost {
args: string[] = [];
newLine: "\n";
@ -114,6 +140,7 @@ namespace ts {
private fs: ts.FileMap<FSEntry>;
private getCanonicalFileName: (s: string) => string;
private toPath: (f: string) => Path;
private callbackQueue: TimeOutCallback[] = [];
readonly watchedDirectories: Map<{ cb: DirectoryWatcherCallback, recursive: boolean }[]> = {};
readonly watchedFiles: Map<FileWatcherCallback[]> = {};
@ -188,6 +215,26 @@ namespace ts {
};
}
triggerDirectoryWatcherCallback(directoryName: string, fileName: string): void {
const path = this.toPath(directoryName);
const callbacks = lookUp(this.watchedDirectories, path);
if (callbacks) {
for (const callback of callbacks) {
callback.cb(fileName);
}
}
}
triggerFileWatcherCallback(fileName: string, removed?: boolean): void {
const path = this.toPath(fileName);
const callbacks = lookUp(this.watchedFiles, path);
if (callbacks) {
for (const callback of callbacks) {
callback(path, removed);
}
}
}
watchFile(fileName: string, callback: FileWatcherCallback) {
const path = this.toPath(fileName);
const callbacks = lookUp(this.watchedFiles, path) || (this.watchedFiles[path] = []);
@ -204,8 +251,27 @@ namespace ts {
}
// TOOD: record and invoke callbacks to simulate timer events
readonly setTimeout = (callback: (...args: any[]) => void, ms: number, ...args: any[]): any => void 0;
readonly clearTimeout = (timeoutId: any): void => void 0;
readonly setTimeout = (callback: TimeOutCallback, time: number) => {
this.callbackQueue.push(callback);
return this.callbackQueue.length - 1;
};
readonly clearTimeout = (timeoutId: any): void => {
if (typeof timeoutId === "number") {
this.callbackQueue.splice(timeoutId, 1);
}
};
checkTimeoutQueueLength(expected: number) {
assert.equal(this.callbackQueue.length, expected, `expected ${expected} timeout callbacks queued but found ${this.callbackQueue.length}.`);
}
runQueuedTimeoutCallbacks() {
for (const callback of this.callbackQueue) {
callback();
}
this.callbackQueue = [];
}
readonly readFile = (s: string) => (<File>this.fs.get(this.toPath(s))).content;
readonly resolvePath = (s: string) => s;
readonly getExecutingFilePath = () => this.executingFilePath;
@ -216,7 +282,20 @@ namespace ts {
readonly exit = () => notImplemented();
}
describe("tsserver project system:", () => {
describe("tsserver-project-system", () => {
const commonFile1: FileOrFolder = {
path: "/a/b/commonFile1.ts",
content: "let x = 1"
};
const commonFile2: FileOrFolder = {
path: "/a/b/commonFile2.ts",
content: "let y = 1"
};
const libFile: FileOrFolder = {
path: "/a/lib/lib.d.ts",
content: libFileContent
};
it("create inferred project", () => {
const appFile: FileOrFolder = {
path: "/a/b/c/app.ts",
@ -225,10 +304,7 @@ namespace ts {
console.log(f)
`
};
const libFile: FileOrFolder = {
path: "/a/lib/lib.d.ts",
content: libFileContent
};
const moduleFile: FileOrFolder = {
path: "/a/b/c/module.d.ts",
content: `export let x: number`
@ -238,13 +314,13 @@ namespace ts {
const { configFileName } = projectService.openClientFile(appFile.path);
assert(!configFileName, `should not find config, got: '${configFileName}`);
assert.equal(projectService.inferredProjects.length, 1, "expected one inferred project");
assert.equal(projectService.configuredProjects.length, 0, "expected no configured project");
checkNumberOfConfiguredProjects(projectService, 0);
checkNumberOfInferredProjects(projectService, 1);
const project = projectService.inferredProjects[0];
checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]);
checkMapKeys("watchedDirectories", host.watchedDirectories, ["/a/b/c", "/a/b", "/a"]);
checkWatchedDirectories(host, ["/a/b/c", "/a/b", "/a"]);
});
it("create configured project without file list", () => {
@ -258,10 +334,6 @@ namespace ts {
]
}`
};
const libFile: FileOrFolder = {
path: "/a/lib/lib.d.ts",
content: libFileContent
};
const file1: FileOrFolder = {
path: "/a/b/c/f1.ts",
content: "let x = 1"
@ -274,21 +346,226 @@ namespace ts {
path: "/a/b/e/f3.ts",
content: "let z = 1"
};
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [ configFile, libFile, file1, file2, file3 ]);
const projectService = new server.ProjectService(host, nullLogger);
const { configFileName, configFileErrors } = projectService.openClientFile(file1.path);
assert(configFileName, "should find config file");
assert.isTrue(!configFileErrors, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`);
assert.equal(projectService.inferredProjects.length, 0, "expected no inferred project");
assert.equal(projectService.configuredProjects.length, 1, "expected one configured project");
checkNumberOfInferredProjects(projectService, 0);
checkNumberOfConfiguredProjects(projectService, 1);
const project = projectService.configuredProjects[0];
checkFileNames("configuredProjects project, actualFileNames", project.getFileNames(), [file1.path, libFile.path, file2.path]);
checkFileNames("configuredProjects project, rootFileNames", project.getRootFiles(), [file1.path, file2.path]);
checkConfiguredProjectActualFiles(project, [file1.path, libFile.path, file2.path]);
checkConfiguredProjectRootFiles(project, [file1.path, file2.path]);
// watching all files except one that was open
checkWatchedFiles(host, [configFile.path, file2.path, libFile.path]);
checkWatchedDirectories(host, [getDirectoryPath(configFile.path)]);
});
checkMapKeys("watchedFiles", host.watchedFiles, [configFile.path, file2.path, libFile.path]); // watching all files except one that was open
checkMapKeys("watchedDirectories", host.watchedDirectories, [getDirectoryPath(configFile.path)]);
it("add and then remove a config file in a folder with loose files", () => {
const configFile: FileOrFolder = {
path: "/a/b/tsconfig.json",
content: `{
"files": ["commonFile1.ts"]
}`
};
const filesWithoutConfig = [ libFile, commonFile1, commonFile2 ];
const filesWithConfig = [ libFile, commonFile1, commonFile2, configFile ];
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", filesWithoutConfig);
const projectService = new server.ProjectService(host, nullLogger);
projectService.openClientFile(commonFile1.path);
projectService.openClientFile(commonFile2.path);
checkNumberOfInferredProjects(projectService, 2);
checkWatchedDirectories(host, ["/a/b", "/a"]);
// Add a tsconfig file
host.reloadFS(filesWithConfig);
host.triggerDirectoryWatcherCallback("/a/b", configFile.path);
checkNumberOfInferredProjects(projectService, 1);
checkNumberOfConfiguredProjects(projectService, 1);
// watching all files except one that was open
checkWatchedFiles(host, [libFile.path, configFile.path]);
// remove the tsconfig file
host.reloadFS(filesWithoutConfig);
host.triggerFileWatcherCallback(configFile.path);
checkNumberOfInferredProjects(projectService, 2);
checkNumberOfConfiguredProjects(projectService, 0);
checkWatchedDirectories(host, ["/a/b", "/a"]);
});
it("add new files to a configured project without file list", () => {
const configFile: FileOrFolder = {
path: "/a/b/tsconfig.json",
content: `{}`
};
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, libFile, configFile]);
const projectService = new server.ProjectService(host, nullLogger);
projectService.openClientFile(commonFile1.path);
checkWatchedDirectories(host, ["/a/b"]);
checkNumberOfConfiguredProjects(projectService, 1);
const project = projectService.configuredProjects[0];
checkConfiguredProjectRootFiles(project, [commonFile1.path]);
// add a new ts file
host.reloadFS([commonFile1, commonFile2, libFile, configFile]);
host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path);
host.runQueuedTimeoutCallbacks();
// project service waits for 250ms to update the project structure, therefore the assertion needs to wait longer.
checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
});
it("should ignore non-existing files specified in the config file", () => {
const configFile: FileOrFolder = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {},
"files": [
"commonFile1.ts",
"commonFile3.ts"
]
}`
};
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, configFile]);
const projectService = new server.ProjectService(host, nullLogger);
projectService.openClientFile(commonFile1.path);
projectService.openClientFile(commonFile2.path);
checkNumberOfConfiguredProjects(projectService, 1);
const project = projectService.configuredProjects[0];
checkConfiguredProjectRootFiles(project, [commonFile1.path]);
checkNumberOfInferredProjects(projectService, 1);
});
it("handle recreated files correctly", () => {
const configFile: FileOrFolder = {
path: "/a/b/tsconfig.json",
content: `{}`
};
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, configFile]);
const projectService = new server.ProjectService(host, nullLogger);
projectService.openClientFile(commonFile1.path);
checkNumberOfConfiguredProjects(projectService, 1);
const project = projectService.configuredProjects[0];
checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
// delete commonFile2
host.reloadFS([commonFile1, configFile]);
host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path);
host.runQueuedTimeoutCallbacks();
checkConfiguredProjectRootFiles(project, [commonFile1.path]);
// re-add commonFile2
host.reloadFS([commonFile1, commonFile2, configFile]);
host.triggerDirectoryWatcherCallback("/a/b", commonFile2.path);
host.runQueuedTimeoutCallbacks();
checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
});
it("should create new inferred projects for files excluded from a configured project", () => {
const configFile: FileOrFolder = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {},
"files": ["${commonFile1.path}", "${commonFile2.path}"]
}`
};
const files = [commonFile1, commonFile2, configFile];
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", files);
const projectService = new server.ProjectService(host, nullLogger);
projectService.openClientFile(commonFile1.path);
const project = projectService.configuredProjects[0];
checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
configFile.content = `{
"compilerOptions": {},
"files": ["${commonFile1.path}"]
}`;
host.reloadFS(files);
host.triggerFileWatcherCallback(configFile.path);
checkNumberOfConfiguredProjects(projectService, 1);
checkConfiguredProjectRootFiles(project, [commonFile1.path]);
projectService.openClientFile(commonFile2.path);
checkNumberOfInferredProjects(projectService, 1);
});
it("files explicitly excluded in config file", () => {
const configFile: FileOrFolder = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {},
"exclude": ["/a/c"]
}`
};
const excludedFile1: FileOrFolder = {
path: "/a/c/excluedFile1.ts",
content: `let t = 1;`
};
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [commonFile1, commonFile2, excludedFile1, configFile]);
const projectService = new server.ProjectService(host, nullLogger);
projectService.openClientFile(commonFile1.path);
checkNumberOfConfiguredProjects(projectService, 1);
const project = projectService.configuredProjects[0];
checkConfiguredProjectRootFiles(project, [commonFile1.path, commonFile2.path]);
projectService.openClientFile(excludedFile1.path);
checkNumberOfInferredProjects(projectService, 1);
});
it("should properly handle module resolution changes in config file", () => {
const file1: FileOrFolder = {
path: "/a/b/file1.ts",
content: `import { T } from "module1";`
};
const nodeModuleFile: FileOrFolder = {
path: "/a/b/node_modules/module1.ts",
content: `export interface T {}`
};
const classicModuleFile: FileOrFolder = {
path: "/a/module1.ts",
content: `export interface T {}`
};
const configFile: FileOrFolder = {
path: "/a/b/tsconfig.json",
content: `{
"compilerOptions": {
"moduleResolution": "node"
},
"files": ["${file1.path}"]
}`
};
const files = [file1, nodeModuleFile, classicModuleFile, configFile];
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", files);
const projectService = new server.ProjectService(host, nullLogger);
projectService.openClientFile(file1.path);
projectService.openClientFile(nodeModuleFile.path);
projectService.openClientFile(classicModuleFile.path);
checkNumberOfConfiguredProjects(projectService, 1);
const project = projectService.configuredProjects[0];
checkConfiguredProjectActualFiles(project, [file1.path, nodeModuleFile.path]);
checkNumberOfInferredProjects(projectService, 1);
configFile.content = `{
"compilerOptions": {
"moduleResolution": "classic"
},
"files": ["${file1.path}"]
}`;
host.reloadFS(files);
host.triggerFileWatcherCallback(configFile.path);
checkConfiguredProjectActualFiles(project, [file1.path, classicModuleFile.path]);
checkNumberOfInferredProjects(projectService, 1);
});
});
}