Skip to content

Commit 9ac5886

Browse files
authored
Add back tests for async plugin that got deleted as part of #51699 (#54019)
1 parent 5ad4ac8 commit 9ac5886

File tree

8 files changed

+578
-1
lines changed

8 files changed

+578
-1
lines changed

src/testRunner/tests.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ import "./unittests/tsserver/openFile";
170170
import "./unittests/tsserver/packageJsonInfo";
171171
import "./unittests/tsserver/partialSemanticServer";
172172
import "./unittests/tsserver/plugins";
173+
import "./unittests/tsserver/pluginsAsync";
173174
import "./unittests/tsserver/projectErrors";
174175
import "./unittests/tsserver/projectReferenceCompileOnSave";
175176
import "./unittests/tsserver/projectReferenceErrors";

src/testRunner/unittests/helpers/virtualFileSystemWithWatch.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,8 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
292292
private readonly environmentVariables?: Map<string, string>;
293293
private readonly executingFilePath: string;
294294
private readonly currentDirectory: string;
295-
public require: ((initialPath: string, moduleName: string) => ModuleImportResult) | undefined;
295+
require?: (initialPath: string, moduleName: string) => ModuleImportResult;
296+
importPlugin?: (root: string, moduleName: string) => Promise<ModuleImportResult>;
296297
public storeFilesChangingSignatureDuringEmit = true;
297298
watchFile: HostWatchFile;
298299
private inodeWatching: boolean | undefined;
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import * as ts from "../../_namespaces/ts";
2+
import { defer } from "../../_namespaces/Utils";
3+
import {
4+
baselineTsserverLogs,
5+
closeFilesForSession,
6+
createLoggerWithInMemoryLogs,
7+
createSession,
8+
openFilesForSession,
9+
} from "../helpers/tsserver";
10+
import { createServerHost, libFile } from "../helpers/virtualFileSystemWithWatch";
11+
12+
describe("unittests:: tsserver:: pluginsAsync:: async loaded plugins", () => {
13+
function setup(globalPlugins: string[]) {
14+
const host = createServerHost([libFile]);
15+
const session = createSession(host, { canUseEvents: true, globalPlugins, logger: createLoggerWithInMemoryLogs(host) });
16+
return { host, session };
17+
}
18+
19+
it("plugins are not loaded immediately", async () => {
20+
const { host, session } = setup(["plugin-a"]);
21+
let pluginModuleInstantiated = false;
22+
let pluginInvoked = false;
23+
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
24+
await Promise.resolve(); // simulate at least a single turn delay
25+
pluginModuleInstantiated = true;
26+
return {
27+
module: (() => {
28+
pluginInvoked = true;
29+
return { create: info => info.languageService };
30+
}) as ts.server.PluginModuleFactory,
31+
error: undefined
32+
};
33+
};
34+
35+
openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
36+
const projectService = session.getProjectService();
37+
38+
session.logger.log(`This should be false because 'executeCommand' should have already triggered plugin enablement asynchronously and there are no plugin enablements currently being processed`);
39+
session.logger.log(`hasNewPluginEnablementRequests:: ${projectService.hasNewPluginEnablementRequests()}`);
40+
41+
session.logger.log(`Should be true because async imports have already been triggered in the background`);
42+
session.logger.log(`hasPendingPluginEnablements:: ${projectService.hasPendingPluginEnablements()}`);
43+
44+
session.logger.log(`Should be false because resolution of async imports happens in a later turn`);
45+
session.logger.log(`pluginModuleInstantiated:: ${pluginModuleInstantiated}`);
46+
47+
await projectService.waitForPendingPlugins();
48+
49+
session.logger.log(`at this point all plugin modules should have been instantiated and all plugins should have been invoked`);
50+
session.logger.log(`pluginModuleInstantiated:: ${pluginModuleInstantiated}`);
51+
session.logger.log(`pluginInvoked:: ${pluginInvoked}`);
52+
53+
baselineTsserverLogs("pluginsAsync", "plugins are not loaded immediately", session);
54+
});
55+
56+
it("plugins evaluation in correct order even if imports resolve out of order", async () => {
57+
const { host, session } = setup(["plugin-a", "plugin-b"]);
58+
const pluginADeferred = defer();
59+
const pluginBDeferred = defer();
60+
host.importPlugin = async (_root: string, moduleName: string): Promise<ts.server.ModuleImportResult> => {
61+
session.logger.log(`request import ${moduleName}`);
62+
const promise = moduleName === "plugin-a" ? pluginADeferred.promise : pluginBDeferred.promise;
63+
await promise;
64+
session.logger.log(`fulfill import ${moduleName}`);
65+
return {
66+
module: (() => {
67+
session.logger.log(`invoke plugin ${moduleName}`);
68+
return { create: info => info.languageService };
69+
}) as ts.server.PluginModuleFactory,
70+
error: undefined
71+
};
72+
};
73+
74+
openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
75+
const projectService = session.getProjectService();
76+
77+
// wait a turn
78+
await Promise.resolve();
79+
80+
// resolve imports out of order
81+
pluginBDeferred.resolve();
82+
pluginADeferred.resolve();
83+
84+
// wait for load to complete
85+
await projectService.waitForPendingPlugins();
86+
87+
baselineTsserverLogs("pluginsAsync", "plugins evaluation in correct order even if imports resolve out of order", session);
88+
});
89+
90+
it("sends projectsUpdatedInBackground event", async () => {
91+
const { host, session } = setup(["plugin-a"]);
92+
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
93+
await Promise.resolve(); // simulate at least a single turn delay
94+
return {
95+
module: (() => ({ create: info => info.languageService })) as ts.server.PluginModuleFactory,
96+
error: undefined
97+
};
98+
};
99+
100+
openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
101+
const projectService = session.getProjectService();
102+
103+
104+
await projectService.waitForPendingPlugins();
105+
106+
baselineTsserverLogs("pluginsAsync", "sends projectsUpdatedInBackground event", session);
107+
});
108+
109+
it("adds external files", async () => {
110+
const { host, session } = setup(["plugin-a"]);
111+
const pluginAShouldLoad = defer();
112+
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
113+
// wait until the initial external files are requested from the project service.
114+
await pluginAShouldLoad.promise;
115+
116+
return {
117+
module: (() => ({
118+
create: info => info.languageService,
119+
getExternalFiles: () => ["external.txt"],
120+
})) as ts.server.PluginModuleFactory,
121+
error: undefined
122+
};
123+
};
124+
125+
openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
126+
const projectService = session.getProjectService();
127+
128+
const project = projectService.inferredProjects[0];
129+
130+
session.logger.log(`External files before plugin is loaded: ${project.getExternalFiles().join(",")}`);
131+
// we've ready the initial set of external files, allow the plugin to continue loading.
132+
pluginAShouldLoad.resolve();
133+
134+
// wait for plugins
135+
await projectService.waitForPendingPlugins();
136+
137+
host.runQueuedTimeoutCallbacks();
138+
139+
session.logger.log(`External files before plugin after plugin is loaded: ${project.getExternalFiles().join(",")}`);
140+
baselineTsserverLogs("pluginsAsync", "adds external files", session);
141+
});
142+
143+
it("project is closed before plugins are loaded", async () => {
144+
const { host, session } = setup(["plugin-a"]);
145+
const pluginALoaded = defer();
146+
const projectClosed = defer();
147+
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
148+
// mark that the plugin has started loading
149+
pluginALoaded.resolve();
150+
151+
// wait until after a project close has been requested to continue
152+
await projectClosed.promise;
153+
return {
154+
module: (() => ({ create: info => info.languageService })) as ts.server.PluginModuleFactory,
155+
error: undefined
156+
};
157+
};
158+
159+
openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
160+
const projectService = session.getProjectService();
161+
162+
163+
// wait for the plugin to start loading
164+
await pluginALoaded.promise;
165+
166+
// close the project
167+
closeFilesForSession(["^memfs:/foo.ts"], session);
168+
169+
// continue loading the plugin
170+
projectClosed.resolve();
171+
172+
await projectService.waitForPendingPlugins();
173+
174+
// the project was closed before plugins were ready. no project update should have been requested
175+
baselineTsserverLogs("pluginsAsync", "project is closed before plugins are loaded", session);
176+
});
177+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
currentDirectory:: / useCaseSensitiveFileNames: false
2+
Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist
3+
Before request
4+
//// [/a/lib/lib.d.ts]
5+
/// <reference no-default-lib="true"/>
6+
interface Boolean {}
7+
interface Function {}
8+
interface CallableFunction {}
9+
interface NewableFunction {}
10+
interface IArguments {}
11+
interface Number { toExponential: any; }
12+
interface Object {}
13+
interface RegExp {}
14+
interface String { charAt: any; }
15+
interface Array<T> { length: number; [n: number]: T; }
16+
17+
18+
Info seq [hh:mm:ss:mss] request:
19+
{
20+
"command": "open",
21+
"arguments": {
22+
"file": "^memfs:/foo.ts",
23+
"fileContent": ""
24+
},
25+
"seq": 1,
26+
"type": "request"
27+
}
28+
Info seq [hh:mm:ss:mss] Search path: ^memfs:
29+
Info seq [hh:mm:ss:mss] For info: ^memfs:/foo.ts :: No config files found.
30+
Info seq [hh:mm:ss:mss] Loading global plugin plugin-a
31+
Info seq [hh:mm:ss:mss] Enabling plugin plugin-a from candidate paths: /a/lib/tsc.js/../../..
32+
Info seq [hh:mm:ss:mss] Dynamically importing plugin-a from /a/lib/tsc.js/../../.. (resolved to /a/lib/tsc.js/../../../node_modules)
33+
Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /dev/null/inferredProject1*
34+
Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined WatchType: Closed Script info
35+
Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms
36+
Info seq [hh:mm:ss:mss] Project '/dev/null/inferredProject1*' (Inferred)
37+
Info seq [hh:mm:ss:mss] Files (2)
38+
/a/lib/lib.d.ts Text-1 "/// <reference no-default-lib=\"true\"/>\ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array<T> { length: number; [n: number]: T; }"
39+
^memfs:/foo.ts SVC-1-0 ""
40+
41+
42+
a/lib/lib.d.ts
43+
Default library for target 'es5'
44+
^memfs:/foo.ts
45+
Root file specified for compilation
46+
47+
Info seq [hh:mm:ss:mss] -----------------------------------------------
48+
Info seq [hh:mm:ss:mss] Project '/dev/null/inferredProject1*' (Inferred)
49+
Info seq [hh:mm:ss:mss] Files (2)
50+
51+
Info seq [hh:mm:ss:mss] -----------------------------------------------
52+
Info seq [hh:mm:ss:mss] Open files:
53+
Info seq [hh:mm:ss:mss] FileName: ^memfs:/foo.ts ProjectRootPath: undefined
54+
Info seq [hh:mm:ss:mss] Projects: /dev/null/inferredProject1*
55+
Info seq [hh:mm:ss:mss] response:
56+
{
57+
"responseRequired": false
58+
}
59+
After request
60+
61+
FsWatches::
62+
/a/lib/lib.d.ts: *new*
63+
{}
64+
65+
External files before plugin is loaded:
66+
Info seq [hh:mm:ss:mss] Plugin validation succeeded
67+
Info seq [hh:mm:ss:mss] Scheduled: /dev/null/inferredProject1*
68+
Info seq [hh:mm:ss:mss] got projects updated in background, updating diagnostics for ^memfs:/foo.ts
69+
Info seq [hh:mm:ss:mss] event:
70+
{"seq":0,"type":"event","event":"projectsUpdatedInBackground","body":{"openFiles":["^memfs:/foo.ts"]}}
71+
Before running Timeout callback:: count: 2
72+
1: /dev/null/inferredProject1*
73+
2: checkOne
74+
75+
Info seq [hh:mm:ss:mss] Running: /dev/null/inferredProject1*
76+
Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /dev/null/inferredProject1*
77+
Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 2 structureChanged: false structureIsReused:: Not Elapsed:: *ms
78+
Info seq [hh:mm:ss:mss] Same program as before
79+
Info seq [hh:mm:ss:mss] event:
80+
{"seq":0,"type":"event","event":"syntaxDiag","body":{"file":"^memfs:/foo.ts","diagnostics":[]}}
81+
After running Timeout callback:: count: 0
82+
83+
External files before plugin after plugin is loaded: external.txt
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
currentDirectory:: / useCaseSensitiveFileNames: false
2+
Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist
3+
Before request
4+
//// [/a/lib/lib.d.ts]
5+
/// <reference no-default-lib="true"/>
6+
interface Boolean {}
7+
interface Function {}
8+
interface CallableFunction {}
9+
interface NewableFunction {}
10+
interface IArguments {}
11+
interface Number { toExponential: any; }
12+
interface Object {}
13+
interface RegExp {}
14+
interface String { charAt: any; }
15+
interface Array<T> { length: number; [n: number]: T; }
16+
17+
18+
Info seq [hh:mm:ss:mss] request:
19+
{
20+
"command": "open",
21+
"arguments": {
22+
"file": "^memfs:/foo.ts",
23+
"fileContent": ""
24+
},
25+
"seq": 1,
26+
"type": "request"
27+
}
28+
Info seq [hh:mm:ss:mss] Search path: ^memfs:
29+
Info seq [hh:mm:ss:mss] For info: ^memfs:/foo.ts :: No config files found.
30+
Info seq [hh:mm:ss:mss] Loading global plugin plugin-a
31+
Info seq [hh:mm:ss:mss] Enabling plugin plugin-a from candidate paths: /a/lib/tsc.js/../../..
32+
Info seq [hh:mm:ss:mss] Dynamically importing plugin-a from /a/lib/tsc.js/../../.. (resolved to /a/lib/tsc.js/../../../node_modules)
33+
Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /dev/null/inferredProject1*
34+
Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined WatchType: Closed Script info
35+
Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms
36+
Info seq [hh:mm:ss:mss] Project '/dev/null/inferredProject1*' (Inferred)
37+
Info seq [hh:mm:ss:mss] Files (2)
38+
/a/lib/lib.d.ts Text-1 "/// <reference no-default-lib=\"true\"/>\ninterface Boolean {}\ninterface Function {}\ninterface CallableFunction {}\ninterface NewableFunction {}\ninterface IArguments {}\ninterface Number { toExponential: any; }\ninterface Object {}\ninterface RegExp {}\ninterface String { charAt: any; }\ninterface Array<T> { length: number; [n: number]: T; }"
39+
^memfs:/foo.ts SVC-1-0 ""
40+
41+
42+
a/lib/lib.d.ts
43+
Default library for target 'es5'
44+
^memfs:/foo.ts
45+
Root file specified for compilation
46+
47+
Info seq [hh:mm:ss:mss] -----------------------------------------------
48+
Info seq [hh:mm:ss:mss] Project '/dev/null/inferredProject1*' (Inferred)
49+
Info seq [hh:mm:ss:mss] Files (2)
50+
51+
Info seq [hh:mm:ss:mss] -----------------------------------------------
52+
Info seq [hh:mm:ss:mss] Open files:
53+
Info seq [hh:mm:ss:mss] FileName: ^memfs:/foo.ts ProjectRootPath: undefined
54+
Info seq [hh:mm:ss:mss] Projects: /dev/null/inferredProject1*
55+
Info seq [hh:mm:ss:mss] response:
56+
{
57+
"responseRequired": false
58+
}
59+
After request
60+
61+
FsWatches::
62+
/a/lib/lib.d.ts: *new*
63+
{}
64+
65+
This should be false because 'executeCommand' should have already triggered plugin enablement asynchronously and there are no plugin enablements currently being processed
66+
hasNewPluginEnablementRequests:: false
67+
Should be true because async imports have already been triggered in the background
68+
hasPendingPluginEnablements:: true
69+
Should be false because resolution of async imports happens in a later turn
70+
pluginModuleInstantiated:: false
71+
Info seq [hh:mm:ss:mss] Plugin validation succeeded
72+
Info seq [hh:mm:ss:mss] Scheduled: /dev/null/inferredProject1*
73+
Info seq [hh:mm:ss:mss] got projects updated in background, updating diagnostics for ^memfs:/foo.ts
74+
Info seq [hh:mm:ss:mss] event:
75+
{"seq":0,"type":"event","event":"projectsUpdatedInBackground","body":{"openFiles":["^memfs:/foo.ts"]}}
76+
at this point all plugin modules should have been instantiated and all plugins should have been invoked
77+
pluginModuleInstantiated:: true
78+
pluginInvoked:: true

0 commit comments

Comments
 (0)