Skip to content

Commit 39ec69e

Browse files
committed
Separate out tests for project loading events into their own unittest file
1 parent 03d66dd commit 39ec69e

File tree

4 files changed

+266
-336
lines changed

4 files changed

+266
-336
lines changed

src/testRunner/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@
8888
"unittests/tsconfigParsing.ts",
8989
"unittests/tscWatchEmit.ts",
9090
"unittests/tscWatchMode.ts",
91+
"unittests/tsserverProjectLoadingEvents.ts",
9192
"unittests/tsserverProjectSystem.ts",
9293
"unittests/typingsInstaller.ts",
9394
"unittests/versionCache.ts",

src/testRunner/unittests/tsserverHelpers.ts

Lines changed: 72 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@ namespace ts.projectSystem {
1515
export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories;
1616
export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed;
1717

18-
//const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
19-
//function mapOutputToJson(s: string) {
20-
// return convertToObject(
21-
// parseJsonText("json.json", s.replace(outputEventRegex, "")),
22-
// []
23-
// );
24-
//}
18+
const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
19+
export function mapOutputToJson(s: string) {
20+
return convertToObject(
21+
parseJsonText("json.json", s.replace(outputEventRegex, "")),
22+
[]
23+
);
24+
}
2525

2626
export const customTypesMap = {
2727
path: <Path>"/typesMap.json",
@@ -330,39 +330,39 @@ namespace ts.projectSystem {
330330
return new TestSession({ ...sessionOptions, ...opts });
331331
}
332332

333-
//function createSessionWithEventTracking<T extends server.ProjectServiceEvent>(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) {
334-
// const events: T[] = [];
335-
// const session = createSession(host, {
336-
// eventHandler: e => {
337-
// if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) {
338-
// events.push(e as T);
339-
// }
340-
// }
341-
// });
333+
export function createSessionWithEventTracking<T extends server.ProjectServiceEvent>(host: server.ServerHost, eventName: T["eventName"], ...eventNames: T["eventName"][]) {
334+
const events: T[] = [];
335+
const session = createSession(host, {
336+
eventHandler: e => {
337+
if (e.eventName === eventName || eventNames.some(eventName => e.eventName === eventName)) {
338+
events.push(e as T);
339+
}
340+
}
341+
});
342342

343-
// return { session, events };
344-
//}
343+
return { session, events };
344+
}
345345

346-
//function createSessionWithDefaultEventHandler<T extends protocol.AnyEvent>(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial<server.SessionOptions> = {}) {
347-
// const session = createSession(host, { canUseEvents: true, ...opts });
346+
export function createSessionWithDefaultEventHandler<T extends protocol.AnyEvent>(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial<server.SessionOptions> = {}) {
347+
const session = createSession(host, { canUseEvents: true, ...opts });
348348

349-
// return {
350-
// session,
351-
// getEvents,
352-
// clearEvents
353-
// };
349+
return {
350+
session,
351+
getEvents,
352+
clearEvents
353+
};
354354

355-
// function getEvents() {
356-
// return mapDefined(host.getOutput(), s => {
357-
// const e = mapOutputToJson(s);
358-
// return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined;
359-
// });
360-
// }
355+
function getEvents() {
356+
return mapDefined(host.getOutput(), s => {
357+
const e = mapOutputToJson(s);
358+
return (isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined;
359+
});
360+
}
361361

362-
// function clearEvents() {
363-
// session.clearMessages();
364-
// }
365-
//}
362+
function clearEvents() {
363+
session.clearMessages();
364+
}
365+
}
366366

367367
export interface CreateProjectServiceParameters {
368368
cancellationToken?: HostCancellationToken;
@@ -467,23 +467,25 @@ namespace ts.projectSystem {
467467
// checkArray("Open files", arrayFrom(projectService.openFiles.keys(), path => projectService.getScriptInfoForPath(path as Path)!.fileName), expectedFiles.map(file => file.path));
468468
//}
469469

470-
//function protocolLocationFromSubstring(str: string, substring: string): protocol.Location {
471-
// const start = str.indexOf(substring);
472-
// Debug.assert(start !== -1);
473-
// return protocolToLocation(str)(start);
474-
//}
475-
//function protocolToLocation(text: string): (pos: number) => protocol.Location {
476-
// const lineStarts = computeLineStarts(text);
477-
// return pos => {
478-
// const x = computeLineAndCharacterOfPosition(lineStarts, pos);
479-
// return { line: x.line + 1, offset: x.character + 1 };
480-
// };
481-
//}
482-
//function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan {
483-
// const span = textSpanFromSubstring(str, substring, options);
484-
// const toLocation = protocolToLocation(str);
485-
// return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) };
486-
//}
470+
export function protocolLocationFromSubstring(str: string, substring: string): protocol.Location {
471+
const start = str.indexOf(substring);
472+
Debug.assert(start !== -1);
473+
return protocolToLocation(str)(start);
474+
}
475+
476+
function protocolToLocation(text: string): (pos: number) => protocol.Location {
477+
const lineStarts = computeLineStarts(text);
478+
return pos => {
479+
const x = computeLineAndCharacterOfPosition(lineStarts, pos);
480+
return { line: x.line + 1, offset: x.character + 1 };
481+
};
482+
}
483+
484+
export function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan {
485+
const span = textSpanFromSubstring(str, substring, options);
486+
const toLocation = protocolToLocation(str);
487+
return { start: toLocation(span.start), end: toLocation(textSpanEnd(span)) };
488+
}
487489
//function protocolRenameSpanFromSubstring(
488490
// str: string,
489491
// substring: string,
@@ -492,11 +494,12 @@ namespace ts.projectSystem {
492494
//): protocol.RenameTextSpan {
493495
// return { ...protocolTextSpanFromSubstring(str, substring, options), ...prefixSuffixText };
494496
//}
495-
//function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan {
496-
// const start = nthIndexOf(str, substring, options ? options.index : 0);
497-
// Debug.assert(start !== -1);
498-
// return createTextSpan(start, substring.length);
499-
//}
497+
498+
export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): TextSpan {
499+
const start = nthIndexOf(str, substring, options ? options.index : 0);
500+
Debug.assert(start !== -1);
501+
return createTextSpan(start, substring.length);
502+
}
500503
//function protocolFileLocationFromSubstring(file: File, substring: string): protocol.FileLocationRequestArgs {
501504
// return { file: file.path, ...protocolLocationFromSubstring(file.content, substring) };
502505
//}
@@ -509,18 +512,19 @@ namespace ts.projectSystem {
509512
//function renameLocation(file: File, substring: string, options?: SpanFromSubstringOptions): RenameLocation {
510513
// return documentSpanFromSubstring(file, substring, options);
511514
//}
512-
//interface SpanFromSubstringOptions {
513-
// readonly index: number;
514-
//}
515515

516-
//function nthIndexOf(str: string, substr: string, n: number): number {
517-
// let index = -1;
518-
// for (; n >= 0; n--) {
519-
// index = str.indexOf(substr, index + 1);
520-
// if (index === -1) return -1;
521-
// }
522-
// return index;
523-
//}
516+
export interface SpanFromSubstringOptions {
517+
readonly index: number;
518+
}
519+
520+
function nthIndexOf(str: string, substr: string, n: number): number {
521+
let index = -1;
522+
for (; n >= 0; n--) {
523+
index = str.indexOf(substr, index + 1);
524+
if (index === -1) return -1;
525+
}
526+
return index;
527+
}
524528

525529
/**
526530
* Test server cancellation token used to mock host token cancellation requests.
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
namespace ts.projectSystem {
2+
describe("tsserverProjectLoadingEvents:: ProjectLoadingStart and ProjectLoadingFinish events", () => {
3+
const projectRoot = "/user/username/projects";
4+
const aTs: File = {
5+
path: `${projectRoot}/a/a.ts`,
6+
content: "export class A { }"
7+
};
8+
const configA: File = {
9+
path: `${projectRoot}/a/tsconfig.json`,
10+
content: "{}"
11+
};
12+
const bTsPath = `${projectRoot}/b/b.ts`;
13+
const configBPath = `${projectRoot}/b/tsconfig.json`;
14+
const files = [libFile, aTs, configA];
15+
16+
function verifyProjectLoadingStartAndFinish(createSession: (host: TestServerHost) => {
17+
session: TestSession;
18+
getNumberOfEvents: () => number;
19+
clearEvents: () => void;
20+
verifyProjectLoadEvents: (expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) => void;
21+
}) {
22+
function createSessionToVerifyEvent(files: ReadonlyArray<File>) {
23+
const host = createServerHost(files);
24+
const originalReadFile = host.readFile;
25+
const { session, getNumberOfEvents, clearEvents, verifyProjectLoadEvents } = createSession(host);
26+
host.readFile = file => {
27+
if (file === configA.path || file === configBPath) {
28+
assert.equal(getNumberOfEvents(), 1, "Event for loading is sent before reading config file");
29+
}
30+
return originalReadFile.call(host, file);
31+
};
32+
const service = session.getProjectService();
33+
return { host, session, verifyEvent, verifyEventWithOpenTs, service, getNumberOfEvents };
34+
35+
function verifyEvent(project: server.Project, reason: string) {
36+
verifyProjectLoadEvents([
37+
{ eventName: server.ProjectLoadingStartEvent, data: { project, reason } },
38+
{ eventName: server.ProjectLoadingFinishEvent, data: { project } }
39+
]);
40+
clearEvents();
41+
}
42+
43+
function verifyEventWithOpenTs(file: File, configPath: string, configuredProjects: number) {
44+
openFilesForSession([file], session);
45+
checkNumberOfProjects(service, { configuredProjects });
46+
const project = service.configuredProjects.get(configPath)!;
47+
assert.isDefined(project);
48+
verifyEvent(project, `Creating possible configured project for ${file.path} to open`);
49+
}
50+
}
51+
52+
it("when project is created by open file", () => {
53+
const bTs: File = {
54+
path: bTsPath,
55+
content: "export class B {}"
56+
};
57+
const configB: File = {
58+
path: configBPath,
59+
content: "{}"
60+
};
61+
const { verifyEventWithOpenTs } = createSessionToVerifyEvent(files.concat(bTs, configB));
62+
verifyEventWithOpenTs(aTs, configA.path, 1);
63+
verifyEventWithOpenTs(bTs, configB.path, 2);
64+
});
65+
66+
it("when change is detected in the config file", () => {
67+
const { host, verifyEvent, verifyEventWithOpenTs, service } = createSessionToVerifyEvent(files);
68+
verifyEventWithOpenTs(aTs, configA.path, 1);
69+
70+
host.writeFile(configA.path, configA.content);
71+
host.checkTimeoutQueueLengthAndRun(2);
72+
const project = service.configuredProjects.get(configA.path)!;
73+
verifyEvent(project, `Change in config file detected`);
74+
});
75+
76+
it("when opening original location project", () => {
77+
const aDTs: File = {
78+
path: `${projectRoot}/a/a.d.ts`,
79+
content: `export declare class A {
80+
}
81+
//# sourceMappingURL=a.d.ts.map
82+
`
83+
};
84+
const aDTsMap: File = {
85+
path: `${projectRoot}/a/a.d.ts.map`,
86+
content: `{"version":3,"file":"a.d.ts","sourceRoot":"","sources":["./a.ts"],"names":[],"mappings":"AAAA,qBAAa,CAAC;CAAI"}`
87+
};
88+
const bTs: File = {
89+
path: bTsPath,
90+
content: `import {A} from "../a/a"; new A();`
91+
};
92+
const configB: File = {
93+
path: configBPath,
94+
content: JSON.stringify({
95+
references: [{ path: "../a" }]
96+
})
97+
};
98+
99+
const { service, session, verifyEventWithOpenTs, verifyEvent } = createSessionToVerifyEvent(files.concat(aDTs, aDTsMap, bTs, configB));
100+
verifyEventWithOpenTs(bTs, configB.path, 1);
101+
102+
session.executeCommandSeq<protocol.ReferencesRequest>({
103+
command: protocol.CommandTypes.References,
104+
arguments: {
105+
file: bTs.path,
106+
...protocolLocationFromSubstring(bTs.content, "A()")
107+
}
108+
});
109+
110+
checkNumberOfProjects(service, { configuredProjects: 2 });
111+
const project = service.configuredProjects.get(configA.path)!;
112+
assert.isDefined(project);
113+
verifyEvent(project, `Creating project for original file: ${aTs.path} for location: ${aDTs.path}`);
114+
});
115+
116+
describe("with external projects and config files ", () => {
117+
const projectFileName = `${projectRoot}/a/project.csproj`;
118+
119+
function createSession(lazyConfiguredProjectsFromExternalProject: boolean) {
120+
const { session, service, verifyEvent: verifyEventWorker, getNumberOfEvents } = createSessionToVerifyEvent(files);
121+
service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject } });
122+
service.openExternalProject(<protocol.ExternalProject>{
123+
projectFileName,
124+
rootFiles: toExternalFiles([aTs.path, configA.path]),
125+
options: {}
126+
});
127+
checkNumberOfProjects(service, { configuredProjects: 1 });
128+
return { session, service, verifyEvent, getNumberOfEvents };
129+
130+
function verifyEvent() {
131+
const projectA = service.configuredProjects.get(configA.path)!;
132+
assert.isDefined(projectA);
133+
verifyEventWorker(projectA, `Creating configured project in external project: ${projectFileName}`);
134+
}
135+
}
136+
137+
it("when lazyConfiguredProjectsFromExternalProject is false", () => {
138+
const { verifyEvent } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ false);
139+
verifyEvent();
140+
});
141+
142+
it("when lazyConfiguredProjectsFromExternalProject is true and file is opened", () => {
143+
const { verifyEvent, getNumberOfEvents, session } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true);
144+
assert.equal(getNumberOfEvents(), 0);
145+
146+
openFilesForSession([aTs], session);
147+
verifyEvent();
148+
});
149+
150+
it("when lazyConfiguredProjectsFromExternalProject is disabled", () => {
151+
const { verifyEvent, getNumberOfEvents, service } = createSession(/*lazyConfiguredProjectsFromExternalProject*/ true);
152+
assert.equal(getNumberOfEvents(), 0);
153+
154+
service.setHostConfiguration({ preferences: { lazyConfiguredProjectsFromExternalProject: false } });
155+
verifyEvent();
156+
});
157+
});
158+
}
159+
160+
describe("when using event handler", () => {
161+
verifyProjectLoadingStartAndFinish(host => {
162+
const { session, events } = createSessionWithEventTracking<server.ProjectLoadingStartEvent | server.ProjectLoadingFinishEvent>(host, server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent);
163+
return {
164+
session,
165+
getNumberOfEvents: () => events.length,
166+
clearEvents: () => events.length = 0,
167+
verifyProjectLoadEvents: expected => assert.deepEqual(events, expected)
168+
};
169+
});
170+
});
171+
172+
describe("when using default event handler", () => {
173+
verifyProjectLoadingStartAndFinish(host => {
174+
const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler<protocol.ProjectLoadingStartEvent | protocol.ProjectLoadingFinishEvent>(host, [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]);
175+
return {
176+
session,
177+
getNumberOfEvents: () => getEvents().length,
178+
clearEvents,
179+
verifyProjectLoadEvents
180+
};
181+
182+
function verifyProjectLoadEvents(expected: [server.ProjectLoadingStartEvent, server.ProjectLoadingFinishEvent]) {
183+
const actual = getEvents().map(e => ({ eventName: e.event, data: e.body }));
184+
const mappedExpected = expected.map(e => {
185+
const { project, ...rest } = e.data;
186+
return { eventName: e.eventName, data: { projectName: project.getProjectName(), ...rest } };
187+
});
188+
assert.deepEqual(actual, mappedExpected);
189+
}
190+
});
191+
});
192+
});
193+
}

0 commit comments

Comments
 (0)