Skip to content

Commit 7e00d7e

Browse files
committed
Merge pull request #8931 from Microsoft/tsserver-projectsystem-tests
initial revision of unit test support for project system in tsserver
2 parents ef0f6c8 + 92177be commit 7e00d7e

File tree

3 files changed

+297
-2
lines changed

3 files changed

+297
-2
lines changed

Diff for: Jakefile.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,8 @@ var harnessSources = harnessCoreSources.concat([
153153
"tsconfigParsing.ts",
154154
"commandLineParsing.ts",
155155
"convertCompilerOptionsFromJson.ts",
156-
"convertTypingOptionsFromJson.ts"
156+
"convertTypingOptionsFromJson.ts",
157+
"tsserverProjectSystem.ts"
157158
].map(function (f) {
158159
return path.join(unittestsDirectory, f);
159160
})).concat([

Diff for: src/server/editorServices.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,7 @@ namespace ts.server {
11381138
else {
11391139
this.log("No config files found.");
11401140
}
1141-
return {};
1141+
return configFileName ? { configFileName } : {};
11421142
}
11431143

11441144
/**

Diff for: tests/cases/unittests/tsserverProjectSystem.ts

+294
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
/// <reference path="..\..\..\src\harness\harness.ts" />
2+
3+
namespace ts {
4+
function notImplemented(): any {
5+
throw new Error("Not yet implemented");
6+
}
7+
8+
const nullLogger: server.Logger = {
9+
close: () => void 0,
10+
isVerbose: () => void 0,
11+
loggingEnabled: () => false,
12+
perftrc: () => void 0,
13+
info: () => void 0,
14+
startGroup: () => void 0,
15+
endGroup: () => void 0,
16+
msg: () => void 0
17+
};
18+
19+
const { content: libFileContent } = Harness.getDefaultLibraryFile(Harness.IO);
20+
21+
function getExecutingFilePathFromLibFile(libFile: FileOrFolder): string {
22+
return combinePaths(getDirectoryPath(libFile.path), "tsc.js");
23+
}
24+
25+
interface FileOrFolder {
26+
path: string;
27+
content?: string;
28+
}
29+
30+
interface FSEntry {
31+
path: Path;
32+
fullPath: string;
33+
}
34+
35+
interface File extends FSEntry {
36+
content: string;
37+
}
38+
39+
interface Folder extends FSEntry {
40+
entries: FSEntry[];
41+
}
42+
43+
function isFolder(s: FSEntry): s is Folder {
44+
return isArray((<Folder>s).entries);
45+
}
46+
47+
function isFile(s: FSEntry): s is File {
48+
return typeof (<File>s).content === "string";
49+
}
50+
51+
function addFolder(fullPath: string, toPath: (s: string) => Path, fs: FileMap<FSEntry>): Folder {
52+
const path = toPath(fullPath);
53+
if (fs.contains(path)) {
54+
Debug.assert(isFolder(fs.get(path)));
55+
return (<Folder>fs.get(path));
56+
}
57+
58+
const entry: Folder = { path, entries: [], fullPath };
59+
fs.set(path, entry);
60+
61+
const baseFullPath = getDirectoryPath(fullPath);
62+
if (fullPath !== baseFullPath) {
63+
addFolder(baseFullPath, toPath, fs).entries.push(entry);
64+
}
65+
66+
return entry;
67+
}
68+
69+
function sizeOfMap(map: Map<any>): number {
70+
let n = 0;
71+
for (const name in map) {
72+
if (hasProperty(map, name)) {
73+
n++;
74+
}
75+
}
76+
return n;
77+
}
78+
79+
function checkMapKeys(caption: string, map: Map<any>, expectedKeys: string[]) {
80+
assert.equal(sizeOfMap(map), expectedKeys.length, `${caption}: incorrect size of map`);
81+
for (const name of expectedKeys) {
82+
assert.isTrue(hasProperty(map, name), `${caption} is expected to contain ${name}, actual keys: ${getKeys(map)}`);
83+
}
84+
}
85+
86+
function checkFileNames(caption: string, actualFileNames: string[], expectedFileNames: string[]) {
87+
assert.equal(actualFileNames.length, expectedFileNames.length, `${caption}: incorrect actual number of files, expected ${JSON.stringify(expectedFileNames)}, got ${actualFileNames}`);
88+
for (const f of expectedFileNames) {
89+
assert.isTrue(contains(actualFileNames, f), `${caption}: expected to find ${f} in ${JSON.stringify(actualFileNames)}`);
90+
}
91+
}
92+
93+
function readDirectory(folder: FSEntry, ext: string, excludes: Path[], result: string[]): void {
94+
if (!folder || !isFolder(folder) || contains(excludes, folder.path)) {
95+
return;
96+
}
97+
for (const entry of folder.entries) {
98+
if (contains(excludes, entry.path)) {
99+
continue;
100+
}
101+
if (isFolder(entry)) {
102+
readDirectory(entry, ext, excludes, result);
103+
}
104+
else if (fileExtensionIs(entry.path, ext)) {
105+
result.push(entry.fullPath);
106+
}
107+
}
108+
}
109+
110+
class TestServerHost implements server.ServerHost {
111+
args: string[] = [];
112+
newLine: "\n";
113+
114+
private fs: ts.FileMap<FSEntry>;
115+
private getCanonicalFileName: (s: string) => string;
116+
private toPath: (f: string) => Path;
117+
readonly watchedDirectories: Map<{ cb: DirectoryWatcherCallback, recursive: boolean }[]> = {};
118+
readonly watchedFiles: Map<FileWatcherCallback[]> = {};
119+
120+
constructor(public useCaseSensitiveFileNames: boolean, private executingFilePath: string, private currentDirectory: string, fileOrFolderList: FileOrFolder[]) {
121+
this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames);
122+
this.toPath = s => toPath(s, currentDirectory, this.getCanonicalFileName);
123+
124+
this.reloadFS(fileOrFolderList);
125+
}
126+
127+
reloadFS(filesOrFolders: FileOrFolder[]) {
128+
this.fs = createFileMap<FSEntry>();
129+
for (const fileOrFolder of filesOrFolders) {
130+
const path = this.toPath(fileOrFolder.path);
131+
const fullPath = getNormalizedAbsolutePath(fileOrFolder.path, this.currentDirectory);
132+
if (typeof fileOrFolder.content === "string") {
133+
const entry = { path, content: fileOrFolder.content, fullPath };
134+
this.fs.set(path, entry);
135+
addFolder(getDirectoryPath(fullPath), this.toPath, this.fs).entries.push(entry);
136+
}
137+
else {
138+
addFolder(fullPath, this.toPath, this.fs);
139+
}
140+
}
141+
}
142+
143+
fileExists(s: string) {
144+
const path = this.toPath(s);
145+
return this.fs.contains(path) && isFile(this.fs.get(path));
146+
};
147+
148+
directoryExists(s: string) {
149+
const path = this.toPath(s);
150+
return this.fs.contains(path) && isFolder(this.fs.get(path));
151+
}
152+
153+
getDirectories(s: string) {
154+
const path = this.toPath(s);
155+
if (!this.fs.contains(path)) {
156+
return [];
157+
}
158+
else {
159+
const entry = this.fs.get(path);
160+
return isFolder(entry) ? map(entry.entries, x => getBaseFileName(x.fullPath)) : [];
161+
}
162+
}
163+
164+
readDirectory(path: string, ext: string, excludes: string[]): string[] {
165+
const result: string[] = [];
166+
readDirectory(this.fs.get(this.toPath(path)), ext, map(excludes, e => toPath(e, path, this.getCanonicalFileName)), result);
167+
return result;
168+
}
169+
170+
watchDirectory(directoryName: string, callback: DirectoryWatcherCallback, recursive: boolean): DirectoryWatcher {
171+
const path = this.toPath(directoryName);
172+
const callbacks = lookUp(this.watchedDirectories, path) || (this.watchedDirectories[path] = []);
173+
callbacks.push({ cb: callback, recursive });
174+
return {
175+
referenceCount: 0,
176+
directoryName,
177+
close: () => {
178+
for (let i = 0; i < callbacks.length; i++) {
179+
if (callbacks[i].cb === callback) {
180+
callbacks.splice(i, 1);
181+
break;
182+
}
183+
}
184+
if (!callbacks.length) {
185+
delete this.watchedDirectories[path];
186+
}
187+
}
188+
};
189+
}
190+
191+
watchFile(fileName: string, callback: FileWatcherCallback) {
192+
const path = this.toPath(fileName);
193+
const callbacks = lookUp(this.watchedFiles, path) || (this.watchedFiles[path] = []);
194+
callbacks.push(callback);
195+
return {
196+
close: () => {
197+
const i = callbacks.indexOf(callback);
198+
callbacks.splice(i, 1);
199+
if (!callbacks.length) {
200+
delete this.watchedFiles[path];
201+
}
202+
}
203+
};
204+
}
205+
206+
// TOOD: record and invoke callbacks to simulate timer events
207+
readonly setTimeout = (callback: (...args: any[]) => void, ms: number, ...args: any[]): any => void 0;
208+
readonly clearTimeout = (timeoutId: any): void => void 0;
209+
readonly readFile = (s: string) => (<File>this.fs.get(this.toPath(s))).content;
210+
readonly resolvePath = (s: string) => s;
211+
readonly getExecutingFilePath = () => this.executingFilePath;
212+
readonly getCurrentDirectory = () => this.currentDirectory;
213+
readonly writeFile = (path: string, content: string) => notImplemented();
214+
readonly write = (s: string) => notImplemented();
215+
readonly createDirectory = (s: string) => notImplemented();
216+
readonly exit = () => notImplemented();
217+
}
218+
219+
describe("tsserver project system:", () => {
220+
it("create inferred project", () => {
221+
const appFile: FileOrFolder = {
222+
path: "/a/b/c/app.ts",
223+
content: `
224+
import {f} from "./module"
225+
console.log(f)
226+
`
227+
};
228+
const libFile: FileOrFolder = {
229+
path: "/a/lib/lib.d.ts",
230+
content: libFileContent
231+
};
232+
const moduleFile: FileOrFolder = {
233+
path: "/a/b/c/module.d.ts",
234+
content: `export let x: number`
235+
};
236+
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [appFile, moduleFile, libFile]);
237+
const projectService = new server.ProjectService(host, nullLogger);
238+
const { configFileName } = projectService.openClientFile(appFile.path);
239+
240+
assert(!configFileName, `should not find config, got: '${configFileName}`);
241+
assert.equal(projectService.inferredProjects.length, 1, "expected one inferred project");
242+
assert.equal(projectService.configuredProjects.length, 0, "expected no configured project");
243+
244+
const project = projectService.inferredProjects[0];
245+
246+
checkFileNames("inferred project", project.getFileNames(), [appFile.path, libFile.path, moduleFile.path]);
247+
checkMapKeys("watchedDirectories", host.watchedDirectories, ["/a/b/c", "/a/b", "/a"]);
248+
});
249+
250+
it("create configured project without file list", () => {
251+
const configFile: FileOrFolder = {
252+
path: "/a/b/tsconfig.json",
253+
content: `
254+
{
255+
"compilerOptions": {},
256+
"exclude": [
257+
"e"
258+
]
259+
}`
260+
};
261+
const libFile: FileOrFolder = {
262+
path: "/a/lib/lib.d.ts",
263+
content: libFileContent
264+
};
265+
const file1: FileOrFolder = {
266+
path: "/a/b/c/f1.ts",
267+
content: "let x = 1"
268+
};
269+
const file2: FileOrFolder = {
270+
path: "/a/b/d/f2.ts",
271+
content: "let y = 1"
272+
};
273+
const file3: FileOrFolder = {
274+
path: "/a/b/e/f3.ts",
275+
content: "let z = 1"
276+
};
277+
const host = new TestServerHost(/*useCaseSensitiveFileNames*/ false, getExecutingFilePathFromLibFile(libFile), "/", [ configFile, libFile, file1, file2, file3 ]);
278+
const projectService = new server.ProjectService(host, nullLogger);
279+
const { configFileName, configFileErrors } = projectService.openClientFile(file1.path);
280+
281+
assert(configFileName, "should find config file");
282+
assert.isTrue(!configFileErrors, `expect no errors in config file, got ${JSON.stringify(configFileErrors)}`);
283+
assert.equal(projectService.inferredProjects.length, 0, "expected no inferred project");
284+
assert.equal(projectService.configuredProjects.length, 1, "expected one configured project");
285+
286+
const project = projectService.configuredProjects[0];
287+
checkFileNames("configuredProjects project, actualFileNames", project.getFileNames(), [file1.path, libFile.path, file2.path]);
288+
checkFileNames("configuredProjects project, rootFileNames", project.getRootFiles(), [file1.path, file2.path]);
289+
290+
checkMapKeys("watchedFiles", host.watchedFiles, [configFile.path, file2.path, libFile.path]); // watching all files except one that was open
291+
checkMapKeys("watchedDirectories", host.watchedDirectories, [getDirectoryPath(configFile.path)]);
292+
});
293+
});
294+
}

0 commit comments

Comments
 (0)