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