• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/* eslint-disable local/boolean-trivia */
2namespace ts.projectSystem {
3    describe("unittests:: tsserver:: webServer", () => {
4        class TestWorkerSession extends server.WorkerSession {
5            constructor(host: server.ServerHost, webHost: server.HostWithWriteMessage, options: Partial<server.StartSessionOptions>, logger: server.Logger) {
6                super(
7                    host,
8                    webHost,
9                    {
10                        globalPlugins: undefined,
11                        pluginProbeLocations: undefined,
12                        allowLocalPluginLoads: undefined,
13                        useSingleInferredProject: true,
14                        useInferredProjectPerProjectRoot: false,
15                        suppressDiagnosticEvents: false,
16                        noGetErrOnBackgroundUpdate: true,
17                        syntaxOnly: undefined,
18                        serverMode: undefined,
19                        ...options
20                    },
21                    logger,
22                    server.nullCancellationToken,
23                    () => emptyArray
24                );
25            }
26
27            getProjectService() {
28                return this.projectService;
29            }
30        }
31
32        function setup(logLevel: server.LogLevel | undefined, options?: Partial<server.StartSessionOptions>, importPlugin?: server.ServerHost["importPlugin"]) {
33            const host = createServerHost([libFile], { windowsStyleRoot: "c:/" });
34            const messages: any[] = [];
35            const webHost: server.WebHost = {
36                readFile: s => host.readFile(s),
37                fileExists: s => host.fileExists(s),
38                writeMessage: s => messages.push(s),
39            };
40            const webSys = server.createWebSystem(webHost, emptyArray, () => host.getExecutingFilePath());
41            webSys.importPlugin = importPlugin;
42            const logger = logLevel !== undefined ? new server.MainProcessLogger(logLevel, webHost) : nullLogger();
43            const session = new TestWorkerSession(webSys, webHost, { serverMode: LanguageServiceMode.PartialSemantic, ...options }, logger);
44            return { getMessages: () => messages, clearMessages: () => messages.length = 0, session };
45
46        }
47
48        describe("open files are added to inferred project and semantic operations succeed", () => {
49            function verify(logLevel: server.LogLevel | undefined) {
50                const { session, clearMessages, getMessages } = setup(logLevel);
51                const service = session.getProjectService();
52                const file: File = {
53                    path: "^memfs:/sample-folder/large.ts",
54                    content: "export const numberConst = 10; export const arrayConst: Array<string> = [];"
55                };
56                session.executeCommand({
57                    seq: 1,
58                    type: "request",
59                    command: protocol.CommandTypes.Open,
60                    arguments: {
61                        file: file.path,
62                        fileContent: file.content
63                    }
64                });
65                checkNumberOfProjects(service, { inferredProjects: 1 });
66                const project = service.inferredProjects[0];
67                checkProjectActualFiles(project, ["/lib.d.ts", file.path]); // Lib files are rooted
68                verifyQuickInfo();
69                verifyGotoDefInLib();
70
71                function verifyQuickInfo() {
72                    clearMessages();
73                    const start = protocolFileLocationFromSubstring(file, "numberConst");
74                    session.onMessage({
75                        seq: 2,
76                        type: "request",
77                        command: protocol.CommandTypes.Quickinfo,
78                        arguments: start
79                    });
80                    assert.deepEqual(last(getMessages()), {
81                        seq: 0,
82                        type: "response",
83                        command: protocol.CommandTypes.Quickinfo,
84                        request_seq: 2,
85                        success: true,
86                        performanceData: undefined,
87                        body: {
88                            kind: ScriptElementKind.constElement,
89                            kindModifiers: "export",
90                            start: { line: start.line, offset: start.offset },
91                            end: { line: start.line, offset: start.offset + "numberConst".length },
92                            displayString: "const numberConst: 10",
93                            documentation: "",
94                            tags: []
95                        }
96                    });
97                    verifyLogger();
98                }
99
100                function verifyGotoDefInLib() {
101                    clearMessages();
102                    const start = protocolFileLocationFromSubstring(file, "Array");
103                    session.onMessage({
104                        seq: 3,
105                        type: "request",
106                        command: protocol.CommandTypes.DefinitionAndBoundSpan,
107                        arguments: start
108                    });
109                    assert.deepEqual(last(getMessages()), {
110                        seq: 0,
111                        type: "response",
112                        command: protocol.CommandTypes.DefinitionAndBoundSpan,
113                        request_seq: 3,
114                        success: true,
115                        performanceData: undefined,
116                        body: {
117                            definitions: [{
118                                file: "/lib.d.ts",
119                                ...protocolTextSpanWithContextFromSubstring({
120                                    fileText: libFile.content,
121                                    text: "Array",
122                                    contextText: "interface Array<T> { length: number; [n: number]: T; }"
123                                })
124                            }],
125                            textSpan: {
126                                start: { line: start.line, offset: start.offset },
127                                end: { line: start.line, offset: start.offset + "Array".length },
128                            }
129                        }
130                    });
131                    verifyLogger();
132                }
133
134                function verifyLogger() {
135                    const messages = getMessages();
136                    assert.equal(messages.length, logLevel === server.LogLevel.verbose ? 4 : 1, `Expected ${JSON.stringify(messages)}`);
137                    if (logLevel === server.LogLevel.verbose) {
138                        verifyLogMessages(messages[0], "info");
139                        verifyLogMessages(messages[1], "perf");
140                        verifyLogMessages(messages[2], "info");
141                    }
142                    clearMessages();
143                }
144
145                function verifyLogMessages(actual: any, expectedLevel: server.MessageLogLevel) {
146                    assert.equal(actual.type, "log");
147                    assert.equal(actual.level, expectedLevel);
148                }
149            }
150
151            it("with logging enabled", () => {
152                verify(server.LogLevel.verbose);
153            });
154
155            it("with logging disabled", () => {
156                verify(/*logLevel*/ undefined);
157            });
158        });
159
160        describe("async loaded plugins", () => {
161            it("plugins are not loaded immediately", async () => {
162                let pluginModuleInstantiated = false;
163                let pluginInvoked = false;
164                const importPlugin = async (_root: string, _moduleName: string): Promise<server.ModuleImportResult> => {
165                    await Promise.resolve(); // simulate at least a single turn delay
166                    pluginModuleInstantiated = true;
167                    return {
168                        module: (() => {
169                            pluginInvoked = true;
170                            return { create: info => info.languageService };
171                        }) as server.PluginModuleFactory,
172                        error: undefined
173                    };
174                };
175
176                const { session } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importPlugin);
177                const projectService = session.getProjectService();
178
179                session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
180
181                // This should be false because `executeCommand` should have already triggered
182                // plugin enablement asynchronously and there are no plugin enablements currently
183                // being processed.
184                expect(projectService.hasNewPluginEnablementRequests()).eq(false);
185
186                // Should be true because async imports have already been triggered in the background
187                expect(projectService.hasPendingPluginEnablements()).eq(true);
188
189                // Should be false because resolution of async imports happens in a later turn.
190                expect(pluginModuleInstantiated).eq(false);
191
192                await projectService.waitForPendingPlugins();
193
194                // at this point all plugin modules should have been instantiated and all plugins
195                // should have been invoked
196                expect(pluginModuleInstantiated).eq(true);
197                expect(pluginInvoked).eq(true);
198            });
199
200            it("plugins evaluation in correct order even if imports resolve out of order", async () => {
201                const pluginADeferred = Utils.defer();
202                const pluginBDeferred = Utils.defer();
203                const log: string[] = [];
204                const importPlugin = async (_root: string, moduleName: string): Promise<server.ModuleImportResult> => {
205                    log.push(`request import ${moduleName}`);
206                    const promise = moduleName === "plugin-a" ? pluginADeferred.promise : pluginBDeferred.promise;
207                    await promise;
208                    log.push(`fulfill import ${moduleName}`);
209                    return {
210                        module: (() => {
211                            log.push(`invoke plugin ${moduleName}`);
212                            return { create: info => info.languageService };
213                        }) as server.PluginModuleFactory,
214                        error: undefined
215                    };
216                };
217
218                const { session } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a", "plugin-b"] }, importPlugin);
219                const projectService = session.getProjectService();
220
221                session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
222
223                // wait a turn
224                await Promise.resolve();
225
226                // resolve imports out of order
227                pluginBDeferred.resolve();
228                pluginADeferred.resolve();
229
230                // wait for load to complete
231                await projectService.waitForPendingPlugins();
232
233                expect(log).to.deep.equal([
234                    "request import plugin-a",
235                    "request import plugin-b",
236                    "fulfill import plugin-b",
237                    "fulfill import plugin-a",
238                    "invoke plugin plugin-a",
239                    "invoke plugin plugin-b",
240                ]);
241            });
242
243            it("sends projectsUpdatedInBackground event", async () => {
244                const importPlugin = async (_root: string, _moduleName: string): Promise<server.ModuleImportResult> => {
245                    await Promise.resolve(); // simulate at least a single turn delay
246                    return {
247                        module: (() => ({ create: info => info.languageService })) as server.PluginModuleFactory,
248                        error: undefined
249                    };
250                };
251
252                const { session, getMessages } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importPlugin);
253                const projectService = session.getProjectService();
254
255                session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
256
257                await projectService.waitForPendingPlugins();
258
259                expect(getMessages()).to.deep.equal([{
260                    seq: 0,
261                    type: "event",
262                    event: "projectsUpdatedInBackground",
263                    body: {
264                        openFiles: ["^memfs:/foo.ts"]
265                    }
266                }]);
267            });
268
269            it("adds external files", async () => {
270                const pluginAShouldLoad = Utils.defer();
271                const pluginAExternalFilesRequested = Utils.defer();
272
273                const importPlugin = async (_root: string, _moduleName: string): Promise<server.ModuleImportResult> => {
274                    // wait until the initial external files are requested from the project service.
275                    await pluginAShouldLoad.promise;
276
277                    return {
278                        module: (() => ({
279                            create: info => info.languageService,
280                            getExternalFiles: () => {
281                                // signal that external files have been requested by the project service.
282                                pluginAExternalFilesRequested.resolve();
283                                return ["external.txt"];
284                            }
285                        })) as server.PluginModuleFactory,
286                        error: undefined
287                    };
288                };
289
290                const { session } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importPlugin);
291                const projectService = session.getProjectService();
292
293                session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
294
295                const project = projectService.inferredProjects[0];
296
297                // get the external files we know about before plugins are loaded
298                const initialExternalFiles = project.getExternalFiles();
299
300                // we've ready the initial set of external files, allow the plugin to continue loading.
301                pluginAShouldLoad.resolve();
302
303                // wait for plugins
304                await projectService.waitForPendingPlugins();
305
306                // wait for the plugin's external files to be requested
307                await pluginAExternalFilesRequested.promise;
308
309                // get the external files we know aobut after plugins are loaded
310                const pluginExternalFiles = project.getExternalFiles();
311
312                expect(initialExternalFiles).to.deep.equal([]);
313                expect(pluginExternalFiles).to.deep.equal(["external.txt"]);
314            });
315
316            it("project is closed before plugins are loaded", async () => {
317                const pluginALoaded = Utils.defer();
318                const projectClosed = Utils.defer();
319                const importPlugin = async (_root: string, _moduleName: string): Promise<server.ModuleImportResult> => {
320                    // mark that the plugin has started loading
321                    pluginALoaded.resolve();
322
323                    // wait until after a project close has been requested to continue
324                    await projectClosed.promise;
325                    return {
326                        module: (() => ({ create: info => info.languageService })) as server.PluginModuleFactory,
327                        error: undefined
328                    };
329                };
330
331                const { session, getMessages } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importPlugin);
332                const projectService = session.getProjectService();
333
334                session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
335
336                // wait for the plugin to start loading
337                await pluginALoaded.promise;
338
339                // close the project
340                session.executeCommand({ seq: 2, type: "request", command: protocol.CommandTypes.Close, arguments: { file: "^memfs:/foo.ts" } });
341
342                // continue loading the plugin
343                projectClosed.resolve();
344
345                await projectService.waitForPendingPlugins();
346
347                // the project was closed before plugins were ready. no project update should have been requested
348                expect(getMessages()).not.to.deep.equal([{
349                    seq: 0,
350                    type: "event",
351                    event: "projectsUpdatedInBackground",
352                    body: {
353                        openFiles: ["^memfs:/foo.ts"]
354                    }
355                }]);
356            });
357        });
358    });
359}
360