Pull out parts of TI Adapter so we can test that more correctly instead of having to copy things (#56387)

This commit is contained in:
Sheetal Nandi
2023-11-14 10:33:54 -08:00
committed by GitHub
parent b970fa4ae5
commit e170bc59d4
115 changed files with 4861 additions and 1195 deletions
+3 -3
View File
@@ -12,7 +12,7 @@ import {
} from "./solutionBuilder";
import {
customTypesMap,
TestTypingsInstaller,
TestTypingsInstallerAdapter,
TestTypingsInstallerOptions,
} from "./typingsInstaller";
import {
@@ -103,13 +103,13 @@ export class TestSession extends ts.server.Session {
private seq = 0;
public override host!: TestSessionAndServiceHost;
public override logger!: LoggerWithInMemoryLogs;
public override readonly typingsInstaller!: TestTypingsInstaller;
public override readonly typingsInstaller!: TestTypingsInstallerAdapter;
public serverCancellationToken: TestServerCancellationToken;
constructor(optsOrHost: TestSessionConstructorOptions) {
const opts = getTestSessionPartialOptionsAndHost(optsOrHost);
opts.logger = opts.logger || createLoggerWithInMemoryLogs(opts.host);
const typingsInstaller = !opts.disableAutomaticTypingAcquisition ? new TestTypingsInstaller(opts) : undefined;
const typingsInstaller = !opts.disableAutomaticTypingAcquisition ? new TestTypingsInstallerAdapter(opts) : undefined;
const cancellationToken = opts.useCancellationToken ?
new TestServerCancellationToken(
opts.logger,
@@ -4,12 +4,6 @@ import {
} from "../../../harness/tsserverLogger";
import * as ts from "../../_namespaces/ts";
import {
ActionInvalidate,
ActionPackageInstalled,
ActionSet,
ActionWatchTypingLocations,
EventBeginInstallTypes,
EventEndInstallTypes,
stringifyIndented,
} from "../../_namespaces/ts.server";
import {
@@ -91,7 +85,7 @@ export type PendingInstallCallback = (
) => void;
export class TestTypingsInstallerWorker extends ts.server.typingsInstaller.TypingsInstaller {
readonly typesRegistry: Map<string, ts.MapLike<string>>;
constructor(readonly testTypingInstaller: TestTypingsInstaller) {
constructor(readonly testTypingInstaller: TestTypingsInstallerAdapter) {
const log = loggerToTypingsInstallerLog(testTypingInstaller.session.logger);
ts.Debug.assert(testTypingInstaller.session.host.patched);
testTypingInstaller.session.host.baselineHost("TI:: Creating typing installer");
@@ -175,17 +169,7 @@ export class TestTypingsInstallerWorker extends ts.server.typingsInstaller.Typin
sendResponse(response: ts.server.SetTypings | ts.server.InvalidateCachedTypings | ts.server.BeginInstallTypes | ts.server.EndInstallTypes | ts.server.WatchTypingLocations | ts.server.PackageInstalledResponse) {
this.log.writeLine(`Sending response:${stringifyIndented(response)}`);
this.testTypingInstaller.onResponse(response);
}
enqueueInstallTypingsRequest(project: ts.server.Project, typeAcquisition: ts.TypeAcquisition, unresolvedImports: ts.SortedReadonlyArray<string>) {
const request = ts.server.createInstallTypingsRequest(
project,
typeAcquisition,
unresolvedImports,
this.testTypingInstaller.globalTypingsCacheLocation,
);
this.install(request);
this.testTypingInstaller.handleMessage(response);
}
}
@@ -196,111 +180,50 @@ export interface TestTypingsInstallerOptions {
throttleLimit?: number;
installAction?: InstallAction;
typesRegistry?: string | readonly string[];
throttledRequests?: number;
}
export class TestTypingsInstaller implements ts.server.ITypingsInstaller {
protected projectService!: ts.server.ProjectService;
public installer!: TestTypingsInstallerWorker;
export class TestTypingsInstallerAdapter extends ts.server.TypingsInstallerAdapter {
worker: TestTypingsInstallerWorker | undefined;
session!: TestSession;
packageInstalledPromise: { resolve(value: ts.ApplyCodeActionCommandResult): void; reject(reason: unknown): void; } | undefined;
// Options
readonly globalTypingsCacheLocation: string;
readonly throttleLimit: number;
readonly installAction: InstallAction;
readonly typesRegistry: string | readonly string[] | undefined;
readonly throttledRequests: number | undefined;
constructor(options: TestTypingsInstallerOptions) {
this.globalTypingsCacheLocation = options.globalTypingsCacheLocation || options.host.getHostSpecificPath("/a/data");
const globalTypingsCacheLocation = options.globalTypingsCacheLocation || options.host.getHostSpecificPath("/a/data");
super(
/*telemetryEnabled*/ false,
options.throttledRequests === undefined ?
{ ...options.logger!, hasLevel: ts.returnFalse } :
options.logger!,
options.host,
globalTypingsCacheLocation,
(...args) => this.session.event(...args),
// Some large number so requests arent throttled
options.throttledRequests === undefined ? 10 : options.throttledRequests,
);
this.throttleLimit = options.throttleLimit || 5;
this.installAction = options.installAction !== undefined ? options.installAction : true;
this.typesRegistry = options.typesRegistry;
this.throttledRequests = options.throttledRequests;
}
isKnownTypesPackageName(name: string): boolean {
// We want to avoid looking this up in the registry as that is expensive. So first check that it's actually an NPM package.
const validationResult = ts.JsTyping.validatePackageName(name);
if (validationResult !== ts.JsTyping.NameValidationResult.Ok) {
return false;
protected override createInstallerProcess(): ts.server.TypingsInstallerWorkerProcess {
return {
send: req => (this.worker ??= new TestTypingsInstallerWorker(this)).handleRequest(req),
};
}
override scheduleRequest(request: ts.server.DiscoverTypings): void {
if (this.throttledRequests === undefined) {
this.activeRequestCount++;
this.installer.send(request);
}
return this.ensureInstaller().typesRegistry.has(name);
}
installPackage(options: ts.server.InstallPackageOptionsWithProject): Promise<ts.ApplyCodeActionCommandResult> {
this.ensureInstaller().installPackage({ kind: "installPackage", ...options });
ts.Debug.assert(this.packageInstalledPromise === undefined);
return new Promise<ts.ApplyCodeActionCommandResult>((resolve, reject) => {
this.packageInstalledPromise = { resolve, reject };
});
}
attach(projectService: ts.server.ProjectService) {
this.projectService = projectService;
}
onProjectClosed(p: ts.server.Project) {
this.installer?.closeProject({ projectName: p.getProjectName(), kind: "closeProject" });
}
enqueueInstallTypingsRequest(project: ts.server.Project, typeAcquisition: ts.TypeAcquisition, unresolvedImports: ts.SortedReadonlyArray<string>) {
this.ensureInstaller().enqueueInstallTypingsRequest(project, typeAcquisition, unresolvedImports);
}
private ensureInstaller() {
return this.installer ??= new TestTypingsInstallerWorker(this);
}
onResponse(response: ts.server.SetTypings | ts.server.InvalidateCachedTypings | ts.server.BeginInstallTypes | ts.server.EndInstallTypes | ts.server.WatchTypingLocations | ts.server.PackageInstalledResponse) {
switch (response.kind) {
case ActionPackageInstalled: {
const { success, message } = response;
if (success) {
this.packageInstalledPromise!.resolve({ successMessage: message });
}
else {
this.packageInstalledPromise!.reject(message);
}
this.packageInstalledPromise = undefined;
this.projectService.updateTypingsForProject(response);
// The behavior is the same as for setTypings, so send the same event.
this.session.event(response, "setTypings");
break;
}
case EventBeginInstallTypes: {
const body: ts.server.protocol.BeginInstallTypesEventBody = {
eventId: response.eventId,
packages: response.packagesToInstall,
};
const eventName: ts.server.protocol.BeginInstallTypesEventName = "beginInstallTypes";
this.session.event(body, eventName);
break;
}
case EventEndInstallTypes: {
const body: ts.server.protocol.EndInstallTypesEventBody = {
eventId: response.eventId,
packages: response.packagesToInstall,
success: response.installSuccess,
};
const eventName: ts.server.protocol.EndInstallTypesEventName = "endInstallTypes";
this.session.event(body, eventName);
break;
}
case ActionInvalidate: {
this.projectService.updateTypingsForProject(response);
break;
}
case ActionSet: {
this.projectService.updateTypingsForProject(response);
this.session.event(response, "setTypings");
break;
}
case ActionWatchTypingLocations:
this.projectService.watchTypingLocations(response);
break;
default:
ts.assertType<never>(response);
else {
super.scheduleRequest(request);
}
}
}
@@ -537,87 +537,242 @@ describe("unittests:: tsserver:: typingsInstaller:: General functionality", () =
baselineTsserverLogs("typingsInstaller", "throttle delayed typings to install", session);
});
it("Throttle - delayed run install requests", () => {
const lodashJs = {
path: "/a/b/lodash.js",
content: "",
};
const commanderJs = {
path: "/a/b/commander.js",
content: "",
};
const file3 = {
path: "/a/b/file3.d.ts",
content: "",
};
describe("throttled testing", () => {
function setup() {
const lodashJs = {
path: "/a/b/lodash.js",
content: "",
};
const commanderJs = {
path: "/a/b/commander.js",
content: "",
};
const file3 = {
path: "/a/b/file3.d.ts",
content: "",
};
const commander: FileWithPackageName = {
path: "/a/data/node_modules/@types/commander/index.d.ts",
content: "declare const commander: { x: number }",
package: "commander",
};
const jquery: FileWithPackageName = {
path: "/a/data/node_modules/@types/jquery/index.d.ts",
content: "declare const jquery: { x: number }",
package: "jquery",
};
const lodash: FileWithPackageName = {
path: "/a/data/node_modules/@types/lodash/index.d.ts",
content: "declare const lodash: { x: number }",
package: "lodash",
};
const cordova: FileWithPackageName = {
path: "/a/data/node_modules/@types/cordova/index.d.ts",
content: "declare const cordova: { x: number }",
package: "cordova",
};
const grunt: FileWithPackageName = {
path: "/a/data/node_modules/@types/grunt/index.d.ts",
content: "declare const grunt: { x: number }",
package: "grunt",
};
const gulp: FileWithPackageName = {
path: "/a/data/node_modules/@types/gulp/index.d.ts",
content: "declare const gulp: { x: number }",
package: "gulp",
};
const commander: FileWithPackageName = {
path: "/a/data/node_modules/@types/commander/index.d.ts",
content: "declare const commander: { x: number }",
package: "commander",
};
const jquery: FileWithPackageName = {
path: "/a/data/node_modules/@types/jquery/index.d.ts",
content: "declare const jquery: { x: number }",
package: "jquery",
};
const lodash: FileWithPackageName = {
path: "/a/data/node_modules/@types/lodash/index.d.ts",
content: "declare const lodash: { x: number }",
package: "lodash",
};
const cordova: FileWithPackageName = {
path: "/a/data/node_modules/@types/cordova/index.d.ts",
content: "declare const cordova: { x: number }",
package: "cordova",
};
const grunt: FileWithPackageName = {
path: "/a/data/node_modules/@types/grunt/index.d.ts",
content: "declare const grunt: { x: number }",
package: "grunt",
};
const gulp: FileWithPackageName = {
path: "/a/data/node_modules/@types/gulp/index.d.ts",
content: "declare const gulp: { x: number }",
package: "gulp",
};
const host = createServerHost([lodashJs, commanderJs, file3, customTypesMap]);
// Create project #1 with 4 typings
const session = new TestSession({
host,
installAction: [commander, jquery, lodash, cordova, grunt, gulp],
throttleLimit: 1,
typesRegistry: ["commander", "jquery", "lodash", "cordova", "gulp", "grunt"],
const host = createServerHost([lodashJs, commanderJs, file3, customTypesMap]);
return { lodashJs, commanderJs, file3, commander, jquery, lodash, cordova, grunt, gulp, host };
}
it("Throttle - delayed run install requests", () => {
const { lodashJs, commanderJs, file3, commander, jquery, lodash, cordova, grunt, gulp, host } = setup();
// Create project #1 with 4 typings
const session = new TestSession({
host,
installAction: [commander, jquery, lodash, cordova, grunt, gulp],
throttleLimit: 1,
typesRegistry: ["commander", "jquery", "lodash", "cordova", "gulp", "grunt"],
});
const projectFileName1 = "/a/app/test1.csproj";
openExternalProjectForSession({
projectFileName: projectFileName1,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(lodashJs.path), toExternalFile(commanderJs.path), toExternalFile(file3.path)],
typeAcquisition: { include: ["jquery", "cordova"] },
}, session);
// Create project #2 with 2 typings
const projectFileName2 = "/a/app/test2.csproj";
openExternalProjectForSession({
projectFileName: projectFileName2,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(file3.path)],
typeAcquisition: { include: ["grunt", "gulp"] },
}, session);
host.runPendingInstalls();
host.runPendingInstalls();
host.runQueuedTimeoutCallbacks(); // for 2 projects
baselineTsserverLogs("typingsInstaller", "throttle delayed run install requests", session);
});
const projectFileName1 = "/a/app/test1.csproj";
openExternalProjectForSession({
projectFileName: projectFileName1,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(lodashJs.path), toExternalFile(commanderJs.path), toExternalFile(file3.path)],
typeAcquisition: { include: ["jquery", "cordova"] },
}, session);
assert.equal(session.typingsInstaller.installer.pendingRunRequests.length, 0, "expect no throttled requests");
it("Throttle - scheduled run install requests without reaching limit", () => {
const { lodashJs, commanderJs, file3, commander, jquery, lodash, cordova, grunt, gulp, host } = setup();
// Create project #2 with 2 typings
const projectFileName2 = "/a/app/test2.csproj";
openExternalProjectForSession({
projectFileName: projectFileName2,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(file3.path)],
typeAcquisition: { include: ["grunt", "gulp"] },
}, session);
assert.equal(session.typingsInstaller.installer.pendingRunRequests.length, 1, "expect one throttled request");
const session = new TestSession({
host,
installAction: [commander, jquery, lodash, cordova, grunt, gulp],
throttledRequests: 1,
typesRegistry: ["commander", "jquery", "lodash", "cordova", "gulp", "grunt"],
});
const projectFileName1 = "/a/app/test1.csproj";
openExternalProjectForSession({
projectFileName: projectFileName1,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(lodashJs.path), toExternalFile(commanderJs.path), toExternalFile(file3.path)],
typeAcquisition: { include: ["jquery", "cordova"] },
}, session);
host.runPendingInstalls();
host.runQueuedTimeoutCallbacks(); // Send the request to worker for project1
host.runPendingInstalls(); // Actual install for project1
// expected one install request from the second project
assert.equal(session.typingsInstaller.installer.pendingRunRequests.length, 0, "expected no throttled requests");
const id = host.getNextTimeoutId();
const projectFileName2 = "/a/app/test2.csproj";
openExternalProjectForSession({
projectFileName: projectFileName2,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(file3.path)],
typeAcquisition: { include: ["grunt", "gulp"] },
}, session);
host.runPendingInstalls();
host.runQueuedTimeoutCallbacks(); // for 2 projects
baselineTsserverLogs("typingsInstaller", "throttle delayed run install requests", session);
host.runQueuedTimeoutCallbacks(id); // Send the request to worker for project2
host.runPendingInstalls(); // Actual install for project2
baselineTsserverLogs("typingsInstaller", "throttle scheduled run install requests without reaching limit", session);
});
it("Throttle - scheduled run install requests with defer", () => {
const { lodashJs, commanderJs, file3, commander, jquery, lodash, cordova, grunt, gulp, host } = setup();
const session = new TestSession({
host,
installAction: [commander, jquery, lodash, cordova, grunt, gulp],
throttledRequests: 1,
typesRegistry: ["commander", "jquery", "lodash", "cordova", "gulp", "grunt"],
});
const projectFileName1 = "/a/app/test1.csproj";
openExternalProjectForSession({
projectFileName: projectFileName1,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(lodashJs.path), toExternalFile(commanderJs.path), toExternalFile(file3.path)],
typeAcquisition: { include: ["jquery", "cordova"] },
}, session);
// this will be deferred
const projectFileName2 = "/a/app/test2.csproj";
openExternalProjectForSession({
projectFileName: projectFileName2,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(file3.path)],
typeAcquisition: { include: ["grunt", "gulp"] },
}, session);
const id = host.getNextTimeoutId();
host.runQueuedTimeoutCallbacks(); // Send the request to worker for project1
host.runPendingInstalls(); // Actual install for project1
host.runQueuedTimeoutCallbacks(id); // Send the request to worker for project2
host.runPendingInstalls(); // Actual install for project2
baselineTsserverLogs("typingsInstaller", "throttle scheduled run install requests with defer", session);
});
it("Throttle - scheduled run install requests with defer refreshed", () => {
const { lodashJs, commanderJs, file3, commander, jquery, lodash, cordova, grunt, gulp, host } = setup();
const session = new TestSession({
host,
installAction: [commander, jquery, lodash, cordova, grunt, gulp],
throttledRequests: 1,
typesRegistry: ["commander", "jquery", "lodash", "cordova", "gulp", "grunt"],
});
const projectFileName1 = "/a/app/test1.csproj";
openExternalProjectForSession({
projectFileName: projectFileName1,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(commanderJs.path), toExternalFile(file3.path)],
typeAcquisition: { include: ["jquery", "cordova"] },
}, session);
// Create project #2 with 2 typings - this will be deferred
const projectFileName2 = "/a/app/test2.csproj";
openExternalProjectForSession({
projectFileName: projectFileName2,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(file3.path)],
typeAcquisition: { include: ["grunt", "gulp"] },
}, session);
// Update project for 3 typings and this should be used instead of first one
openExternalProjectForSession({
projectFileName: projectFileName2,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(lodashJs.path), toExternalFile(file3.path)],
typeAcquisition: { include: ["grunt", "gulp"] },
}, session);
const id = host.getNextTimeoutId();
host.runQueuedTimeoutCallbacks(); // Send the request to worker for project1
host.runPendingInstalls(); // Actual install for project1
host.runQueuedTimeoutCallbacks(id); // Send the request to worker for project2
host.runPendingInstalls(); // Actual install for project2
baselineTsserverLogs("typingsInstaller", "throttle scheduled run install requests with defer refreshed", session);
});
it("Throttle - scheduled run install requests with defer while queuing again", () => {
const { lodashJs, commanderJs, file3, commander, jquery, lodash, cordova, grunt, gulp, host } = setup();
const session = new TestSession({
host,
installAction: [commander, jquery, lodash, cordova, grunt, gulp],
throttledRequests: 1,
typesRegistry: ["commander", "jquery", "lodash", "cordova", "gulp", "grunt"],
});
const projectFileName1 = "/a/app/test1.csproj";
openExternalProjectForSession({
projectFileName: projectFileName1,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(commanderJs.path), toExternalFile(file3.path)],
typeAcquisition: { include: ["jquery"] },
}, session);
const projectFileName2 = "/a/app/test2.csproj";
openExternalProjectForSession({
projectFileName: projectFileName2,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(file3.path)],
typeAcquisition: { include: ["grunt", "gulp"] },
}, session);
const projectFileName3 = "/a/app/test3.csproj";
openExternalProjectForSession({
projectFileName: projectFileName3,
options: { allowJS: true, moduleResolution: ts.ModuleResolutionKind.Node10 },
rootFiles: [toExternalFile(lodashJs.path), toExternalFile(file3.path)],
typeAcquisition: { include: ["cordova"] },
}, session);
const id = host.getNextTimeoutId();
host.runQueuedTimeoutCallbacks(); // Send the request to worker for project1
host.runPendingInstalls(); // Actual install for project1
const id2 = host.getNextTimeoutId();
host.runQueuedTimeoutCallbacks(id); // Send the request to worker for project2
host.runPendingInstalls(); // Actual install for project2
host.runQueuedTimeoutCallbacks(id2); // Send the request to worker for project3
host.runPendingInstalls(); // Actual install for project3
baselineTsserverLogs("typingsInstaller", "throttle scheduled run install requests with defer while queuing again", session);
});
});
it("configured scoped name projects discover from node_modules", () => {