Skip to content
Prev Previous commit
Next Next commit
Add tests for async service plugin timing
  • Loading branch information
rbuckton committed May 25, 2022
commit 39dac60da9461a297fbf000049477ed66fa6e13b
16 changes: 14 additions & 2 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4086,6 +4086,16 @@ namespace ts.server {
project.endEnablePlugin(project.beginEnablePluginSync(pluginConfigEntry, searchPaths, pluginConfigOverrides));
Comment thread
rbuckton marked this conversation as resolved.
}

/* @internal */
hasNewPluginEnablementRequests() {
return !!this.pendingPluginEnablements;
}

/* @internal */
hasPendingPluginEnablements() {
return !!this.currentPluginEnablementPromise;
}

/**
* Waits for any ongoing plugin enablement requests to complete.
*/
Expand All @@ -4107,8 +4117,10 @@ namespace ts.server {
}

private async enableRequestedPluginsAsync() {
// If we're already enabling plugins, wait for any existing operations to complete
await this.waitForPendingPlugins();
if (this.currentPluginEnablementPromise) {
// If we're already enabling plugins, wait for any existing operations to complete
await this.waitForPendingPlugins();
}

// Skip if there are no new plugin enablement requests
if (!this.pendingPluginEnablements) {
Expand Down
119 changes: 117 additions & 2 deletions src/testRunner/unittests/tsserver/webServer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable boolean-trivia */
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really needed and cant be fixed? If it cant be fixed can you pls not disable this for the whole file. instead wherever its needed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rule is flagged on every expect(...).eq(true) (or false) in the file. It felt like enough individual exceptions that it was worth disabling for the whole file, and labeling every call with .eq(/*value*/ true) seemed like overkill.

namespace ts.projectSystem {
describe("unittests:: tsserver:: webServer", () => {
class TestWorkerSession extends server.WorkerSession {
Expand Down Expand Up @@ -27,7 +28,8 @@ namespace ts.projectSystem {
return this.projectService;
}
}
function setup(logLevel: server.LogLevel | undefined) {

function setup(logLevel: server.LogLevel | undefined, options?: Partial<server.StartSessionOptions>, importServicePlugin?: server.ServerHost["importServicePlugin"]) {
const host = createServerHost([libFile], { windowsStyleRoot: "c:/" });
const messages: any[] = [];
const webHost: server.WebHost = {
Expand All @@ -36,8 +38,9 @@ namespace ts.projectSystem {
writeMessage: s => messages.push(s),
};
const webSys = server.createWebSystem(webHost, emptyArray, () => host.getExecutingFilePath());
if (importServicePlugin) webSys.importServicePlugin = importServicePlugin;
Comment thread
rbuckton marked this conversation as resolved.
Outdated
const logger = logLevel !== undefined ? new server.MainProcessLogger(logLevel, webHost) : nullLogger();
const session = new TestWorkerSession(webSys, webHost, { serverMode: LanguageServiceMode.PartialSemantic }, logger);
const session = new TestWorkerSession(webSys, webHost, { serverMode: LanguageServiceMode.PartialSemantic, ...options }, logger);
return { getMessages: () => messages, clearMessages: () => messages.length = 0, session };

}
Expand Down Expand Up @@ -153,5 +156,117 @@ namespace ts.projectSystem {
verify(/*logLevel*/ undefined);
});
});

describe("async loaded plugins", () => {
it("plugins are not loaded immediately", async () => {
let pluginModuleInstantiated = false;
let pluginInvoked = false;
const importServicePlugin = async (_root: string, _moduleName: string): Promise<server.ImportPluginResult> => {
await Promise.resolve(); // simulate at least a single turn delay
pluginModuleInstantiated = true;
return {
module: (() => {
pluginInvoked = true;
return { create: info => info.languageService };
}) as server.PluginModuleFactory,
error: undefined
};
};

const { session } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importServicePlugin);
const projectService = session.getProjectService();

session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });

// This should be false because `executeCommand` should have already triggered
// plugin enablement asynchronously and there are no plugin enablements currently
// being processed.
expect(projectService.hasNewPluginEnablementRequests()).eq(false);

// Should be true because async imports have already been triggered in the background
expect(projectService.hasPendingPluginEnablements()).eq(true);

// Should be false because resolution of async imports happens in a later turn.
expect(pluginModuleInstantiated).eq(false);

await projectService.waitForPendingPlugins();

// at this point all plugin modules should have been instantiated and all plugins
// should have been invoked
expect(pluginModuleInstantiated).eq(true);
expect(pluginInvoked).eq(true);
});

it("plugins evaluation in correct order even if imports resolve out of order", async () => {
let resolvePluginA!: () => void;
let resolvePluginB!: () => void;
const pluginAPromise = new Promise<void>(_resolve => resolvePluginA = _resolve);
const pluginBPromise = new Promise<void>(_resolve => resolvePluginB = _resolve);
const log: string[] = [];
const importServicePlugin = async (_root: string, moduleName: string): Promise<server.ImportPluginResult> => {
log.push(`request import ${moduleName}`);
const promise = moduleName === "plugin-a" ? pluginAPromise : pluginBPromise;
await promise;
log.push(`fulfill import ${moduleName}`);
return {
module: (() => {
log.push(`invoke plugin ${moduleName}`);
return { create: info => info.languageService };
}) as server.PluginModuleFactory,
error: undefined
};
};

const { session } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a", "plugin-b"] }, importServicePlugin);
const projectService = session.getProjectService();

session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });

// wait a turn
await Promise.resolve();

// resolve imports out of order
resolvePluginB();
resolvePluginA();

// wait for load to complete
await projectService.waitForPendingPlugins();

expect(log).to.deep.equal([
"request import plugin-a",
"request import plugin-b",
"fulfill import plugin-b",
"fulfill import plugin-a",
"invoke plugin plugin-a",
"invoke plugin plugin-b",
]);
});

it("sends projectsUpdatedInBackground event", async () => {
const importServicePlugin = async (_root: string, _moduleName: string): Promise<server.ImportPluginResult> => {
await Promise.resolve(); // simulate at least a single turn delay
return {
module: (() => ({ create: info => info.languageService })) as server.PluginModuleFactory,
error: undefined
};
};

const { session, getMessages } = setup(/*logLevel*/ undefined, { globalPlugins: ["plugin-a"] }, importServicePlugin);
const projectService = session.getProjectService();

session.executeCommand({ seq: 1, type: "request", command: protocol.CommandTypes.Open, arguments: { file: "^memfs:/foo.ts", content: "" } });
Comment thread
rbuckton marked this conversation as resolved.

await projectService.waitForPendingPlugins();
Comment thread
rbuckton marked this conversation as resolved.

expect(getMessages()).to.deep.equal([{
seq: 0,
type: "event",
event: "projectsUpdatedInBackground",
body: {
openFiles: ["^memfs:/foo.ts"]
}
}]);
});
});
});
}