Create baselines for tsserver event tests (#53330)

This commit is contained in:
Sheetal Nandi
2023-03-17 16:56:01 -07:00
committed by GitHub
parent bf369f1b95
commit 905a0b4e69
98 changed files with 19165 additions and 927 deletions

View File

@@ -267,9 +267,12 @@ function convertToLocation(lineAndCharacter: LineAndCharacter): protocol.Locatio
return { line: lineAndCharacter.line + 1, offset: lineAndCharacter.character + 1 };
}
function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: true): protocol.DiagnosticWithFileName;
function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: false): protocol.Diagnostic;
function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: boolean): protocol.Diagnostic | protocol.DiagnosticWithFileName {
/** @internal */
export function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: true): protocol.DiagnosticWithFileName;
/** @internal */
export function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: false): protocol.Diagnostic;
/** @internal */
export function formatDiagnosticToProtocol(diag: Diagnostic, includeFileName: boolean): protocol.Diagnostic | protocol.DiagnosticWithFileName {
const start = (diag.file && convertToLocation(getLineAndCharacterOfPosition(diag.file, diag.start!)))!; // TODO: GH#18217
const end = (diag.file && convertToLocation(getLineAndCharacterOfPosition(diag.file, diag.start! + diag.length!)))!; // TODO: GH#18217
const text = flattenDiagnosticMessageText(diag.messageText, "\n");

View File

@@ -21,7 +21,6 @@ import {
createLoggerWithInMemoryLogs,
createProjectService,
createSession,
createSessionWithEventTracking,
openFilesForSession,
verifyGetErrRequest,
} from "./helpers";
@@ -751,25 +750,22 @@ describe("unittests:: tsserver:: ConfiguredProjects", () => {
const originalGetFileSize = host.getFileSize;
host.getFileSize = (filePath: string) =>
filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
const { session, events } = createSessionWithEventTracking<ts.server.ProjectLanguageServiceStateEvent>(host, ts.server.ProjectLanguageServiceStateEvent);
const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) });
session.executeCommand({
seq: 0,
type: "request",
command: "open",
arguments: { file: f1.path }
} as ts.server.protocol.OpenRequest);
session.logger.log(`Language languageServiceEnabled:: ${session.getProjectService().configuredProjects.get(config.path)!.languageServiceEnabled}`);
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const project = configuredProjectAt(projectService, 0);
assert.isFalse(project.languageServiceEnabled, "Language service enabled");
assert.equal(events.length, 1, "should receive event");
assert.equal(events[0].data.project, project, "project name");
assert.isFalse(events[0].data.languageServiceEnabled, "Language service state");
const options = projectService.getFormatCodeOptions(f1.path as ts.server.NormalizedPath);
const edits = project.getLanguageService().getFormattingEditsForDocument(f1.path, options);
assert.deepEqual(edits, [{ span: ts.createTextSpan(/*start*/ 7, /*length*/ 3), newText: " " }]);
session.executeCommandSeq({
command: ts.server.protocol.CommandTypes.FormatFull,
arguments: {
file: f1.path,
}
});
baselineTsserverLogs("configuredProjects", "syntactic features work even if language service is disabled", session);
});
it("when multiple projects are open, detects correct default project", () => {
@@ -1263,7 +1259,7 @@ describe("unittests:: tsserver:: ConfiguredProjects:: when reading tsconfig file
};
const host = createServerHost([file1, libFile, configFile]);
const { session, events } = createSessionWithEventTracking<ts.server.ConfigFileDiagEvent>(host, ts.server.ConfigFileDiagEvent);
const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) });
const originalReadFile = host.readFile;
host.readFile = f => {
return f === configFile.path ?
@@ -1272,15 +1268,6 @@ describe("unittests:: tsserver:: ConfiguredProjects:: when reading tsconfig file
};
openFilesForSession([file1], session);
assert.deepEqual(events, [{
eventName: ts.server.ConfigFileDiagEvent,
data: {
triggerFile: file1.path,
configFileName: configFile.path,
diagnostics: [
ts.createCompilerDiagnostic(ts.Diagnostics.Cannot_read_file_0, configFile.path)
]
}
}]);
baselineTsserverLogs("configuredProjects", "should be tolerated without crashing the server", session);
});
});

View File

@@ -5,16 +5,18 @@ import {
libFile,
} from "../../virtualFileSystemWithWatch";
import {
checkNumberOfProjects,
checkProjectActualFiles,
createSessionWithEventTracking,
baselineTsserverLogs,
createLoggerWithInMemoryLogs,
createSession,
openFilesForSession,
} from "../helpers";
describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large file", () => {
function getFileType(useLargeTsFile: boolean) {
return useLargeTsFile ? "ts" : "js";
}
function getLargeFile(useLargeTsFile: boolean) {
return `src/large.${useLargeTsFile ? "ts" : "js"}`;
return `src/large.${getFileType(useLargeTsFile)}`;
}
function createSessionWithEventHandler(files: File[], useLargeTsFile: boolean) {
@@ -25,23 +27,9 @@ describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large fi
};
files.push(largeFile);
const host = createServerHost(files);
const { session, events: largeFileReferencedEvents } = createSessionWithEventTracking<ts.server.LargeFileReferencedEvent>(host, ts.server.LargeFileReferencedEvent);
const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) });
return { session, verifyLargeFile };
function verifyLargeFile(project: ts.server.Project) {
checkProjectActualFiles(project, files.map(f => f.path));
// large file for non ts file should be empty and for ts file should have content
const service = session.getProjectService();
const info = service.getScriptInfo(largeFile.path)!;
assert.equal(info.cacheSourceFile!.sourceFile.text, useLargeTsFile ? largeFile.content : "");
assert.deepEqual(largeFileReferencedEvents, useLargeTsFile ? ts.emptyArray : [{
eventName: ts.server.LargeFileReferencedEvent,
data: { file: largeFile.path, fileSize: largeFile.fileSize, maxFileSize: ts.server.maxFileSize }
}]);
}
return session;
}
function verifyLargeFile(useLargeTsFile: boolean) {
@@ -55,11 +43,9 @@ describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large fi
content: JSON.stringify({ files: ["src/file.ts", getLargeFile(useLargeTsFile)], compilerOptions: { target: 1, allowJs: true } })
};
const files = [file, libFile, tsconfig];
const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile);
const service = session.getProjectService();
const session = createSessionWithEventHandler(files, useLargeTsFile);
openFilesForSession([file], session);
checkNumberOfProjects(service, { configuredProjects: 1 });
verifyLargeFile(service.configuredProjects.get(tsconfig.path)!);
baselineTsserverLogs("events/largeFileReferenced", `when large ${getFileType(useLargeTsFile)} file is included by tsconfig`, session);
});
it("when large file is included by module resolution", () => {
@@ -68,11 +54,9 @@ describe("unittests:: tsserver:: events:: LargeFileReferencedEvent with large fi
content: `export var y = 10;import {x} from "./large"`
};
const files = [file, libFile];
const { session, verifyLargeFile } = createSessionWithEventHandler(files, useLargeTsFile);
const service = session.getProjectService();
const session = createSessionWithEventHandler(files, useLargeTsFile);
openFilesForSession([file], session);
checkNumberOfProjects(service, { inferredProjects: 1 });
verifyLargeFile(service.inferredProjects[0]);
baselineTsserverLogs("events/largeFileReferenced", `when large ${getFileType(useLargeTsFile)} file is included by module resolution`, session);
});
}

View File

@@ -6,11 +6,9 @@ import {
} from "../../virtualFileSystemWithWatch";
import {
baselineTsserverLogs,
checkNumberOfProjects,
configuredProjectAt,
createLoggerWithInMemoryLogs,
createProjectService,
createSessionWithEventTracking,
createSession,
} from "../helpers";
describe("unittests:: tsserver:: events:: ProjectLanguageServiceStateEvent", () => {
@@ -36,30 +34,19 @@ describe("unittests:: tsserver:: events:: ProjectLanguageServiceStateEvent", ()
host.getFileSize = (filePath: string) =>
filePath === f2.path ? ts.server.maxProgramSizeForNonTsFiles + 1 : originalGetFileSize.call(host, filePath);
const { session, events } = createSessionWithEventTracking<ts.server.ProjectLanguageServiceStateEvent>(host, ts.server.ProjectLanguageServiceStateEvent);
const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) });
session.executeCommand({
seq: 0,
type: "request",
command: "open",
arguments: { file: f1.path }
} as ts.server.protocol.OpenRequest);
const projectService = session.getProjectService();
checkNumberOfProjects(projectService, { configuredProjects: 1 });
const project = configuredProjectAt(projectService, 0);
assert.isFalse(project.languageServiceEnabled, "Language service enabled");
assert.equal(events.length, 1, "should receive event");
assert.equal(events[0].data.project, project, "project name");
assert.equal(events[0].data.project.getProjectName(), config.path, "config path");
assert.isFalse(events[0].data.languageServiceEnabled, "Language service state");
session.logger.log(`Language service enabled: ${session.getProjectService().configuredProjects.get(config.path)!.languageServiceEnabled}`);
host.writeFile(configWithExclude.path, configWithExclude.content);
host.checkTimeoutQueueLengthAndRun(2);
checkNumberOfProjects(projectService, { configuredProjects: 1 });
assert.isTrue(project.languageServiceEnabled, "Language service enabled");
assert.equal(events.length, 2, "should receive event");
assert.equal(events[1].data.project, project, "project");
assert.equal(events[1].data.project.getProjectName(), config.path, "config path");
assert.isTrue(events[1].data.languageServiceEnabled, "Language service state");
session.logger.log(`Language service enabled: ${session.getProjectService().configuredProjects.get(config.path)!.languageServiceEnabled}`);
baselineTsserverLogs("events/projectLanguageServiceState", "language service disabled events are triggered", session);
});
it("Large file size is determined correctly", () => {
@@ -87,6 +74,6 @@ describe("unittests:: tsserver:: events:: ProjectLanguageServiceStateEvent", ()
const project = service.configuredProjects.get(config.path)!;
service.logger.info(`languageServiceEnabled: ${project.languageServiceEnabled}`);
service.logger.info(`lastFileExceededProgramSize: ${project.lastFileExceededProgramSize}`);
baselineTsserverLogs("projectLanguageServiceStateEvent", "large file size is determined correctly", service);
baselineTsserverLogs("events/projectLanguageServiceState", "large file size is determined correctly", service);
});
});

View File

@@ -6,9 +6,10 @@ import {
TestServerHost,
} from "../../virtualFileSystemWithWatch";
import {
checkNumberOfProjects,
createSessionWithDefaultEventHandler,
createSessionWithEventTracking,
baselineTsserverLogs,
createLoggerWithInMemoryLogs,
createSession,
createSessionWithCustomEventHandler,
openFilesForSession,
protocolLocationFromSubstring,
TestSession,
@@ -28,220 +29,157 @@ describe("unittests:: tsserver:: events:: ProjectLoadingStart and ProjectLoading
const configBPath = `/user/username/projects/b/tsconfig.json`;
const files = [libFile, aTs, configA];
function verifyProjectLoadingStartAndFinish(createSession: (host: TestServerHost) => {
session: TestSession;
getNumberOfEvents: () => number;
clearEvents: () => void;
verifyProjectLoadEvents: (expected: [ts.server.ProjectLoadingStartEvent, ts.server.ProjectLoadingFinishEvent]) => void;
}) {
function createSessionToVerifyEvent(files: readonly File[]) {
const host = createServerHost(files);
const originalReadFile = host.readFile;
const { session, getNumberOfEvents, clearEvents, verifyProjectLoadEvents } = createSession(host);
host.readFile = file => {
if (file === configA.path || file === configBPath) {
assert.equal(getNumberOfEvents(), 1, "Event for loading is sent before reading config file");
}
return originalReadFile.call(host, file);
};
const service = session.getProjectService();
return { host, session, verifyEvent, verifyEventWithOpenTs, service, getNumberOfEvents };
function verifyEvent(project: ts.server.Project, reason: string) {
verifyProjectLoadEvents([
{ eventName: ts.server.ProjectLoadingStartEvent, data: { project, reason } },
{ eventName: ts.server.ProjectLoadingFinishEvent, data: { project } }
]);
clearEvents();
}
function verifyEventWithOpenTs(file: File, configPath: string, configuredProjects: number) {
openFilesForSession([file], session);
checkNumberOfProjects(service, { configuredProjects });
const project = service.configuredProjects.get(configPath)!;
assert.isDefined(project);
verifyEvent(project, `Creating possible configured project for ${file.path} to open`);
}
}
it("when project is created by open file", () => {
const bTs: File = {
path: bTsPath,
content: "export class B {}"
};
const configB: File = {
path: configBPath,
content: "{}"
};
const { verifyEventWithOpenTs } = createSessionToVerifyEvent(files.concat(bTs, configB));
verifyEventWithOpenTs(aTs, configA.path, 1);
verifyEventWithOpenTs(bTs, configB.path, 2);
});
it("when change is detected in the config file", () => {
const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files);
verifyEventWithOpenTs(aTs, configA.path, 1);
host.writeFile(configA.path, configA.content);
host.checkTimeoutQueueLengthAndRun(2);
const project = service.configuredProjects.get(configA.path)!;
verifyEvent(project, `Change in config file detected`);
});
it("when change is detected in an extended config file", () => {
const bTs: File = {
path: bTsPath,
content: "export class B {}"
};
const configB: File = {
path: configBPath,
content: JSON.stringify({
extends: "../a/tsconfig.json",
})
};
const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files.concat(bTs, configB));
verifyEventWithOpenTs(bTs, configB.path, 1);
host.writeFile(configA.path, configA.content);
host.checkTimeoutQueueLengthAndRun(2);
const project = service.configuredProjects.get(configB.path)!;
verifyEvent(project, `Change in extended config file ${configA.path} detected`);
});
describe("when opening original location project", () => {
it("with project references", () => {
verify();
});
it("when disableSourceOfProjectReferenceRedirect is true", () => {
verify(/*disableSourceOfProjectReferenceRedirect*/ true);
});
function verify(disableSourceOfProjectReferenceRedirect?: true) {
const aDTs: File = {
path: `/user/username/projects/a/a.d.ts`,
content: `export declare class A {
}
//# sourceMappingURL=a.d.ts.map
`
};
const aDTsMap: File = {
path: `/user/username/projects/a/a.d.ts.map`,
content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}`
};
function verifyProjectLoadingStartAndFinish(sessionType: string, createSession: (host: TestServerHost) => TestSession) {
describe(sessionType, () => {
it("when project is created by open file", () => {
const bTs: File = {
path: bTsPath,
content: `import {A} from "../a/a"; new A();`
content: "export class B {}"
};
const configB: File = {
path: configBPath,
content: "{}"
};
const host = createServerHost(files.concat(bTs, configB));
const session = createSession(host);
openFilesForSession([aTs], session);
openFilesForSession([bTs], session);
baselineTsserverLogs("events/projectLoading", `project is created by open file ${sessionType}`, session);
});
it("when change is detected in the config file", () => {
const host = createServerHost(files);
const session = createSession(host);
openFilesForSession([aTs], session);
host.writeFile(configA.path, configA.content);
host.checkTimeoutQueueLengthAndRun(2);
baselineTsserverLogs("events/projectLoading", `change is detected in the config file ${sessionType}`, session);
});
it("when change is detected in an extended config file", () => {
const bTs: File = {
path: bTsPath,
content: "export class B {}"
};
const configB: File = {
path: configBPath,
content: JSON.stringify({
...(disableSourceOfProjectReferenceRedirect && {
compilerOptions: {
disableSourceOfProjectReferenceRedirect
}
}),
references: [{ path: "../a" }]
extends: "../a/tsconfig.json",
})
};
const host = createServerHost(files.concat(bTs, configB));
const session = createSession(host);
openFilesForSession([bTs], session);
const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB));
verifyEventWithOpenTs(bTs, configB.path, 1);
host.writeFile(configA.path, configA.content);
host.checkTimeoutQueueLengthAndRun(2);
baselineTsserverLogs("events/projectLoading", `change is detected in an extended config file ${sessionType}`, session);
});
session.executeCommandSeq<ts.server.protocol.ReferencesRequest>({
command: ts.server.protocol.CommandTypes.References,
arguments: {
file: bTs.path,
...protocolLocationFromSubstring(bTs.content, "A()")
}
describe("when opening original location project", () => {
it("with project references", () => {
verify();
});
checkNumberOfProjects(service, { configuredProjects: 2 });
const project = service.configuredProjects.get(configA.path)!;
assert.isDefined(project);
verifyEvent(
project,
disableSourceOfProjectReferenceRedirect ?
`Creating project for original file: ${aTs.path} for location: ${aDTs.path}` :
`Creating project for original file: ${aTs.path}`
);
}
});
it("when disableSourceOfProjectReferenceRedirect is true", () => {
verify(/*disableSourceOfProjectReferenceRedirect*/ true);
});
describe("with external projects and config files ", () => {
const projectFileName = `/user/username/projects/a/project.csproj`;
function verify(disableSourceOfProjectReferenceRedirect?: true) {
const aDTs: File = {
path: `/user/username/projects/a/a.d.ts`,
content: `export declare class A {
}
//# sourceMappingURL=a.d.ts.map
`
};
const aDTsMap: File = {
path: `/user/username/projects/a/a.d.ts.map`,
content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}`
};
const bTs: File = {
path: bTsPath,
content: `import {A} from "../a/a"; new A();`
};
const configB: File = {
path: configBPath,
content: JSON.stringify({
...(disableSourceOfProjectReferenceRedirect && {
compilerOptions: {
disableSourceOfProjectReferenceRedirect
}
}),
references: [{ path: "../a" }]
})
};
function createSession(lazyConfiguredProjectsFromExternalProject: boolean) {
const { session, service, verifyEvent: verifyEventWorker, getNumberOfEvents } = createSessionToVerifyEvent(files);
service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
service.openExternalProject({
projectFileName,
rootFiles: toExternalFiles([aTs.path, configA.path]),
options: {}
} as ts.server.protocol.ExternalProject);
checkNumberOfProjects(service, { configuredProjects: 1 });
return { session, service, verifyEvent, getNumberOfEvents };
const host = createServerHost(files.concat(aDTs, aDTsMap, bTs, configB));
const session = createSession(host);
openFilesForSession([bTs], session);
function verifyEvent() {
const projectA = service.configuredProjects.get(configA.path)!;
assert.isDefined(projectA);
verifyEventWorker(projectA, `Creating configured project in external project: ${projectFileName}`);
session.executeCommandSeq<ts.server.protocol.ReferencesRequest>({
command: ts.server.protocol.CommandTypes.References,
arguments: {
file: bTs.path,
...protocolLocationFromSubstring(bTs.content, "A()")
}
});
baselineTsserverLogs("events/projectLoading", `opening original location project${disableSourceOfProjectReferenceRedirect ? " disableSourceOfProjectReferenceRedirect" : ""} ${sessionType}`, session);
}
}
it("when lazyConfiguredProjectsFromExternalProject is false", () => {
const { verifyEvent } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ false);
verifyEvent();
});
it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => {
const { verifyEvent, getNumberOfEvents, session } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true);
assert.equal(getNumberOfEvents(), 0);
describe("with external projects and config files ", () => {
const projectFileName = `/user/username/projects/a/project.csproj`;
openFilesForSession([aTs], session);
verifyEvent();
});
function createSessionAndOpenProject(lazyConfiguredProjectsFromExternalProject: boolean) {
const host = createServerHost(files);
const session = createSession(host);
session.executeCommandSeq<ts.server.protocol.ConfigureRequest>({
command: ts.server.protocol.CommandTypes.Configure,
arguments: {
preferences: { lazyConfiguredProjectsFromExternalProject }
}
});
session.executeCommandSeq<ts.server.protocol.OpenExternalProjectRequest>({
command: ts.server.protocol.CommandTypes.OpenExternalProject,
arguments: {
projectFileName,
rootFiles: toExternalFiles([aTs.path, configA.path]),
options: {}
}
});
return session;
}
it("when lazyConfiguredProjectsFromExternalProject is disabled", () => {
const { verifyEvent, getNumberOfEvents, service } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true);
assert.equal(getNumberOfEvents(), 0);
it("when lazyConfiguredProjectsFromExternalProject is false", () => {
const session = createSessionAndOpenProject(/*lazyConfiguredProjectsFromExternalProject*/ false);
baselineTsserverLogs("events/projectLoading", `lazyConfiguredProjectsFromExternalProject is false ${sessionType}`, session);
});
service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } });
verifyEvent();
it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => {
const session = createSessionAndOpenProject(/*lazyConfiguredProjectsFromExternalProject*/ true);
openFilesForSession([aTs], session);
baselineTsserverLogs("events/projectLoading", `lazyConfiguredProjectsFromExternalProject is true and file is opened ${sessionType}`, session);
});
it("when lazyConfiguredProjectsFromExternalProject is disabled", () => {
const session = createSessionAndOpenProject(/*lazyConfiguredProjectsFromExternalProject*/ true);
session.executeCommandSeq<ts.server.protocol.ConfigureRequest>({
command: ts.server.protocol.CommandTypes.Configure,
arguments: {
preferences: { lazyConfiguredProjectsFromExternalProject: false }
}
});
baselineTsserverLogs("events/projectLoading", `lazyConfiguredProjectsFromExternalProject is disabled ${sessionType}`, session);
});
});
});
}
describe("when using event handler", () => {
verifyProjectLoadingStartAndFinish(host => {
const { session, events } = createSessionWithEventTracking<ts.server.ProjectLoadingStartEvent | ts.server.ProjectLoadingFinishEvent>(host, [ts.server.ProjectLoadingStartEvent, ts.server.ProjectLoadingFinishEvent]);
return {
session,
getNumberOfEvents: () => events.length,
clearEvents: () => events.length = 0,
verifyProjectLoadEvents: expected => assert.deepEqual(events, expected)
};
});
});
describe("when using default event handler", () => {
verifyProjectLoadingStartAndFinish(host => {
const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler<ts.server.protocol.ProjectLoadingStartEvent | ts.server.protocol.ProjectLoadingFinishEvent>(host, [ts.server.ProjectLoadingStartEvent, ts.server.ProjectLoadingFinishEvent]);
return {
session,
getNumberOfEvents: () => getEvents().length,
clearEvents,
verifyProjectLoadEvents
};
function verifyProjectLoadEvents(expected: [ts.server.ProjectLoadingStartEvent, ts.server.ProjectLoadingFinishEvent]) {
const actual = getEvents().map(e => ({ eventName: e.event, data: e.body }));
const mappedExpected = expected.map(e => {
const { project, ...rest } = e.data;
return { eventName: e.eventName, data: { projectName: project.getProjectName(), ...rest } };
});
assert.deepEqual(actual, mappedExpected);
}
});
});
verifyProjectLoadingStartAndFinish("when using event handler", host => createSessionWithCustomEventHandler(host));
verifyProjectLoadingStartAndFinish("when using default event handler", host => createSession(
host,
{ canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) }
));
});

View File

@@ -7,44 +7,15 @@ import {
} from "../../virtualFileSystemWithWatch";
import {
baselineTsserverLogs,
createHasErrorMessageLogger,
createLoggerWithInMemoryLogs,
createSessionWithDefaultEventHandler,
createSessionWithEventTracking,
Logger,
createSession,
createSessionWithCustomEventHandler,
openFilesForSession,
TestSession,
} from "../helpers";
describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
function verifyFiles(caption: string, actual: readonly string[], expected: readonly string[]) {
assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`);
const seen = new Map<string, true>();
ts.forEach(actual, f => {
assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`);
seen.set(f, true);
assert.isTrue(ts.contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`);
});
}
function createVerifyInitialOpen(session: TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: ts.server.ProjectsUpdatedInBackgroundEvent[]) => void) {
return (file: File) => {
session.executeCommandSeq({
command: ts.server.protocol.CommandTypes.Open,
arguments: {
file: file.path
}
} as ts.server.protocol.OpenRequest);
verifyProjectsUpdatedInBackgroundEventHandler([]);
};
}
interface ProjectsUpdatedInBackgroundEventVerifier {
session: TestSession;
verifyProjectsUpdatedInBackgroundEventHandler(events: ts.server.ProjectsUpdatedInBackgroundEvent[]): void;
verifyInitialOpen(file: File): void;
}
function verifyProjectsUpdatedInBackgroundEvent(scenario: string, createSession: (host: TestServerHost, logger?: Logger) => ProjectsUpdatedInBackgroundEventVerifier) {
function verifyProjectsUpdatedInBackgroundEvent(scenario: string, createSession: (host: TestServerHost) => TestSession) {
it("when adding new file", () => {
const commonFile1: File = {
path: "/a/b/file1.ts",
@@ -62,87 +33,53 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
path: "/a/b/tsconfig.json",
content: `{}`
};
const openFiles = [commonFile1.path];
const host = createServerHost([commonFile1, libFile, configFile]);
const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host);
verifyInitialOpen(commonFile1);
const session = createSession(host);
openFilesForSession([commonFile1], session);
host.writeFile(commonFile2.path, commonFile2.content);
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: ts.server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
host.writeFile(commonFile3.path, commonFile3.content);
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: ts.server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and when adding new file`, session);
});
describe("with --out or --outFile setting", () => {
function verifyEventWithOutSettings(compilerOptions: ts.CompilerOptions = {}) {
const config: File = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions
})
};
function verifyEventWithOutSettings(subScenario: string, compilerOptions: ts.CompilerOptions = {}) {
it(subScenario, () => {
const config: File = {
path: "/a/tsconfig.json",
content: JSON.stringify({
compilerOptions
})
};
const f1: File = {
path: "/a/a.ts",
content: "export let x = 1"
};
const f2: File = {
path: "/a/b.ts",
content: "export let y = 1"
};
const f1: File = {
path: "/a/a.ts",
content: "export let x = 1"
};
const f2: File = {
path: "/a/b.ts",
content: "export let y = 1"
};
const openFiles = [f1.path];
const files = [f1, config, libFile];
const host = createServerHost(files);
const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host);
verifyInitialOpen(f1);
const files = [f1, config, libFile];
const host = createServerHost(files);
const session = createSession(host);
openFilesForSession([f1], session);
host.writeFile(f2.path, f2.content);
host.runQueuedTimeoutCallbacks();
host.writeFile(f2.path, f2.content);
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: ts.server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
host.writeFile(f2.path, "export let x = 11");
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: ts.server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
host.writeFile(f2.path, "export let x = 11");
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and ${subScenario}`, session);
});
}
it("when both options are not set", () => {
verifyEventWithOutSettings();
});
it("when --out is set", () => {
const outJs = "/a/out.js";
verifyEventWithOutSettings({ out: outJs });
});
it("when --outFile is set", () => {
const outJs = "/a/out.js";
verifyEventWithOutSettings({ outFile: outJs });
});
verifyEventWithOutSettings("when both options are not set");
verifyEventWithOutSettings("when --out is set", { out: "/a/out.js" });
verifyEventWithOutSettings("when --outFile is set", { outFile: "/a/out.js" });
});
describe("with modules and configured project", () => {
@@ -191,50 +128,23 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
const files: File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile];
const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files;
const filesToReload = firstReloadFileList?.map(fileName => ts.find(files, file => file.path === fileName)!) || files;
const host = createServerHost([filesToReload[0], configFile]);
// Initial project creation
const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host);
const openFiles = [filesToReload[0].path];
verifyInitialOpen(filesToReload[0]);
const session = createSession(host);
openFilesForSession([filesToReload[0]], session);
// Since this is first event, it will have all the files
filesToReload.forEach(f => host.ensureFileOrFolder(f));
if (!firstReloadFileList) host.runQueuedTimeoutCallbacks(); // Invalidated module resolutions to schedule project update
verifyProjectsUpdatedInBackgroundEvent();
return {
host,
host, session,
moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile,
updateContentOfOpenFile,
verifyNoProjectsUpdatedInBackgroundEvent,
verifyProjectsUpdatedInBackgroundEvent
};
function getFiles(filelist: string[]) {
return ts.map(filelist, getFile);
}
function getFile(fileName: string) {
return ts.find(files, file => file.path === fileName)!;
}
function verifyNoProjectsUpdatedInBackgroundEvent() {
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([]);
}
function verifyProjectsUpdatedInBackgroundEvent() {
host.runQueuedTimeoutCallbacks();
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: ts.server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
}
function updateContentOfOpenFile(file: File, newContent: string) {
session.executeCommandSeq<ts.server.protocol.ChangeRequest>({
command: ts.server.protocol.CommandTypes.Change,
@@ -252,35 +162,36 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
}
it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => {
const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState();
const { host, moduleFile1, session } = getInitialState();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };`
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { console.log('hi'); };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should contains only itself`, session);
});
it("should be up-to-date with the reference map changes", () => {
const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState();
const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, session } = getInitialState();
// Change file1Consumer1 content to `export let y = Foo();`
updateContentOfOpenFile(file1Consumer1, "export let y = Foo();");
verifyNoProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
// Add the import statements back to file1Consumer1
updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`);
verifyNoProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
// Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };`
host.writeFile(moduleFile1.path, `export var T: number;export var T2: string;export function Foo() { };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
// Multiple file edits in one go:
@@ -288,65 +199,72 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`);
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should be up-to-date with the reference map changes`, session);
});
it("should be up-to-date with deleted files", () => {
const { host, moduleFile1, file1Consumer2, verifyProjectsUpdatedInBackgroundEvent } = getInitialState();
const { host, moduleFile1, file1Consumer2, session } = getInitialState();
// Change the content of moduleFile1 to `export var T: number;export function Foo() { };`
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`);
// Delete file1Consumer2
host.deleteFile(file1Consumer2.path);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should be up-to-date with deleted files`, session);
});
it("should be up-to-date with newly created files", () => {
const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState();
const { host, moduleFile1, session, } = getInitialState();
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`);
host.writeFile("/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should be up-to-date with newly created files`, session);
});
it("should detect changes in non-root files", () => {
const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
const { host, moduleFile1, session } = getInitialState({
configObj: { files: [file1Consumer1Path] },
});
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
// change file1 internal, and verify only file1 is affected
host.writeFile(moduleFile1.path, moduleFile1.content + "var T1: number;");
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should detect changes in non-root files`, session);
});
it("should return all files if a global file changed shape", () => {
const { host, globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState();
const { host, globalFile3, session } = getInitialState();
host.writeFile(globalFile3.path, globalFile3.content + "var T2: string;");
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should return all files if a global file changed shape`, session);
});
it("should always return the file itself if '--isolatedModules' is specified", () => {
const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
const { host, moduleFile1, session } = getInitialState({
configObj: { compilerOptions: { isolatedModules: true } }
});
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should always return the file itself if --isolatedModules is specified`, session);
});
it("should always return the file itself if '--out' or '--outFile' is specified", () => {
const outFilePath = "/a/b/out.js";
const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
const { host, moduleFile1, session } = getInitialState({
configObj: { compilerOptions: { module: "system", outFile: outFilePath } }
});
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should always return the file itself if --out or --outFile is specified`, session);
});
it("should return cascaded affected file list", () => {
@@ -354,21 +272,22 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
path: "/a/b/file1Consumer1Consumer1.ts",
content: `import {y} from "./file1Consumer1";`
};
const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, session } = getInitialState({
getAdditionalFileOrFolder: () => [file1Consumer1Consumer1]
});
updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;");
verifyNoProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
// Doesnt change the shape of file1Consumer1
host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
// Change both files before the timeout
updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;");
host.writeFile(moduleFile1.path, `export var T2: number;export function Foo() { };`);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should return cascaded affected file list`, session);
});
it("should work fine for files with circular references", () => {
@@ -384,13 +303,14 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
/// <reference path="./file1.ts" />
export var t2 = 10;`
};
const { host, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
const { host, session } = getInitialState({
getAdditionalFileOrFolder: () => [file1, file2],
firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath]
});
host.writeFile(file2.path, file2.content + "export var t3 = 10;");
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should work fine for files with circular references`, session);
});
it("should detect removed code file", () => {
@@ -400,13 +320,14 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
/// <reference path="./moduleFile1.ts" />
export var x = Foo();`
};
const { host, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
const { host, session } = getInitialState({
getAdditionalFileOrFolder: () => [referenceFile1],
firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath]
});
host.deleteFile(moduleFile1Path);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should detect removed code file`, session);
});
it("should detect non-existing code file", () => {
@@ -416,17 +337,18 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
/// <reference path="./moduleFile2.ts" />
export var x = Foo();`
};
const { host, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({
const { host, moduleFile2, updateContentOfOpenFile, session } = getInitialState({
getAdditionalFileOrFolder: () => [referenceFile1],
firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath]
});
updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();");
verifyNoProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
// Create module File2 and see both files are saved
host.writeFile(moduleFile2.path, moduleFile2.content);
verifyProjectsUpdatedInBackgroundEvent();
host.runQueuedTimeoutCallbacks();
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and should detect non-existing code file`, session);
});
});
@@ -451,34 +373,19 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
content: JSON.stringify({ compilerOptions: { typeRoots: [] } })
};
const openFiles = [file1.path];
const host = createServerHost([file1, file3, libFile, configFile]);
const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host, createLoggerWithInMemoryLogs(host));
verifyInitialOpen(file1);
const session = createSession(host);
openFilesForSession([file1], session);
file3.content += "export class d {}";
host.writeFile(file3.path, file3.content);
host.checkTimeoutQueueLengthAndRun(2);
// Since this is first event
verifyProjectsUpdatedInBackgroundEventHandler([{
eventName: ts.server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}]);
host.writeFile(file2.path, file2.content);
host.runQueuedTimeoutCallbacks(); // For invalidation
host.runQueuedTimeoutCallbacks(); // For actual update
verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{
eventName: ts.server.ProjectsUpdatedInBackgroundEvent,
data: {
openFiles
}
}] : []);
baselineTsserverLogs("projectUpdatedInBackground", `${scenario} and ${subScenario}`, session);
baselineTsserverLogs("events/projectUpdatedInBackground", `${scenario} and ${subScenario}`, session);
});
}
verifyWithMaxCacheLimit("project is not at root level", /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true);
@@ -487,85 +394,23 @@ describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => {
}
describe("when event handler is set in the session", () => {
verifyProjectsUpdatedInBackgroundEvent("when event handler is set in the session", createSessionWithProjectChangedEventHandler);
function createSessionWithProjectChangedEventHandler(host: TestServerHost, logger: Logger | undefined): ProjectsUpdatedInBackgroundEventVerifier {
const { session, events: projectChangedEvents } = createSessionWithEventTracking<ts.server.ProjectsUpdatedInBackgroundEvent>(
host,
ts.server.ProjectsUpdatedInBackgroundEvent,
logger && { logger }
);
return {
session,
verifyProjectsUpdatedInBackgroundEventHandler,
verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler)
};
function eventToString(event: ts.server.ProjectsUpdatedInBackgroundEvent) {
return JSON.stringify(event && { eventName: event.eventName, data: event.data });
}
function eventsToString(events: readonly ts.server.ProjectsUpdatedInBackgroundEvent[]) {
return "[" + ts.map(events, eventToString).join(",") + "]";
}
function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: readonly ts.server.ProjectsUpdatedInBackgroundEvent[]) {
assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`);
ts.forEach(projectChangedEvents, (actualEvent, i) => {
const expectedEvent = expectedEvents[i];
assert.strictEqual(actualEvent.eventName, expectedEvent.eventName);
verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles);
});
// Verified the events, reset them
projectChangedEvents.length = 0;
}
}
verifyProjectsUpdatedInBackgroundEvent("when event handler is set in the session", createSessionWithCustomEventHandler);
});
describe("when event handler is not set but session is created with canUseEvents = true", () => {
describe("without noGetErrOnBackgroundUpdate, diagnostics for open files are queued", () => {
verifyProjectsUpdatedInBackgroundEvent("without noGetErrOnBackgroundUpdate", createSessionThatUsesEvents);
verifyProjectsUpdatedInBackgroundEvent("without noGetErrOnBackgroundUpdate", host => createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
}));
});
describe("with noGetErrOnBackgroundUpdate, diagnostics for open file are not queued", () => {
verifyProjectsUpdatedInBackgroundEvent("with noGetErrOnBackgroundUpdate", (host, logger) => createSessionThatUsesEvents(host, logger, /*noGetErrOnBackgroundUpdate*/ true));
verifyProjectsUpdatedInBackgroundEvent("with noGetErrOnBackgroundUpdate", host => createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host),
noGetErrOnBackgroundUpdate: true
}));
});
function createSessionThatUsesEvents(host: TestServerHost, logger: Logger | undefined, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier {
const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler<ts.server.protocol.ProjectsUpdatedInBackgroundEvent>(
host,
ts.server.ProjectsUpdatedInBackgroundEvent,
{ noGetErrOnBackgroundUpdate, logger: logger || createHasErrorMessageLogger() }
);
return {
session,
verifyProjectsUpdatedInBackgroundEventHandler,
verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler)
};
function verifyProjectsUpdatedInBackgroundEventHandler(expected: readonly ts.server.ProjectsUpdatedInBackgroundEvent[]) {
const expectedEvents: ts.server.protocol.ProjectsUpdatedInBackgroundEventBody[] = ts.map(expected, e => {
return {
openFiles: e.data.openFiles
};
});
const events = getEvents();
assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${ts.map(events, e => e.body)} Expected: ${expectedEvents}`);
ts.forEach(events, (actualEvent, i) => {
const expectedEvent = expectedEvents[i];
verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles);
});
// Verified the events, reset them
clearEvents();
if (events.length) {
host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate
}
}
}
});
});

View File

@@ -387,81 +387,6 @@ export function toExternalFiles(fileNames: string[]) {
return ts.map(fileNames, toExternalFile);
}
export function fileStats(nonZeroStats: Partial<ts.server.FileStats>): ts.server.FileStats {
return { ts: 0, tsSize: 0, tsx: 0, tsxSize: 0, dts: 0, dtsSize: 0, js: 0, jsSize: 0, jsx: 0, jsxSize: 0, deferred: 0, deferredSize: 0, ...nonZeroStats };
}
export class TestServerEventManager {
private events: ts.server.ProjectServiceEvent[] = [];
readonly session: TestSession;
readonly service: ts.server.ProjectService;
readonly host: TestServerHost;
constructor(files: File[], suppressDiagnosticEvents?: boolean) {
this.host = createServerHost(files);
this.session = createSession(this.host, {
canUseEvents: true,
eventHandler: event => this.events.push(event),
suppressDiagnosticEvents,
});
this.service = this.session.getProjectService();
}
getEvents(): readonly ts.server.ProjectServiceEvent[] {
const events = this.events;
this.events = [];
return events;
}
getEvent<T extends ts.server.ProjectServiceEvent>(eventName: T["eventName"]): T["data"] {
let eventData: T["data"] | undefined;
ts.filterMutate(this.events, e => {
if (e.eventName === eventName) {
if (eventData !== undefined) {
assert(false, "more than one event found");
}
eventData = e.data;
return false;
}
return true;
});
return ts.Debug.checkDefined(eventData);
}
hasZeroEvent<T extends ts.server.ProjectServiceEvent>(eventName: T["eventName"]) {
this.events.forEach(event => assert.notEqual(event.eventName, eventName));
}
assertProjectInfoTelemetryEvent(partial: Partial<ts.server.ProjectInfoTelemetryEventData>, configFile = "/tsconfig.json"): void {
assert.deepEqual<ts.server.ProjectInfoTelemetryEventData>(this.getEvent<ts.server.ProjectInfoTelemetryEvent>(ts.server.ProjectInfoTelemetryEvent), {
projectId: ts.sys.createSHA256Hash!(configFile),
fileStats: fileStats({ ts: 1 }),
compilerOptions: {},
extends: false,
files: false,
include: false,
exclude: false,
compileOnSave: false,
typeAcquisition: {
enable: false,
exclude: false,
include: false,
},
configFileName: "tsconfig.json",
projectType: "configured",
languageServiceEnabled: true,
version: ts.version,
...partial,
});
}
assertOpenFileTelemetryEvent(info: ts.server.OpenFileInfo): void {
assert.deepEqual<ts.server.OpenFileInfoTelemetryEventData>(this.getEvent<ts.server.OpenFileInfoTelemetryEvent>(ts.server.OpenFileInfoTelemetryEvent), { info });
}
assertNoOpenFilesTelemetryEvent(): void {
this.hasZeroEvent<ts.server.OpenFileInfoTelemetryEvent>(ts.server.OpenFileInfoTelemetryEvent);
}
}
export type TestSessionAndServiceHost = TestServerHostTrackingWrittenFiles & {
patched: boolean;
baselineHost(title: string): void;
@@ -528,7 +453,6 @@ export interface TestSessionOptions extends ts.server.SessionOptions {
export type TestSessionRequest<T extends ts.server.protocol.Request> = Pick<T, "command" | "arguments">;
export class TestSession extends ts.server.Session {
private seq = 0;
public events: ts.server.protocol.Event[] = [];
public testhost: TestSessionAndServiceHost;
public override logger: Logger;
@@ -574,13 +498,7 @@ export class TestSession extends ts.server.Session {
return this.executeCommand(request);
}
public override event<T extends object>(body: T, eventName: string) {
this.events.push(ts.server.toEvent(eventName, body));
super.event(body, eventName);
}
public clearMessages() {
ts.clear(this.events);
this.testhost.clearOutput();
}
}
@@ -610,38 +528,32 @@ export function createSession(host: TestServerHost, opts: Partial<TestSessionOpt
return new TestSession({ ...sessionOptions, ...opts });
}
export function createSessionWithEventTracking<T extends ts.server.ProjectServiceEvent>(host: TestServerHost, eventNames: T["eventName"] | T["eventName"][], opts: Partial<TestSessionOptions> = {}) {
const events: T[] = [];
const session = createSession(host, {
eventHandler: e => {
if (ts.isArray(eventNames) ? eventNames.some(eventName => e.eventName === eventName) : eventNames === e.eventName) {
events.push(e as T);
}
},
...opts
});
return { session, events };
}
export function createSessionWithDefaultEventHandler<T extends ts.server.protocol.AnyEvent>(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial<TestSessionOptions> = {}) {
const session = createSession(host, { canUseEvents: true, ...opts });
return {
session,
getEvents,
clearEvents
};
function getEvents() {
return ts.mapDefined(host.getOutput(), s => {
const e = mapOutputToJson(s);
return (ts.isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined;
});
}
function clearEvents() {
session.clearMessages();
export function createSessionWithCustomEventHandler(host: TestServerHost, opts?: Partial<TestSessionOptions>) {
const session = createSession(host, { eventHandler, logger: createLoggerWithInMemoryLogs(host), ...opts });
return session;
function eventHandler(event: ts.server.ProjectServiceEvent) {
let data = event.data as any;
switch (event.eventName) {
// No change to data
case ts.server.ProjectsUpdatedInBackgroundEvent:
case ts.server.LargeFileReferencedEvent:
case ts.server.ProjectInfoTelemetryEvent:
case ts.server.OpenFileInfoTelemetryEvent:
break;
// Convert project to project name
case ts.server.ProjectLoadingStartEvent:
case ts.server.ProjectLoadingFinishEvent:
case ts.server.ProjectLanguageServiceStateEvent:
data = { ...data, project: event.data.project.getProjectName() };
break;
// Map diagnostics
case ts.server.ConfigFileDiagEvent:
data = { ...data, diagnostics: ts.map(event.data.diagnostics, diagnostic => ts.server.formatDiagnosticToProtocol(diagnostic, /*includeFileName*/ true)) };
break;
default:
ts.Debug.assertNever(event);
}
session.event(data, `CustomHandler::${event.eventName}`);
}
}

View File

@@ -1,18 +1,24 @@
import * as ts from "../../_namespaces/ts";
import { File } from "../virtualFileSystemWithWatch";
import { createServerHost, File } from "../virtualFileSystemWithWatch";
import {
checkNumberOfProjects,
fileStats,
TestServerEventManager,
baselineTsserverLogs,
closeFilesForSession,
createLoggerWithInMemoryLogs,
createSession,
openFilesForSession,
toExternalFiles,
} from "./helpers";
describe("unittests:: tsserver:: project telemetry", () => {
it("does nothing for inferred project", () => {
const file = makeFile("/a.js");
const et = new TestServerEventManager([file]);
et.service.openClientFile(file.path);
et.hasZeroEvent(ts.server.ProjectInfoTelemetryEvent);
const host = createServerHost([file]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([file], session);
baselineTsserverLogs("telemetry", "does nothing for inferred project", session);
});
it("only sends an event once", () => {
@@ -20,22 +26,16 @@ describe("unittests:: tsserver:: project telemetry", () => {
const file2 = makeFile("/b.ts");
const tsconfig = makeFile("/a/tsconfig.json", {});
const et = new TestServerEventManager([file, file2, tsconfig]);
et.service.openClientFile(file.path);
et.assertProjectInfoTelemetryEvent({}, tsconfig.path);
et.service.closeClientFile(file.path);
checkNumberOfProjects(et.service, { configuredProjects: 1 });
et.service.openClientFile(file2.path);
checkNumberOfProjects(et.service, { inferredProjects: 1 });
et.hasZeroEvent(ts.server.ProjectInfoTelemetryEvent);
et.service.openClientFile(file.path);
checkNumberOfProjects(et.service, { configuredProjects: 1, inferredProjects: 1 });
et.hasZeroEvent(ts.server.ProjectInfoTelemetryEvent);
const host = createServerHost([file, file2, tsconfig]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([file], session);
closeFilesForSession([file], session);
openFilesForSession([file2], session);
openFilesForSession([file], session);
baselineTsserverLogs("telemetry", "only sends an event once", session);
});
it("counts files by extension", () => {
@@ -44,54 +44,49 @@ describe("unittests:: tsserver:: project telemetry", () => {
const compilerOptions: ts.CompilerOptions = { allowJs: true };
const tsconfig = makeFile("/tsconfig.json", { compilerOptions, include: ["src"] });
const et = new TestServerEventManager([...files, notIncludedFile, tsconfig]);
et.service.openClientFile(files[0].path);
et.assertProjectInfoTelemetryEvent({
fileStats: fileStats({ ts: 2, tsx: 1, js: 1, jsx: 1, dts: 1 }),
compilerOptions,
include: true,
const host = createServerHost([...files, notIncludedFile, tsconfig]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([files[0]], session);
baselineTsserverLogs("telemetry", "counts files by extension", session);
});
it("works with external project", () => {
const file1 = makeFile("/a.ts");
const et = new TestServerEventManager([file1]);
const host = createServerHost([file1]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
const compilerOptions: ts.server.protocol.CompilerOptions = { strict: true };
const projectFileName = "/hunter2/foo.csproj";
open();
// TODO: Apparently compilerOptions is mutated, so have to repeat it here!
et.assertProjectInfoTelemetryEvent({
compilerOptions: { strict: true },
compileOnSave: true,
// These properties can't be present for an external project, so they are undefined instead of false.
extends: undefined,
files: undefined,
include: undefined,
exclude: undefined,
configFileName: "other",
projectType: "external",
}, "/hunter2/foo.csproj");
// Also test that opening an external project only sends an event once.
et.service.closeClientFile(file1.path);
closeFilesForSession([file1], session);
et.service.closeExternalProject(projectFileName);
checkNumberOfProjects(et.service, { externalProjects: 0 });
session.executeCommandSeq<ts.server.protocol.CloseExternalProjectRequest>({
command: ts.server.protocol.CommandTypes.CloseExternalProject,
arguments: { projectFileName }
});
open();
assert.equal(et.getEvents().length, 0);
baselineTsserverLogs("telemetry", "works with external project", session);
function open(): void {
et.service.openExternalProject({
rootFiles: toExternalFiles([file1.path]),
options: compilerOptions,
projectFileName,
session.executeCommandSeq<ts.server.protocol.OpenExternalProjectRequest>({
command: ts.server.protocol.CommandTypes.OpenExternalProject,
arguments: {
rootFiles: toExternalFiles([file1.path]),
options: compilerOptions,
projectFileName,
}
});
checkNumberOfProjects(et.service, { externalProjects: 1 });
et.service.openClientFile(file1.path); // Only on file open the project will be updated
openFilesForSession([file1], session); // Only on file open the project will be updated
}
});
@@ -128,39 +123,16 @@ describe("unittests:: tsserver:: project telemetry", () => {
// Sensitive data doesn't get through even if sent to an option of safe type
checkJs: "hunter2" as any as boolean,
};
const safeCompilerOptions: ts.CompilerOptions = {
project: "",
outFile: "",
outDir: "",
rootDir: "",
baseUrl: "",
rootDirs: [""],
typeRoots: [""],
types: [""],
sourceRoot: "",
mapRoot: "",
jsxFactory: "",
out: "",
reactNamespace: "",
charset: "",
locale: "",
declarationDir: "",
paths: "" as any,
declaration: true,
lib: ["es6", "dom"],
};
(compilerOptions as any).unknownCompilerOption = "hunter2"; // These are always ignored.
const tsconfig = makeFile("/tsconfig.json", { compilerOptions, files: ["/a.ts"] });
const et = new TestServerEventManager([file, tsconfig]);
et.service.openClientFile(file.path);
et.assertProjectInfoTelemetryEvent({
compilerOptions: safeCompilerOptions,
files: true,
const host = createServerHost([file, tsconfig]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([file], session);
baselineTsserverLogs("telemetry", "does not expose paths", session);
});
it("sends telemetry for extends, files, include, exclude, and compileOnSave", () => {
@@ -173,16 +145,13 @@ describe("unittests:: tsserver:: project telemetry", () => {
exclude: ["hunter2"],
compileOnSave: true,
});
const et = new TestServerEventManager([tsconfig, file]);
et.service.openClientFile(file.path);
et.assertProjectInfoTelemetryEvent({
extends: true,
files: true,
include: true,
exclude: true,
compileOnSave: true,
const host = createServerHost([file, tsconfig]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([file], session);
baselineTsserverLogs("telemetry", "sends telemetry for extends, files, include, exclude, and compileOnSave", session);
});
const autoJsCompilerOptions = {
@@ -204,18 +173,13 @@ describe("unittests:: tsserver:: project telemetry", () => {
exclude: [],
},
});
const et = new TestServerEventManager([jsconfig, file]);
et.service.openClientFile(file.path);
et.assertProjectInfoTelemetryEvent({
fileStats: fileStats({ js: 1 }),
compilerOptions: autoJsCompilerOptions,
typeAcquisition: {
enable: true,
include: true,
exclude: false,
},
configFileName: "jsconfig.json",
}, "/jsconfig.json");
const host = createServerHost([file, jsconfig]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([file], session);
baselineTsserverLogs("telemetry", "sends telemetry for typeAcquisition settings", session);
});
it("sends telemetry for file sizes", () => {
@@ -224,73 +188,67 @@ describe("unittests:: tsserver:: project telemetry", () => {
const tsconfig = makeFile("/jsconfig.json", {
compilerOptions: autoJsCompilerOptions
});
const et = new TestServerEventManager([tsconfig, jsFile, tsFile]);
et.service.openClientFile(jsFile.path);
et.assertProjectInfoTelemetryEvent({
fileStats: fileStats({ js: 1, jsSize: 1, ts: 1, tsSize: 2 }),
compilerOptions: autoJsCompilerOptions,
typeAcquisition: {
enable: true,
include: false,
exclude: false,
},
configFileName: "jsconfig.json",
}, "/jsconfig.json");
const host = createServerHost([tsconfig, tsFile, jsFile]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([jsFile], session);
baselineTsserverLogs("telemetry", "sends telemetry for file sizes", session);
});
it("detects whether language service was disabled", () => {
const file = makeFile("/a.js");
const tsconfig = makeFile("/jsconfig.json", {});
const et = new TestServerEventManager([tsconfig, file]);
const host = createServerHost([tsconfig, file]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
const fileSize = ts.server.maxProgramSizeForNonTsFiles + 1;
et.host.getFileSize = () => fileSize;
et.service.openClientFile(file.path);
et.getEvent<ts.server.ProjectLanguageServiceStateEvent>(ts.server.ProjectLanguageServiceStateEvent);
et.assertProjectInfoTelemetryEvent({
fileStats: fileStats({ js: 1, jsSize: fileSize }),
compilerOptions: autoJsCompilerOptions,
configFileName: "jsconfig.json",
typeAcquisition: {
enable: true,
include: false,
exclude: false,
},
languageServiceEnabled: false,
}, "/jsconfig.json");
host.getFileSize = () => fileSize;
openFilesForSession([file], session);
baselineTsserverLogs("telemetry", "detects whether language service was disabled", session);
});
describe("open files telemetry", () => {
it("sends event for inferred project", () => {
const ajs = makeFile("/a.js", "// @ts-check\nconst x = 0;");
const bjs = makeFile("/b.js");
const et = new TestServerEventManager([ajs, bjs]);
et.service.openClientFile(ajs.path);
et.assertOpenFileTelemetryEvent({ checkJs: true });
et.service.openClientFile(bjs.path);
et.assertOpenFileTelemetryEvent({ checkJs: false });
const host = createServerHost([ajs, bjs]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([ajs, bjs], session);
// No repeated send for opening a file seen before.
et.service.openClientFile(bjs.path);
et.assertNoOpenFilesTelemetryEvent();
openFilesForSession([bjs], session);
baselineTsserverLogs("telemetry", "sends event for inferred project", session);
});
it("not for '.ts' file", () => {
const ats = makeFile("/a.ts", "");
const et = new TestServerEventManager([ats]);
et.service.openClientFile(ats.path);
et.assertNoOpenFilesTelemetryEvent();
const host = createServerHost([ats]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([ats], session);
baselineTsserverLogs("telemetry", "not for ts file", session);
});
it("even for project with 'ts-check' in config", () => {
const file = makeFile("/a.js");
const compilerOptions: ts.CompilerOptions = { checkJs: true };
const jsconfig = makeFile("/jsconfig.json", { compilerOptions });
const et = new TestServerEventManager([jsconfig, file]);
et.service.openClientFile(file.path);
et.assertOpenFileTelemetryEvent({ checkJs: false });
const host = createServerHost([jsconfig, file]);
const session = createSession(host, {
canUseEvents: true,
logger: createLoggerWithInMemoryLogs(host)
});
openFilesForSession([file], session);
baselineTsserverLogs("telemetry", "even for project with ts-check in config", session);
});
});
});