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}