Skip to content

Commit

Permalink
Add back tests for async plugin that got deleted as part of #51699
Browse files Browse the repository at this point in the history
  • Loading branch information
sheetalkamat committed Apr 25, 2023
1 parent 6c3239c commit 4e878d1
Show file tree
Hide file tree
Showing 8 changed files with 578 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/testRunner/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ import "./unittests/tsserver/openFile";
import "./unittests/tsserver/packageJsonInfo";
import "./unittests/tsserver/partialSemanticServer";
import "./unittests/tsserver/plugins";
import "./unittests/tsserver/pluginsAsync";
import "./unittests/tsserver/projectErrors";
import "./unittests/tsserver/projectReferenceCompileOnSave";
import "./unittests/tsserver/projectReferenceErrors";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,8 @@ export class TestServerHost implements server.ServerHost, FormatDiagnosticsHost,
private readonly environmentVariables?: Map<string, string>;
private readonly executingFilePath: string;
private readonly currentDirectory: string;
public require: ((initialPath: string, moduleName: string) => ModuleImportResult) | undefined;
require?: (initialPath: string, moduleName: string) => ModuleImportResult;
importPlugin?: (root: string, moduleName: string) => Promise<ModuleImportResult>;
public storeFilesChangingSignatureDuringEmit = true;
watchFile: HostWatchFile;
private inodeWatching: boolean | undefined;
Expand Down
177 changes: 177 additions & 0 deletions src/testRunner/unittests/tsserver/pluginsAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
import * as ts from "../../_namespaces/ts";
import { defer } from "../../_namespaces/Utils";
import {
baselineTsserverLogs,
closeFilesForSession,
createLoggerWithInMemoryLogs,
createSession,
openFilesForSession,
} from "../helpers/tsserver";
import { createServerHost, libFile } from "../helpers/virtualFileSystemWithWatch";

describe("unittests:: tsserver:: pluginsAsync:: async loaded plugins", () => {
function setup(globalPlugins: string[]) {
const host = createServerHost([libFile]);
const session = createSession(host, { canUseEvents: true, globalPlugins, logger: createLoggerWithInMemoryLogs(host) });
return { host, session };
}

it("plugins are not loaded immediately", async () => {
const { host, session } = setup(["plugin-a"]);
let pluginModuleInstantiated = false;
let pluginInvoked = false;
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
await Promise.resolve(); // simulate at least a single turn delay
pluginModuleInstantiated = true;
return {
module: (() => {
pluginInvoked = true;
return { create: info => info.languageService };
}) as ts.server.PluginModuleFactory,
error: undefined
};
};

openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
const projectService = session.getProjectService();

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`);
session.logger.log(`hasNewPluginEnablementRequests:: ${projectService.hasNewPluginEnablementRequests()}`);

session.logger.log(`Should be true because async imports have already been triggered in the background`);
session.logger.log(`hasPendingPluginEnablements:: ${projectService.hasPendingPluginEnablements()}`);

session.logger.log(`Should be false because resolution of async imports happens in a later turn`);
session.logger.log(`pluginModuleInstantiated:: ${pluginModuleInstantiated}`);

await projectService.waitForPendingPlugins();

session.logger.log(`at this point all plugin modules should have been instantiated and all plugins should have been invoked`);
session.logger.log(`pluginModuleInstantiated:: ${pluginModuleInstantiated}`);
session.logger.log(`pluginInvoked:: ${pluginInvoked}`);

baselineTsserverLogs("pluginsAsync", "plugins are not loaded immediately", session);
});

it("plugins evaluation in correct order even if imports resolve out of order", async () => {
const { host, session } = setup(["plugin-a", "plugin-b"]);
const pluginADeferred = defer();
const pluginBDeferred = defer();
host.importPlugin = async (_root: string, moduleName: string): Promise<ts.server.ModuleImportResult> => {
session.logger.log(`request import ${moduleName}`);
const promise = moduleName === "plugin-a" ? pluginADeferred.promise : pluginBDeferred.promise;
await promise;
session.logger.log(`fulfill import ${moduleName}`);
return {
module: (() => {
session.logger.log(`invoke plugin ${moduleName}`);
return { create: info => info.languageService };
}) as ts.server.PluginModuleFactory,
error: undefined
};
};

openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
const projectService = session.getProjectService();

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

// resolve imports out of order
pluginBDeferred.resolve();
pluginADeferred.resolve();

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

baselineTsserverLogs("pluginsAsync", "plugins evaluation in correct order even if imports resolve out of order", session);
});

it("sends projectsUpdatedInBackground event", async () => {
const { host, session } = setup(["plugin-a"]);
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
await Promise.resolve(); // simulate at least a single turn delay
return {
module: (() => ({ create: info => info.languageService })) as ts.server.PluginModuleFactory,
error: undefined
};
};

openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
const projectService = session.getProjectService();


await projectService.waitForPendingPlugins();

baselineTsserverLogs("pluginsAsync", "sends projectsUpdatedInBackground event", session);
});

it("adds external files", async () => {
const { host, session } = setup(["plugin-a"]);
const pluginAShouldLoad = defer();
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
// wait until the initial external files are requested from the project service.
await pluginAShouldLoad.promise;

return {
module: (() => ({
create: info => info.languageService,
getExternalFiles: () => ["external.txt"],
})) as ts.server.PluginModuleFactory,
error: undefined
};
};

openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
const projectService = session.getProjectService();

const project = projectService.inferredProjects[0];

session.logger.log(`External files before plugin is loaded: ${project.getExternalFiles().join(",")}`);
// we've ready the initial set of external files, allow the plugin to continue loading.
pluginAShouldLoad.resolve();

// wait for plugins
await projectService.waitForPendingPlugins();

host.runQueuedTimeoutCallbacks();

session.logger.log(`External files before plugin after plugin is loaded: ${project.getExternalFiles().join(",")}`);
baselineTsserverLogs("pluginsAsync", "adds external files", session);
});

it("project is closed before plugins are loaded", async () => {
const { host, session } = setup(["plugin-a"]);
const pluginALoaded = defer();
const projectClosed = defer();
host.importPlugin = async (_root: string, _moduleName: string): Promise<ts.server.ModuleImportResult> => {
// mark that the plugin has started loading
pluginALoaded.resolve();

// wait until after a project close has been requested to continue
await projectClosed.promise;
return {
module: (() => ({ create: info => info.languageService })) as ts.server.PluginModuleFactory,
error: undefined
};
};

openFilesForSession([{ file: "^memfs:/foo.ts", content: "" }], session);
const projectService = session.getProjectService();


// wait for the plugin to start loading
await pluginALoaded.promise;

// close the project
closeFilesForSession(["^memfs:/foo.ts"], session);

// continue loading the plugin
projectClosed.resolve();

await projectService.waitForPendingPlugins();

// the project was closed before plugins were ready. no project update should have been requested
baselineTsserverLogs("pluginsAsync", "project is closed before plugins are loaded", session);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
currentDirectory:: / useCaseSensitiveFileNames: false
Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist
Before request
//// [/a/lib/lib.d.ts]
/// <reference no-default-lib="true"/>
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> { length: number; [n: number]: T; }
Info seq [hh:mm:ss:mss] request:
{
"command": "open",
"arguments": {
"file": "^memfs:/foo.ts",
"fileContent": ""
},
"seq": 1,
"type": "request"
}
Info seq [hh:mm:ss:mss] Search path: ^memfs:
Info seq [hh:mm:ss:mss] For info: ^memfs:/foo.ts :: No config files found.
Info seq [hh:mm:ss:mss] Loading global plugin plugin-a
Info seq [hh:mm:ss:mss] Enabling plugin plugin-a from candidate paths: /a/lib/tsc.js/../../..
Info seq [hh:mm:ss:mss] Dynamically importing plugin-a from /a/lib/tsc.js/../../.. (resolved to /a/lib/tsc.js/../../../node_modules)
Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /dev/null/inferredProject1*
Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined WatchType: Closed Script info
Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms
Info seq [hh:mm:ss:mss] Project '/dev/null/inferredProject1*' (Inferred)
Info seq [hh:mm:ss:mss] Files (2)
/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; }"
^memfs:/foo.ts SVC-1-0 ""
a/lib/lib.d.ts
Default library for target 'es5'
^memfs:/foo.ts
Root file specified for compilation
Info seq [hh:mm:ss:mss] -----------------------------------------------
Info seq [hh:mm:ss:mss] Project '/dev/null/inferredProject1*' (Inferred)
Info seq [hh:mm:ss:mss] Files (2)

Info seq [hh:mm:ss:mss] -----------------------------------------------
Info seq [hh:mm:ss:mss] Open files:
Info seq [hh:mm:ss:mss] FileName: ^memfs:/foo.ts ProjectRootPath: undefined
Info seq [hh:mm:ss:mss] Projects: /dev/null/inferredProject1*
Info seq [hh:mm:ss:mss] response:
{
"responseRequired": false
}
After request

FsWatches::
/a/lib/lib.d.ts: *new*
{}

External files before plugin is loaded:
Info seq [hh:mm:ss:mss] Plugin validation succeeded
Info seq [hh:mm:ss:mss] Scheduled: /dev/null/inferredProject1*
Info seq [hh:mm:ss:mss] got projects updated in background, updating diagnostics for ^memfs:/foo.ts
Info seq [hh:mm:ss:mss] event:
{"seq":0,"type":"event","event":"projectsUpdatedInBackground","body":{"openFiles":["^memfs:/foo.ts"]}}
Before running Timeout callback:: count: 2
1: /dev/null/inferredProject1*
2: checkOne

Info seq [hh:mm:ss:mss] Running: /dev/null/inferredProject1*
Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /dev/null/inferredProject1*
Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 2 structureChanged: false structureIsReused:: Not Elapsed:: *ms
Info seq [hh:mm:ss:mss] Same program as before
Info seq [hh:mm:ss:mss] event:
{"seq":0,"type":"event","event":"syntaxDiag","body":{"file":"^memfs:/foo.ts","diagnostics":[]}}
After running Timeout callback:: count: 0

External files before plugin after plugin is loaded: external.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
currentDirectory:: / useCaseSensitiveFileNames: false
Info seq [hh:mm:ss:mss] Provided types map file "/a/lib/typesMap.json" doesn't exist
Before request
//// [/a/lib/lib.d.ts]
/// <reference no-default-lib="true"/>
interface Boolean {}
interface Function {}
interface CallableFunction {}
interface NewableFunction {}
interface IArguments {}
interface Number { toExponential: any; }
interface Object {}
interface RegExp {}
interface String { charAt: any; }
interface Array<T> { length: number; [n: number]: T; }
Info seq [hh:mm:ss:mss] request:
{
"command": "open",
"arguments": {
"file": "^memfs:/foo.ts",
"fileContent": ""
},
"seq": 1,
"type": "request"
}
Info seq [hh:mm:ss:mss] Search path: ^memfs:
Info seq [hh:mm:ss:mss] For info: ^memfs:/foo.ts :: No config files found.
Info seq [hh:mm:ss:mss] Loading global plugin plugin-a
Info seq [hh:mm:ss:mss] Enabling plugin plugin-a from candidate paths: /a/lib/tsc.js/../../..
Info seq [hh:mm:ss:mss] Dynamically importing plugin-a from /a/lib/tsc.js/../../.. (resolved to /a/lib/tsc.js/../../../node_modules)
Info seq [hh:mm:ss:mss] Starting updateGraphWorker: Project: /dev/null/inferredProject1*
Info seq [hh:mm:ss:mss] FileWatcher:: Added:: WatchInfo: /a/lib/lib.d.ts 500 undefined WatchType: Closed Script info
Info seq [hh:mm:ss:mss] Finishing updateGraphWorker: Project: /dev/null/inferredProject1* Version: 1 structureChanged: true structureIsReused:: Not Elapsed:: *ms
Info seq [hh:mm:ss:mss] Project '/dev/null/inferredProject1*' (Inferred)
Info seq [hh:mm:ss:mss] Files (2)
/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; }"
^memfs:/foo.ts SVC-1-0 ""
a/lib/lib.d.ts
Default library for target 'es5'
^memfs:/foo.ts
Root file specified for compilation
Info seq [hh:mm:ss:mss] -----------------------------------------------
Info seq [hh:mm:ss:mss] Project '/dev/null/inferredProject1*' (Inferred)
Info seq [hh:mm:ss:mss] Files (2)
Info seq [hh:mm:ss:mss] -----------------------------------------------
Info seq [hh:mm:ss:mss] Open files:
Info seq [hh:mm:ss:mss] FileName: ^memfs:/foo.ts ProjectRootPath: undefined
Info seq [hh:mm:ss:mss] Projects: /dev/null/inferredProject1*
Info seq [hh:mm:ss:mss] response:
{
"responseRequired": false
}
After request
FsWatches::
/a/lib/lib.d.ts: *new*
{}
This should be false because 'executeCommand' should have already triggered plugin enablement asynchronously and there are no plugin enablements currently being processed
hasNewPluginEnablementRequests:: false
Should be true because async imports have already been triggered in the background
hasPendingPluginEnablements:: true
Should be false because resolution of async imports happens in a later turn
pluginModuleInstantiated:: false
Info seq [hh:mm:ss:mss] Plugin validation succeeded
Info seq [hh:mm:ss:mss] Scheduled: /dev/null/inferredProject1*
Info seq [hh:mm:ss:mss] got projects updated in background, updating diagnostics for ^memfs:/foo.ts
Info seq [hh:mm:ss:mss] event:
{"seq":0,"type":"event","event":"projectsUpdatedInBackground","body":{"openFiles":["^memfs:/foo.ts"]}}
at this point all plugin modules should have been instantiated and all plugins should have been invoked
pluginModuleInstantiated:: true
pluginInvoked:: true
Loading

0 comments on commit 4e878d1

Please sign in to comment.