• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import * as ts from "../../_namespaces/ts";
2import * as Harness from "../../_namespaces/Harness";
3import * as Utils from "../../_namespaces/Utils";
4
5export import TI = ts.server.typingsInstaller;
6export import protocol = ts.server.protocol;
7export import CommandNames = ts.server.CommandNames;
8
9export import TestServerHost = ts.TestFSWithWatch.TestServerHost;
10export type File = ts.TestFSWithWatch.File;
11export type SymLink = ts.TestFSWithWatch.SymLink;
12export type Folder = ts.TestFSWithWatch.Folder;
13export import createServerHost = ts.TestFSWithWatch.createServerHost;
14export import checkArray = ts.TestFSWithWatch.checkArray;
15export import libFile = ts.TestFSWithWatch.libFile;
16
17export import commonFile1 = ts.tscWatch.commonFile1;
18export import commonFile2 = ts.tscWatch.commonFile2;
19
20const outputEventRegex = /Content\-Length: [\d]+\r\n\r\n/;
21export function mapOutputToJson(s: string) {
22    return ts.convertToObject(
23        ts.parseJsonText("json.json", s.replace(outputEventRegex, "")),
24        []
25    );
26}
27
28export const customTypesMap = {
29    path: "/typesMap.json" as ts.Path,
30    content: `{
31            "typesMap": {
32                "jquery": {
33                    "match": "jquery(-(\\\\.?\\\\d+)+)?(\\\\.intellisense)?(\\\\.min)?\\\\.js$",
34                    "types": ["jquery"]
35                },
36                "quack": {
37                    "match": "/duckquack-(\\\\d+)\\\\.min\\\\.js",
38                    "types": ["duck-types"]
39                }
40            },
41            "simpleMap": {
42                "Bacon": "baconjs",
43                "bliss": "blissfuljs",
44                "commander": "commander",
45                "cordova": "cordova",
46                "react": "react",
47                "lodash": "lodash"
48            }
49        }`
50};
51
52export interface PostExecAction {
53    readonly success: boolean;
54    readonly callback: TI.RequestCompletedAction;
55}
56
57export interface Logger extends ts.server.Logger {
58    logs: string[];
59    host?: TestServerHost;
60}
61
62export function nullLogger(): Logger {
63    return {
64        close: ts.noop,
65        hasLevel: ts.returnFalse,
66        loggingEnabled: ts.returnFalse,
67        perftrc: ts.noop,
68        info: ts.noop,
69        msg: ts.noop,
70        startGroup: ts.noop,
71        endGroup: ts.noop,
72        getLogFileName: ts.returnUndefined,
73        logs: [],
74    };
75}
76
77export function createHasErrorMessageLogger(): Logger {
78    return {
79        ...nullLogger(),
80        msg: (s, type) => ts.Debug.fail(`Error: ${s}, type: ${type}`),
81    };
82}
83
84function handleLoggerGroup(logger: Logger, host: TestServerHost | undefined): Logger {
85    let inGroup = false;
86    let firstInGroup = false;
87    let seq = 0;
88    logger.startGroup = () => {
89        inGroup = true;
90        firstInGroup = true;
91    };
92    logger.endGroup = () => inGroup = false;
93    logger.host = host;
94    const originalInfo = logger.info;
95    logger.info = s => msg(s, ts.server.Msg.Info, s => originalInfo.call(logger, s));
96    return logger;
97
98    function msg(s: string, type = ts.server.Msg.Err, write: (s: string) => void) {
99        s = `[${nowString()}] ${s}`;
100        if (!inGroup || firstInGroup) s = padStringRight(type + " " + seq.toString(), "          ") + s;
101        if (ts.Debug.isDebugging) console.log(s);
102        write(s);
103        if (!inGroup) seq++;
104    }
105
106    function padStringRight(str: string, padding: string) {
107        return (str + padding).slice(0, padding.length);
108    }
109
110    function nowString() {
111        // E.g. "12:34:56.789"
112        const d = logger.host!.now();
113        return `${ts.padLeft(d.getUTCHours().toString(), 2, "0")}:${ts.padLeft(d.getUTCMinutes().toString(), 2, "0")}:${ts.padLeft(d.getUTCSeconds().toString(), 2, "0")}.${ts.padLeft(d.getUTCMilliseconds().toString(), 3, "0")}`;
114    }
115}
116
117export function createLoggerWritingToConsole(host: TestServerHost): Logger {
118    return handleLoggerGroup({
119        ...nullLogger(),
120        hasLevel: ts.returnTrue,
121        loggingEnabled: ts.returnTrue,
122        perftrc: s => console.log(s),
123        info: s => console.log(s),
124        msg: (s, type) => console.log(`${type}:: ${s}`),
125    }, host);
126}
127
128export function createLoggerWithInMemoryLogs(host: TestServerHost): Logger {
129    const logger = createHasErrorMessageLogger();
130    return handleLoggerGroup({
131        ...logger,
132        hasLevel: ts.returnTrue,
133        loggingEnabled: ts.returnTrue,
134        info: s => logger.logs.push(
135            s.replace(/Elapsed::?\s*\d+(?:\.\d+)?ms/g, "Elapsed:: *ms")
136                .replace(/\"updateGraphDurationMs\"\:\d+(?:\.\d+)?/g, `"updateGraphDurationMs":*`)
137                .replace(/\"createAutoImportProviderProgramDurationMs\"\:\d+(?:\.\d+)?/g, `"createAutoImportProviderProgramDurationMs":*`)
138                .replace(`"version":"${ts.version}"`, `"version":"FakeVersion"`)
139                .replace(/getCompletionData: Get current token: \d+(?:\.\d+)?/g, `getCompletionData: Get current token: *`)
140                .replace(/getCompletionData: Is inside comment: \d+(?:\.\d+)?/g, `getCompletionData: Is inside comment: *`)
141                .replace(/getCompletionData: Get previous token: \d+(?:\.\d+)?/g, `getCompletionData: Get previous token: *`)
142                .replace(/getCompletionsAtPosition: isCompletionListBlocker: \d+(?:\.\d+)?/g, `getCompletionsAtPosition: isCompletionListBlocker: *`)
143                .replace(/getCompletionData: Semantic work: \d+(?:\.\d+)?/g, `getCompletionData: Semantic work: *`)
144                .replace(/getCompletionsAtPosition: getCompletionEntriesFromSymbols: \d+(?:\.\d+)?/g, `getCompletionsAtPosition: getCompletionEntriesFromSymbols: *`)
145                .replace(/forEachExternalModuleToImportFrom autoImportProvider: \d+(?:\.\d+)?/g, `forEachExternalModuleToImportFrom autoImportProvider: *`)
146                .replace(/getExportInfoMap: done in \d+(?:\.\d+)?/g, `getExportInfoMap: done in *`)
147                .replace(/collectAutoImports: \d+(?:\.\d+)?/g, `collectAutoImports: *`)
148                .replace(/dependencies in \d+(?:\.\d+)?/g, `dependencies in *`)
149                .replace(/\"exportMapKey\"\:\s*\"[_$a-zA-Z][_$_$a-zA-Z0-9]*\|\d+\|/g, match => match.replace(/\|\d+\|/, `|*|`))
150        )
151    }, host);
152}
153
154export function baselineTsserverLogs(scenario: string, subScenario: string, sessionOrService: { logger: Logger; }) {
155    ts.Debug.assert(sessionOrService.logger.logs.length); // Ensure caller used in memory logger
156    Harness.Baseline.runBaseline(`tsserver/${scenario}/${subScenario.split(" ").join("-")}.js`, sessionOrService.logger.logs.join("\r\n"));
157}
158
159export function appendAllScriptInfos(service: ts.server.ProjectService, logs: string[]) {
160    logs.push("");
161    logs.push(`ScriptInfos:`);
162    service.filenameToScriptInfo.forEach(info => logs.push(`path: ${info.path} fileName: ${info.fileName}`));
163    logs.push("");
164}
165
166export function appendProjectFileText(project: ts.server.Project, logs: string[]) {
167    logs.push("");
168    logs.push(`Project: ${project.getProjectName()}`);
169    project.getCurrentProgram()?.getSourceFiles().forEach(f => {
170        logs.push(JSON.stringify({ fileName: f.fileName, version: f.version }));
171        logs.push(f.text);
172        logs.push("");
173    });
174    logs.push("");
175}
176
177export class TestTypingsInstaller extends TI.TypingsInstaller implements ts.server.ITypingsInstaller {
178    protected projectService!: ts.server.ProjectService;
179    constructor(
180        readonly globalTypingsCacheLocation: string,
181        throttleLimit: number,
182        installTypingHost: ts.server.ServerHost,
183        readonly typesRegistry = new ts.Map<string, ts.MapLike<string>>(),
184        log?: TI.Log) {
185        super(installTypingHost, globalTypingsCacheLocation, "/safeList.json" as ts.Path, customTypesMap.path, throttleLimit, log);
186    }
187
188    protected postExecActions: PostExecAction[] = [];
189
190    isKnownTypesPackageName = ts.notImplemented;
191    installPackage = ts.notImplemented;
192    inspectValue = ts.notImplemented;
193
194    executePendingCommands() {
195        const actionsToRun = this.postExecActions;
196        this.postExecActions = [];
197        for (const action of actionsToRun) {
198            action.callback(action.success);
199        }
200    }
201
202    checkPendingCommands(expectedCount: number) {
203        assert.equal(this.postExecActions.length, expectedCount, `Expected ${expectedCount} post install actions`);
204    }
205
206    onProjectClosed = ts.noop;
207
208    attach(projectService: ts.server.ProjectService) {
209        this.projectService = projectService;
210    }
211
212    getInstallTypingHost() {
213        return this.installTypingHost;
214    }
215
216    installWorker(_requestId: number, _args: string[], _cwd: string, cb: TI.RequestCompletedAction): void {
217        this.addPostExecAction("success", cb);
218    }
219
220    sendResponse(response: ts.server.SetTypings | ts.server.InvalidateCachedTypings) {
221        this.projectService.updateTypingsForProject(response);
222    }
223
224    enqueueInstallTypingsRequest(project: ts.server.Project, typeAcquisition: ts.TypeAcquisition, unresolvedImports: ts.SortedReadonlyArray<string>) {
225        const request = ts.server.createInstallTypingsRequest(project, typeAcquisition, unresolvedImports, this.globalTypingsCacheLocation);
226        this.install(request);
227    }
228
229    addPostExecAction(stdout: string | string[], cb: TI.RequestCompletedAction) {
230        const out = ts.isString(stdout) ? stdout : createNpmPackageJsonString(stdout);
231        const action: PostExecAction = {
232            success: !!out,
233            callback: cb
234        };
235        this.postExecActions.push(action);
236    }
237}
238
239function createNpmPackageJsonString(installedTypings: string[]): string {
240    const dependencies: ts.MapLike<any> = {};
241    for (const typing of installedTypings) {
242        dependencies[typing] = "1.0.0";
243    }
244    return JSON.stringify({ dependencies });
245}
246
247export function createTypesRegistry(...list: string[]): ts.ESMap<string, ts.MapLike<string>> {
248    const versionMap = {
249        "latest": "1.3.0",
250        "ts2.0": "1.0.0",
251        "ts2.1": "1.0.0",
252        "ts2.2": "1.2.0",
253        "ts2.3": "1.3.0",
254        "ts2.4": "1.3.0",
255        "ts2.5": "1.3.0",
256        "ts2.6": "1.3.0",
257        "ts2.7": "1.3.0"
258    };
259    const map = new ts.Map<string, ts.MapLike<string>>();
260    for (const l of list) {
261        map.set(l, versionMap);
262    }
263    return map;
264}
265
266export function toExternalFile(fileName: string): protocol.ExternalFile {
267    return { fileName };
268}
269
270export function toExternalFiles(fileNames: string[]) {
271    return ts.map(fileNames, toExternalFile);
272}
273
274export function fileStats(nonZeroStats: Partial<ts.server.FileStats>): ts.server.FileStats {
275    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, ets: 0, etsSize: 0, dets: 0, detsSize: 0, ...nonZeroStats };
276}
277
278export class TestServerEventManager {
279    private events: ts.server.ProjectServiceEvent[] = [];
280    readonly session: TestSession;
281    readonly service: ts.server.ProjectService;
282    readonly host: TestServerHost;
283    constructor(files: File[], suppressDiagnosticEvents?: boolean) {
284        this.host = createServerHost(files);
285        this.session = createSession(this.host, {
286            canUseEvents: true,
287            eventHandler: event => this.events.push(event),
288            suppressDiagnosticEvents,
289        });
290        this.service = this.session.getProjectService();
291    }
292
293    getEvents(): readonly ts.server.ProjectServiceEvent[] {
294        const events = this.events;
295        this.events = [];
296        return events;
297    }
298
299    getEvent<T extends ts.server.ProjectServiceEvent>(eventName: T["eventName"]): T["data"] {
300        let eventData: T["data"] | undefined;
301        ts.filterMutate(this.events, e => {
302            if (e.eventName === eventName) {
303                if (eventData !== undefined) {
304                    assert(false, "more than one event found");
305                }
306                eventData = e.data;
307                return false;
308            }
309            return true;
310        });
311        return ts.Debug.checkDefined(eventData);
312    }
313
314    hasZeroEvent<T extends ts.server.ProjectServiceEvent>(eventName: T["eventName"]) {
315        this.events.forEach(event => assert.notEqual(event.eventName, eventName));
316    }
317
318    assertProjectInfoTelemetryEvent(partial: Partial<ts.server.ProjectInfoTelemetryEventData>, configFile = "/tsconfig.json"): void {
319        assert.deepEqual<ts.server.ProjectInfoTelemetryEventData>(this.getEvent<ts.server.ProjectInfoTelemetryEvent>(ts.server.ProjectInfoTelemetryEvent), {
320            projectId: ts.sys.createSHA256Hash!(configFile),
321            fileStats: fileStats({ ts: 1 }),
322            compilerOptions: {},
323            extends: false,
324            files: false,
325            include: false,
326            exclude: false,
327            compileOnSave: false,
328            typeAcquisition: {
329                enable: false,
330                exclude: false,
331                include: false,
332            },
333            configFileName: "tsconfig.json",
334            projectType: "configured",
335            languageServiceEnabled: true,
336            version: ts.version, // eslint-disable-line @typescript-eslint/no-unnecessary-qualifier
337            ...partial,
338        });
339    }
340
341    assertOpenFileTelemetryEvent(info: ts.server.OpenFileInfo): void {
342        assert.deepEqual<ts.server.OpenFileInfoTelemetryEventData>(this.getEvent<ts.server.OpenFileInfoTelemetryEvent>(ts.server.OpenFileInfoTelemetryEvent), { info });
343    }
344    assertNoOpenFilesTelemetryEvent(): void {
345        this.hasZeroEvent<ts.server.OpenFileInfoTelemetryEvent>(ts.server.OpenFileInfoTelemetryEvent);
346    }
347}
348
349export type TestSessionAndServiceHost = ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles & {
350    baselineHost(title: string): void;
351};
352function patchHostTimeouts(
353    inputHost: ts.TestFSWithWatch.TestServerHostTrackingWrittenFiles,
354    session: TestSession | TestProjectService
355) {
356    const host = inputHost as TestSessionAndServiceHost;
357    const originalCheckTimeoutQueueLength = host.checkTimeoutQueueLength;
358    const originalRunQueuedTimeoutCallbacks = host.runQueuedTimeoutCallbacks;
359    const originalRunQueuedImmediateCallbacks = host.runQueuedImmediateCallbacks;
360    let hostDiff: ReturnType<TestServerHost["snap"]> | undefined;
361
362    host.checkTimeoutQueueLengthAndRun = checkTimeoutQueueLengthAndRun;
363    host.checkTimeoutQueueLength = checkTimeoutQueueLength;
364    host.runQueuedTimeoutCallbacks = runQueuedTimeoutCallbacks;
365    host.runQueuedImmediateCallbacks = runQueuedImmediateCallbacks;
366    host.baselineHost = baselineHost;
367    return host;
368
369    function checkTimeoutQueueLengthAndRun(expected: number) {
370        host.baselineHost(`Before checking timeout queue length (${expected}) and running`);
371        originalCheckTimeoutQueueLength.call(host, expected);
372        originalRunQueuedTimeoutCallbacks.call(host);
373        host.baselineHost(`After checking timeout queue length (${expected}) and running`);
374    }
375
376    function checkTimeoutQueueLength(expected: number) {
377        host.baselineHost(`Checking timeout queue length: ${expected}`);
378        originalCheckTimeoutQueueLength.call(host, expected);
379    }
380
381    function runQueuedTimeoutCallbacks(timeoutId?: number) {
382        host.baselineHost(`Before running timeout callback${timeoutId === undefined ? "s" : timeoutId}`);
383        originalRunQueuedTimeoutCallbacks.call(host, timeoutId);
384        host.baselineHost(`After running timeout callback${timeoutId === undefined ? "s" : timeoutId}`);
385    }
386
387    function runQueuedImmediateCallbacks(checkCount?: number) {
388        host.baselineHost(`Before running immediate callbacks${checkCount === undefined ? "" : ` and checking length (${checkCount})`}`);
389        originalRunQueuedImmediateCallbacks.call(host, checkCount);
390        host.baselineHost(`Before running immediate callbacks${checkCount === undefined ? "" : ` and checking length (${checkCount})`}`);
391    }
392
393    function baselineHost(title: string) {
394        if (!session.logger.hasLevel(ts.server.LogLevel.verbose)) return;
395        session.logger.logs.push(title);
396        host.diff(session.logger.logs, hostDiff);
397        host.serializeWatches(session.logger.logs);
398        hostDiff = host.snap();
399        host.writtenFiles.clear();
400    }
401}
402
403export interface TestSessionOptions extends ts.server.SessionOptions {
404    logger: Logger;
405}
406
407export class TestSession extends ts.server.Session {
408    private seq = 0;
409    public events: protocol.Event[] = [];
410    public testhost: TestSessionAndServiceHost;
411    public logger: Logger;
412
413    constructor(opts: TestSessionOptions) {
414        super(opts);
415        this.logger = opts.logger;
416        this.testhost = patchHostTimeouts(
417            ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(this.host as TestServerHost),
418            this
419        );
420    }
421
422    getProjectService() {
423        return this.projectService;
424    }
425
426    public getSeq() {
427        return this.seq;
428    }
429
430    public getNextSeq() {
431        return this.seq + 1;
432    }
433
434    public executeCommand(request: protocol.Request) {
435        return this.baseline("response", super.executeCommand(this.baseline("request", request)));
436    }
437
438    public executeCommandSeq<T extends ts.server.protocol.Request>(request: Partial<T>) {
439        this.seq++;
440        request.seq = this.seq;
441        request.type = "request";
442        return this.executeCommand(request as T);
443    }
444
445    public event<T extends object>(body: T, eventName: string) {
446        this.events.push(ts.server.toEvent(eventName, body));
447        super.event(body, eventName);
448    }
449
450    public clearMessages() {
451        ts.clear(this.events);
452        this.testhost.clearOutput();
453    }
454
455    private baseline<T extends protocol.Request | ts.server.HandlerResponse>(type: "request" | "response", requestOrResult: T): T {
456        if (!this.logger.hasLevel(ts.server.LogLevel.verbose)) return requestOrResult;
457        if (type === "request") this.logger.info(`request:${ts.server.indent(JSON.stringify(requestOrResult, undefined, 2))}`);
458        this.testhost.baselineHost(type === "request" ? "Before request" : "After request");
459        if (type === "response") this.logger.info(`response:${ts.server.indent(JSON.stringify(requestOrResult, undefined, 2))}`);
460        return requestOrResult;
461    }
462}
463
464export function createSession(host: ts.server.ServerHost, opts: Partial<TestSessionOptions> = {}) {
465    if (opts.typingsInstaller === undefined) {
466        opts.typingsInstaller = new TestTypingsInstaller("/a/data/", /*throttleLimit*/ 5, host);
467    }
468
469    if (opts.eventHandler !== undefined) {
470        opts.canUseEvents = true;
471    }
472
473    const sessionOptions: TestSessionOptions = {
474        host,
475        cancellationToken: ts.server.nullCancellationToken,
476        useSingleInferredProject: false,
477        useInferredProjectPerProjectRoot: false,
478        typingsInstaller: undefined!, // TODO: GH#18217
479        byteLength: Utils.byteLength,
480        hrtime: process.hrtime,
481        logger: opts.logger || createHasErrorMessageLogger(),
482        canUseEvents: false
483    };
484
485    return new TestSession({ ...sessionOptions, ...opts });
486}
487
488export function createSessionWithEventTracking<T extends ts.server.ProjectServiceEvent>(host: ts.server.ServerHost, eventNames: T["eventName"] | T["eventName"][], opts: Partial<TestSessionOptions> = {}) {
489    const events: T[] = [];
490    const session = createSession(host, {
491        eventHandler: e => {
492            if (ts.isArray(eventNames) ? eventNames.some(eventName => e.eventName === eventName) : eventNames === e.eventName) {
493                events.push(e as T);
494            }
495        },
496        ...opts
497    });
498
499    return { session, events };
500}
501
502export function createSessionWithDefaultEventHandler<T extends protocol.AnyEvent>(host: TestServerHost, eventNames: T["event"] | T["event"][], opts: Partial<TestSessionOptions> = {}) {
503    const session = createSession(host, { canUseEvents: true, ...opts });
504
505    return {
506        session,
507        getEvents,
508        clearEvents
509    };
510
511    function getEvents() {
512        return ts.mapDefined(host.getOutput(), s => {
513            const e = mapOutputToJson(s);
514            return (ts.isArray(eventNames) ? eventNames.some(eventName => e.event === eventName) : e.event === eventNames) ? e as T : undefined;
515        });
516    }
517
518    function clearEvents() {
519        session.clearMessages();
520    }
521}
522
523export interface TestProjectServiceOptions extends ts.server.ProjectServiceOptions {
524    logger: Logger;
525}
526
527export class TestProjectService extends ts.server.ProjectService {
528    public testhost: TestSessionAndServiceHost;
529    constructor(host: TestServerHost, public logger: Logger, cancellationToken: ts.HostCancellationToken, useSingleInferredProject: boolean,
530        typingsInstaller: ts.server.ITypingsInstaller, opts: Partial<TestProjectServiceOptions> = {}) {
531        super({
532            host,
533            logger,
534            session: undefined,
535            cancellationToken,
536            useSingleInferredProject,
537            useInferredProjectPerProjectRoot: false,
538            typingsInstaller,
539            typesMapLocation: customTypesMap.path,
540            ...opts
541        });
542        this.testhost = patchHostTimeouts(
543            ts.TestFSWithWatch.changeToHostTrackingWrittenFiles(this.host as TestServerHost),
544            this
545        );
546        this.testhost.baselineHost("Creating project service");
547    }
548
549    checkNumberOfProjects(count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) {
550        checkNumberOfProjects(this, count);
551    }
552}
553
554export function createProjectService(host: TestServerHost, options?: Partial<TestProjectServiceOptions>) {
555    const cancellationToken = options?.cancellationToken || ts.server.nullCancellationToken;
556    const logger = options?.logger || createHasErrorMessageLogger();
557    const useSingleInferredProject = options?.useSingleInferredProject !== undefined ? options.useSingleInferredProject : false;
558    return new TestProjectService(host, logger, cancellationToken, useSingleInferredProject, options?.typingsInstaller || ts.server.nullTypingsInstaller, options);
559}
560
561export function checkNumberOfConfiguredProjects(projectService: ts.server.ProjectService, expected: number) {
562    assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`);
563}
564
565export function checkNumberOfExternalProjects(projectService: ts.server.ProjectService, expected: number) {
566    assert.equal(projectService.externalProjects.length, expected, `expected ${expected} external project(s)`);
567}
568
569export function checkNumberOfInferredProjects(projectService: ts.server.ProjectService, expected: number) {
570    assert.equal(projectService.inferredProjects.length, expected, `expected ${expected} inferred project(s)`);
571}
572
573export function checkNumberOfProjects(projectService: ts.server.ProjectService, count: { inferredProjects?: number, configuredProjects?: number, externalProjects?: number }) {
574    checkNumberOfConfiguredProjects(projectService, count.configuredProjects || 0);
575    checkNumberOfExternalProjects(projectService, count.externalProjects || 0);
576    checkNumberOfInferredProjects(projectService, count.inferredProjects || 0);
577}
578
579export function configuredProjectAt(projectService: ts.server.ProjectService, index: number) {
580    const values = projectService.configuredProjects.values();
581    while (index > 0) {
582        const iterResult = values.next();
583        if (iterResult.done) return ts.Debug.fail("Expected a result.");
584        index--;
585    }
586    const iterResult = values.next();
587    if (iterResult.done) return ts.Debug.fail("Expected a result.");
588    return iterResult.value;
589}
590
591export function checkProjectActualFiles(project: ts.server.Project, expectedFiles: readonly string[]) {
592    checkArray(`${ts.server.ProjectKind[project.projectKind]} project: ${project.getProjectName()}:: actual files`, project.getFileNames(), expectedFiles);
593}
594
595export function checkProjectRootFiles(project: ts.server.Project, expectedFiles: readonly string[]) {
596    checkArray(`${ts.server.ProjectKind[project.projectKind]} project: ${project.getProjectName()}::, rootFileNames`, project.getRootFiles(), expectedFiles);
597}
598
599export function mapCombinedPathsInAncestor(dir: string, path2: string, mapAncestor: (ancestor: string) => boolean) {
600    dir = ts.normalizePath(dir);
601    const result: string[] = [];
602    ts.forEachAncestorDirectory(dir, ancestor => {
603        if (mapAncestor(ancestor)) {
604            result.push(ts.combinePaths(ancestor, path2));
605        }
606    });
607    return result;
608}
609
610export function getRootsToWatchWithAncestorDirectory(dir: string, path2: string) {
611    return mapCombinedPathsInAncestor(dir, path2, ancestor => ancestor.split(ts.directorySeparator).length > 4);
612}
613
614export const nodeModules = "node_modules";
615export function getNodeModuleDirectories(dir: string) {
616    return getRootsToWatchWithAncestorDirectory(dir, nodeModules);
617}
618
619export const nodeModulesAtTypes = "node_modules/@types";
620export function getTypeRootsFromLocation(currentDirectory: string) {
621    return getRootsToWatchWithAncestorDirectory(currentDirectory, nodeModulesAtTypes);
622}
623
624export function getConfigFilesToWatch(folder: string) {
625    return [
626        ...getRootsToWatchWithAncestorDirectory(folder, "tsconfig.json"),
627        ...getRootsToWatchWithAncestorDirectory(folder, "jsconfig.json")
628    ];
629}
630
631export function protocolLocationFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.Location {
632    const start = nthIndexOf(str, substring, options ? options.index : 0);
633    ts.Debug.assert(start !== -1);
634    return protocolToLocation(str)(start);
635}
636
637export function protocolToLocation(text: string): (pos: number) => protocol.Location {
638    const lineStarts = ts.computeLineStarts(text);
639    return pos => {
640        const x = ts.computeLineAndCharacterOfPosition(lineStarts, pos);
641        return { line: x.line + 1, offset: x.character + 1 };
642    };
643}
644
645export function protocolTextSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): protocol.TextSpan {
646    const span = textSpanFromSubstring(str, substring, options);
647    const toLocation = protocolToLocation(str);
648    return { start: toLocation(span.start), end: toLocation(ts.textSpanEnd(span)) };
649}
650
651export interface DocumentSpanFromSubstring {
652    file: File;
653    text: string;
654    options?: SpanFromSubstringOptions;
655    contextText?: string;
656    contextOptions?: SpanFromSubstringOptions;
657}
658export function protocolFileSpanFromSubstring({ file, text, options }: DocumentSpanFromSubstring): protocol.FileSpan {
659    return { file: file.path, ...protocolTextSpanFromSubstring(file.content, text, options) };
660}
661
662interface FileSpanWithContextFromSubString {
663    file: File;
664    text: string;
665    options?: SpanFromSubstringOptions;
666    contextText?: string;
667    contextOptions?: SpanFromSubstringOptions;
668}
669export function protocolFileSpanWithContextFromSubstring({ contextText, contextOptions, ...rest }: FileSpanWithContextFromSubString): protocol.FileSpanWithContext {
670    const result = protocolFileSpanFromSubstring(rest);
671    const contextSpan = contextText !== undefined ?
672        protocolFileSpanFromSubstring({ file: rest.file, text: contextText, options: contextOptions }) :
673        undefined;
674    return contextSpan ?
675        {
676            ...result,
677            contextStart: contextSpan.start,
678            contextEnd: contextSpan.end
679        } :
680        result;
681}
682
683export interface ProtocolTextSpanWithContextFromString {
684    fileText: string;
685    text: string;
686    options?: SpanFromSubstringOptions;
687    contextText?: string;
688    contextOptions?: SpanFromSubstringOptions;
689}
690export function protocolTextSpanWithContextFromSubstring({ fileText, text, options, contextText, contextOptions }: ProtocolTextSpanWithContextFromString): protocol.TextSpanWithContext {
691    const span = textSpanFromSubstring(fileText, text, options);
692    const toLocation = protocolToLocation(fileText);
693    const contextSpan = contextText !== undefined ? textSpanFromSubstring(fileText, contextText, contextOptions) : undefined;
694    return {
695        start: toLocation(span.start),
696        end: toLocation(ts.textSpanEnd(span)),
697        ...contextSpan && {
698            contextStart: toLocation(contextSpan.start),
699            contextEnd: toLocation(ts.textSpanEnd(contextSpan))
700        }
701    };
702}
703
704export interface ProtocolRenameSpanFromSubstring extends ProtocolTextSpanWithContextFromString {
705    prefixSuffixText?: {
706        readonly prefixText?: string;
707        readonly suffixText?: string;
708    };
709}
710export function protocolRenameSpanFromSubstring({ prefixSuffixText, ...rest }: ProtocolRenameSpanFromSubstring): protocol.RenameTextSpan {
711    return {
712        ...protocolTextSpanWithContextFromSubstring(rest),
713        ...prefixSuffixText
714    };
715}
716
717export function textSpanFromSubstring(str: string, substring: string, options?: SpanFromSubstringOptions): ts.TextSpan {
718    const start = nthIndexOf(str, substring, options ? options.index : 0);
719    ts.Debug.assert(start !== -1);
720    return ts.createTextSpan(start, substring.length);
721}
722
723export function protocolFileLocationFromSubstring(file: File, substring: string, options?: SpanFromSubstringOptions): protocol.FileLocationRequestArgs {
724    return { file: file.path, ...protocolLocationFromSubstring(file.content, substring, options) };
725}
726
727export interface SpanFromSubstringOptions {
728    readonly index: number;
729}
730
731function nthIndexOf(str: string, substr: string, n: number): number {
732    let index = -1;
733    for (; n >= 0; n--) {
734        index = str.indexOf(substr, index + 1);
735        if (index === -1) return -1;
736    }
737    return index;
738}
739
740/**
741 * Test server cancellation token used to mock host token cancellation requests.
742 * The cancelAfterRequest constructor param specifies how many isCancellationRequested() calls
743 * should be made before canceling the token. The id of the request to cancel should be set with
744 * setRequestToCancel();
745 */
746export class TestServerCancellationToken implements ts.server.ServerCancellationToken {
747    private currentId: number | undefined = -1;
748    private requestToCancel = -1;
749    private isCancellationRequestedCount = 0;
750
751    constructor(private cancelAfterRequest = 0) {
752    }
753
754    setRequest(requestId: number) {
755        this.currentId = requestId;
756    }
757
758    setRequestToCancel(requestId: number) {
759        this.resetToken();
760        this.requestToCancel = requestId;
761    }
762
763    resetRequest(requestId: number) {
764        assert.equal(requestId, this.currentId, "unexpected request id in cancellation");
765        this.currentId = undefined;
766    }
767
768    isCancellationRequested() {
769        this.isCancellationRequestedCount++;
770        // If the request id is the request to cancel and isCancellationRequestedCount
771        // has been met then cancel the request. Ex: cancel the request if it is a
772        // nav bar request & isCancellationRequested() has already been called three times.
773        return this.requestToCancel === this.currentId && this.isCancellationRequestedCount >= this.cancelAfterRequest;
774    }
775
776    resetToken() {
777        this.currentId = -1;
778        this.isCancellationRequestedCount = 0;
779        this.requestToCancel = -1;
780    }
781}
782
783export function makeSessionRequest<T>(command: string, args: T): protocol.Request {
784    return {
785        seq: 0,
786        type: "request",
787        command,
788        arguments: args
789    };
790}
791
792export function executeSessionRequest<TRequest extends protocol.Request, TResponse extends protocol.Response>(session: ts.server.Session, command: TRequest["command"], args: TRequest["arguments"]): TResponse["body"] {
793    return session.executeCommand(makeSessionRequest(command, args)).response as TResponse["body"];
794}
795
796export function executeSessionRequestNoResponse<TRequest extends protocol.Request>(session: ts.server.Session, command: TRequest["command"], args: TRequest["arguments"]): void {
797    session.executeCommand(makeSessionRequest(command, args));
798}
799
800export function openFilesForSession(files: readonly (File | { readonly file: File | string, readonly projectRootPath: string, content?: string })[], session: ts.server.Session): void {
801    for (const file of files) {
802        session.executeCommand(makeSessionRequest<protocol.OpenRequestArgs>(CommandNames.Open,
803            "projectRootPath" in file ? { file: typeof file.file === "string" ? file.file : file.file.path, projectRootPath: file.projectRootPath } : { file: file.path })); // eslint-disable-line local/no-in-operator
804    }
805}
806
807export function closeFilesForSession(files: readonly File[], session: ts.server.Session): void {
808    for (const file of files) {
809        session.executeCommand(makeSessionRequest<protocol.FileRequestArgs>(CommandNames.Close, { file: file.path }));
810    }
811}
812
813export interface MakeReferenceItem extends DocumentSpanFromSubstring {
814    isDefinition?: boolean;
815    isWriteAccess?: boolean;
816    lineText?: string;
817}
818
819export function makeReferenceItem({ isDefinition, isWriteAccess, lineText, ...rest }: MakeReferenceItem): protocol.ReferencesResponseItem {
820    return {
821        ...protocolFileSpanWithContextFromSubstring(rest),
822        isDefinition,
823        isWriteAccess: isWriteAccess === undefined ? !!isDefinition : isWriteAccess,
824        lineText,
825    };
826}
827
828export interface VerifyGetErrRequestBase {
829    session: TestSession;
830    host: TestServerHost;
831    existingTimeouts?: number;
832}
833export interface VerifyGetErrRequest extends VerifyGetErrRequestBase {
834    files: readonly (string | File)[];
835    skip?: CheckAllErrors["skip"];
836}
837export function verifyGetErrRequest(request: VerifyGetErrRequest) {
838    const { session, files } = request;
839    session.executeCommandSeq<protocol.GeterrRequest>({
840        command: protocol.CommandTypes.Geterr,
841        arguments: { delay: 0, files: files.map(filePath) }
842    });
843    checkAllErrors(request);
844}
845
846interface SkipErrors { semantic?: true; suggestion?: true }
847export interface CheckAllErrors extends VerifyGetErrRequestBase {
848    files: readonly any[];
849    skip?: readonly (SkipErrors | undefined)[];
850}
851function checkAllErrors({ session, host, existingTimeouts, files, skip }: CheckAllErrors) {
852    ts.Debug.assert(session.logger.logs.length);
853    for (let i = 0; i < files.length; i++) {
854        if (existingTimeouts !== undefined) {
855            host.checkTimeoutQueueLength(existingTimeouts + 1);
856            host.runQueuedTimeoutCallbacks(host.getNextTimeoutId() - 1);
857        }
858        else {
859            host.checkTimeoutQueueLengthAndRun(1);
860        }
861        if (!skip?.[i]?.semantic) host.runQueuedImmediateCallbacks(1);
862        if (!skip?.[i]?.suggestion) host.runQueuedImmediateCallbacks(1);
863    }
864}
865
866function filePath(file: string | File) {
867    return ts.isString(file) ? file : file.path;
868}
869
870function verifyErrorsUsingGeterr({scenario, subScenario, allFiles, openFiles, getErrRequest }: VerifyGetErrScenario) {
871    it("verifies the errors in open file", () => {
872        const host = createServerHost([...allFiles(), libFile]);
873        const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) });
874        openFilesForSession(openFiles(), session);
875
876        verifyGetErrRequest({ session, host, files: getErrRequest() });
877        baselineTsserverLogs(scenario, `${subScenario} getErr`, session);
878    });
879}
880
881function verifyErrorsUsingGeterrForProject({ scenario, subScenario, allFiles, openFiles, getErrForProjectRequest }: VerifyGetErrScenario) {
882    it("verifies the errors in projects", () => {
883        const host = createServerHost([...allFiles(), libFile]);
884        const session = createSession(host, { canUseEvents: true, logger: createLoggerWithInMemoryLogs(host) });
885        openFilesForSession(openFiles(), session);
886
887        for (const expected of getErrForProjectRequest()) {
888            session.executeCommandSeq<protocol.GeterrForProjectRequest>({
889                command: protocol.CommandTypes.GeterrForProject,
890                arguments: { delay: 0, file: filePath(expected.project) }
891            });
892            checkAllErrors({ session, host, files: expected.files });
893        }
894        baselineTsserverLogs(scenario, `${subScenario} geterrForProject`, session);
895    });
896}
897
898function verifyErrorsUsingSyncMethods({ scenario, subScenario, allFiles, openFiles, syncDiagnostics }: VerifyGetErrScenario) {
899    it("verifies the errors using sync commands", () => {
900        const host = createServerHost([...allFiles(), libFile]);
901        const session = createSession(host, { logger: createLoggerWithInMemoryLogs(host) });
902        openFilesForSession(openFiles(), session);
903        for (const { file, project } of syncDiagnostics()) {
904            const reqArgs = { file: filePath(file), projectFileName: project && filePath(project) };
905            session.executeCommandSeq<protocol.SyntacticDiagnosticsSyncRequest>({
906                command: protocol.CommandTypes.SyntacticDiagnosticsSync,
907                arguments: reqArgs
908            });
909            session.executeCommandSeq<protocol.SemanticDiagnosticsSyncRequest>({
910                command: protocol.CommandTypes.SemanticDiagnosticsSync,
911                arguments: reqArgs
912            });
913            session.executeCommandSeq<protocol.SuggestionDiagnosticsSyncRequest>({
914                command: protocol.CommandTypes.SuggestionDiagnosticsSync,
915                arguments: reqArgs
916            });
917        }
918        baselineTsserverLogs(scenario, `${subScenario} gerErr with sync commands`, session);
919    });
920}
921
922export interface GetErrForProjectDiagnostics {
923    project: string | File;
924    files: readonly (string | File)[];
925    skip?: CheckAllErrors["skip"];
926}
927export interface SyncDiagnostics {
928    file: string | File;
929    project?: string | File;
930}
931export interface VerifyGetErrScenario {
932    scenario: string;
933    subScenario: string;
934    allFiles: () => readonly File[];
935    openFiles: () => readonly File[];
936    getErrRequest: () => VerifyGetErrRequest["files"];
937    getErrForProjectRequest: () => readonly GetErrForProjectDiagnostics[];
938    syncDiagnostics: () => readonly SyncDiagnostics[];
939}
940export function verifyGetErrScenario(scenario: VerifyGetErrScenario) {
941    verifyErrorsUsingGeterr(scenario);
942    verifyErrorsUsingGeterrForProject(scenario);
943    verifyErrorsUsingSyncMethods(scenario);
944}
945export function verifyDynamic(service: ts.server.ProjectService, path: string) {
946    const info = ts.Debug.checkDefined(service.filenameToScriptInfo.get(path), `Expected ${path} in :: ${JSON.stringify(ts.arrayFrom(service.filenameToScriptInfo.entries(), ([key, f]) => ({ key, fileName: f.fileName, path: f.path })))}`);
947    assert.isTrue(info.isDynamic);
948}
949
950export function createHostWithSolutionBuild(files: readonly ts.TestFSWithWatch.FileOrFolderOrSymLink[], rootNames: readonly string[]) {
951    const host = ts.projectSystem.createServerHost(files);
952    // ts build should succeed
953    ts.tscWatch.ensureErrorFreeBuild(host, rootNames);
954    return host;
955}