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