1namespace ts.projectSystem { 2 describe("unittests:: tsserver:: events:: ProjectsUpdatedInBackground", () => { 3 function verifyFiles(caption: string, actual: readonly string[], expected: readonly string[]) { 4 assert.equal(actual.length, expected.length, `Incorrect number of ${caption}. Actual: ${actual} Expected: ${expected}`); 5 const seen = new Map<string, true>(); 6 forEach(actual, f => { 7 assert.isFalse(seen.has(f), `${caption}: Found duplicate ${f}. Actual: ${actual} Expected: ${expected}`); 8 seen.set(f, true); 9 assert.isTrue(contains(expected, f), `${caption}: Expected not to contain ${f}. Actual: ${actual} Expected: ${expected}`); 10 }); 11 } 12 13 function createVerifyInitialOpen(session: TestSession, verifyProjectsUpdatedInBackgroundEventHandler: (events: server.ProjectsUpdatedInBackgroundEvent[]) => void) { 14 return (file: File) => { 15 session.executeCommandSeq({ 16 command: server.CommandNames.Open, 17 arguments: { 18 file: file.path 19 } 20 } as protocol.OpenRequest); 21 verifyProjectsUpdatedInBackgroundEventHandler([]); 22 }; 23 } 24 25 interface ProjectsUpdatedInBackgroundEventVerifier { 26 session: TestSession; 27 verifyProjectsUpdatedInBackgroundEventHandler(events: server.ProjectsUpdatedInBackgroundEvent[]): void; 28 verifyInitialOpen(file: File): void; 29 } 30 31 function verifyProjectsUpdatedInBackgroundEvent(scenario: string, createSession: (host: TestServerHost, logger?: Logger) => ProjectsUpdatedInBackgroundEventVerifier) { 32 it("when adding new file", () => { 33 const commonFile1: File = { 34 path: "/a/b/file1.ts", 35 content: "export var x = 10;" 36 }; 37 const commonFile2: File = { 38 path: "/a/b/file2.ts", 39 content: "export var y = 10;" 40 }; 41 const commonFile3: File = { 42 path: "/a/b/file3.ts", 43 content: "export var z = 10;" 44 }; 45 const configFile: File = { 46 path: "/a/b/tsconfig.json", 47 content: `{}` 48 }; 49 const openFiles = [commonFile1.path]; 50 const host = createServerHost([commonFile1, libFile, configFile]); 51 const { verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); 52 verifyInitialOpen(commonFile1); 53 54 host.writeFile(commonFile2.path, commonFile2.content); 55 host.runQueuedTimeoutCallbacks(); 56 verifyProjectsUpdatedInBackgroundEventHandler([{ 57 eventName: server.ProjectsUpdatedInBackgroundEvent, 58 data: { 59 openFiles 60 } 61 }]); 62 63 host.writeFile(commonFile3.path, commonFile3.content); 64 host.runQueuedTimeoutCallbacks(); 65 verifyProjectsUpdatedInBackgroundEventHandler([{ 66 eventName: server.ProjectsUpdatedInBackgroundEvent, 67 data: { 68 openFiles 69 } 70 }]); 71 }); 72 73 describe("with --out or --outFile setting", () => { 74 function verifyEventWithOutSettings(compilerOptions: CompilerOptions = {}) { 75 const config: File = { 76 path: "/a/tsconfig.json", 77 content: JSON.stringify({ 78 compilerOptions 79 }) 80 }; 81 82 const f1: File = { 83 path: "/a/a.ts", 84 content: "export let x = 1" 85 }; 86 const f2: File = { 87 path: "/a/b.ts", 88 content: "export let y = 1" 89 }; 90 91 const openFiles = [f1.path]; 92 const files = [f1, config, libFile]; 93 const host = createServerHost(files); 94 const { verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host); 95 verifyInitialOpen(f1); 96 97 host.writeFile(f2.path, f2.content); 98 host.runQueuedTimeoutCallbacks(); 99 100 verifyProjectsUpdatedInBackgroundEventHandler([{ 101 eventName: server.ProjectsUpdatedInBackgroundEvent, 102 data: { 103 openFiles 104 } 105 }]); 106 107 host.writeFile(f2.path, "export let x = 11"); 108 host.runQueuedTimeoutCallbacks(); 109 verifyProjectsUpdatedInBackgroundEventHandler([{ 110 eventName: server.ProjectsUpdatedInBackgroundEvent, 111 data: { 112 openFiles 113 } 114 }]); 115 } 116 117 it("when both options are not set", () => { 118 verifyEventWithOutSettings(); 119 }); 120 121 it("when --out is set", () => { 122 const outJs = "/a/out.js"; 123 verifyEventWithOutSettings({ out: outJs }); 124 }); 125 126 it("when --outFile is set", () => { 127 const outJs = "/a/out.js"; 128 verifyEventWithOutSettings({ outFile: outJs }); 129 }); 130 }); 131 132 describe("with modules and configured project", () => { 133 const file1Consumer1Path = "/a/b/file1Consumer1.ts"; 134 const moduleFile1Path = "/a/b/moduleFile1.ts"; 135 const configFilePath = "/a/b/tsconfig.json"; 136 interface InitialStateParams { 137 /** custom config file options */ 138 configObj?: any; 139 /** Additional files and folders to add */ 140 getAdditionalFileOrFolder?(): File[]; 141 /** initial list of files to reload in fs and first file in this list being the file to open */ 142 firstReloadFileList?: string[]; 143 } 144 function getInitialState({ configObj = {}, getAdditionalFileOrFolder, firstReloadFileList }: InitialStateParams = {}) { 145 const moduleFile1: File = { 146 path: moduleFile1Path, 147 content: "export function Foo() { };", 148 }; 149 150 const file1Consumer1: File = { 151 path: file1Consumer1Path, 152 content: `import {Foo} from "./moduleFile1"; export var y = 10;`, 153 }; 154 155 const file1Consumer2: File = { 156 path: "/a/b/file1Consumer2.ts", 157 content: `import {Foo} from "./moduleFile1"; let z = 10;`, 158 }; 159 160 const moduleFile2: File = { 161 path: "/a/b/moduleFile2.ts", 162 content: `export var Foo4 = 10;`, 163 }; 164 165 const globalFile3: File = { 166 path: "/a/b/globalFile3.ts", 167 content: `interface GlobalFoo { age: number }` 168 }; 169 170 const additionalFiles = getAdditionalFileOrFolder ? getAdditionalFileOrFolder() : []; 171 const configFile = { 172 path: configFilePath, 173 content: JSON.stringify(configObj || { compilerOptions: {} }) 174 }; 175 176 const files: File[] = [file1Consumer1, moduleFile1, file1Consumer2, moduleFile2, ...additionalFiles, globalFile3, libFile, configFile]; 177 178 const filesToReload = firstReloadFileList && getFiles(firstReloadFileList) || files; 179 const host = createServerHost([filesToReload[0], configFile]); 180 181 // Initial project creation 182 const { session, verifyProjectsUpdatedInBackgroundEventHandler, verifyInitialOpen } = createSession(host); 183 const openFiles = [filesToReload[0].path]; 184 verifyInitialOpen(filesToReload[0]); 185 186 // Since this is first event, it will have all the files 187 filesToReload.forEach(f => host.ensureFileOrFolder(f)); 188 if (!firstReloadFileList) host.runQueuedTimeoutCallbacks(); // Invalidated module resolutions to schedule project update 189 verifyProjectsUpdatedInBackgroundEvent(); 190 191 return { 192 host, 193 moduleFile1, file1Consumer1, file1Consumer2, moduleFile2, globalFile3, configFile, 194 updateContentOfOpenFile, 195 verifyNoProjectsUpdatedInBackgroundEvent, 196 verifyProjectsUpdatedInBackgroundEvent 197 }; 198 199 function getFiles(filelist: string[]) { 200 return map(filelist, getFile); 201 } 202 203 function getFile(fileName: string) { 204 return find(files, file => file.path === fileName)!; 205 } 206 207 function verifyNoProjectsUpdatedInBackgroundEvent() { 208 host.runQueuedTimeoutCallbacks(); 209 verifyProjectsUpdatedInBackgroundEventHandler([]); 210 } 211 212 function verifyProjectsUpdatedInBackgroundEvent() { 213 host.runQueuedTimeoutCallbacks(); 214 verifyProjectsUpdatedInBackgroundEventHandler([{ 215 eventName: server.ProjectsUpdatedInBackgroundEvent, 216 data: { 217 openFiles 218 } 219 }]); 220 } 221 222 function updateContentOfOpenFile(file: File, newContent: string) { 223 session.executeCommandSeq<protocol.ChangeRequest>({ 224 command: server.CommandNames.Change, 225 arguments: { 226 file: file.path, 227 insertString: newContent, 228 endLine: 1, 229 endOffset: file.content.length, 230 line: 1, 231 offset: 1 232 } 233 }); 234 file.content = newContent; 235 } 236 } 237 238 it("should contains only itself if a module file's shape didn't change, and all files referencing it if its shape changed", () => { 239 const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); 240 241 // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` 242 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); 243 verifyProjectsUpdatedInBackgroundEvent(); 244 245 // Change the content of moduleFile1 to `export var T: number;export function Foo() { console.log('hi'); };` 246 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { console.log('hi'); };`); 247 verifyProjectsUpdatedInBackgroundEvent(); 248 }); 249 250 it("should be up-to-date with the reference map changes", () => { 251 const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyProjectsUpdatedInBackgroundEvent, verifyNoProjectsUpdatedInBackgroundEvent } = getInitialState(); 252 253 // Change file1Consumer1 content to `export let y = Foo();` 254 updateContentOfOpenFile(file1Consumer1, "export let y = Foo();"); 255 verifyNoProjectsUpdatedInBackgroundEvent(); 256 257 // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` 258 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); 259 verifyProjectsUpdatedInBackgroundEvent(); 260 261 // Add the import statements back to file1Consumer1 262 updateContentOfOpenFile(file1Consumer1, `import {Foo} from "./moduleFile1";let y = Foo();`); 263 verifyNoProjectsUpdatedInBackgroundEvent(); 264 265 // Change the content of moduleFile1 to `export var T: number;export var T2: string;export function Foo() { };` 266 host.writeFile(moduleFile1.path, `export var T: number;export var T2: string;export function Foo() { };`); 267 verifyProjectsUpdatedInBackgroundEvent(); 268 269 // Multiple file edits in one go: 270 271 // Change file1Consumer1 content to `export let y = Foo();` 272 // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` 273 updateContentOfOpenFile(file1Consumer1, `export let y = Foo();`); 274 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); 275 verifyProjectsUpdatedInBackgroundEvent(); 276 }); 277 278 it("should be up-to-date with deleted files", () => { 279 const { host, moduleFile1, file1Consumer2, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); 280 281 // Change the content of moduleFile1 to `export var T: number;export function Foo() { };` 282 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); 283 284 // Delete file1Consumer2 285 host.deleteFile(file1Consumer2.path); 286 verifyProjectsUpdatedInBackgroundEvent(); 287 }); 288 289 it("should be up-to-date with newly created files", () => { 290 const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent, } = getInitialState(); 291 292 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); 293 host.writeFile("/a/b/file1Consumer3.ts", `import {Foo} from "./moduleFile1"; let y = Foo();`); 294 verifyProjectsUpdatedInBackgroundEvent(); 295 }); 296 297 it("should detect changes in non-root files", () => { 298 const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ 299 configObj: { files: [file1Consumer1Path] }, 300 }); 301 302 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); 303 verifyProjectsUpdatedInBackgroundEvent(); 304 305 // change file1 internal, and verify only file1 is affected 306 host.writeFile(moduleFile1.path, moduleFile1.content + "var T1: number;"); 307 verifyProjectsUpdatedInBackgroundEvent(); 308 }); 309 310 it("should return all files if a global file changed shape", () => { 311 const { host, globalFile3, verifyProjectsUpdatedInBackgroundEvent } = getInitialState(); 312 313 host.writeFile(globalFile3.path, globalFile3.content + "var T2: string;"); 314 verifyProjectsUpdatedInBackgroundEvent(); 315 }); 316 317 it("should always return the file itself if '--isolatedModules' is specified", () => { 318 const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ 319 configObj: { compilerOptions: { isolatedModules: true } } 320 }); 321 322 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); 323 verifyProjectsUpdatedInBackgroundEvent(); 324 }); 325 326 it("should always return the file itself if '--out' or '--outFile' is specified", () => { 327 const outFilePath = "/a/b/out.js"; 328 const { host, moduleFile1, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ 329 configObj: { compilerOptions: { module: "system", outFile: outFilePath } } 330 }); 331 332 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); 333 verifyProjectsUpdatedInBackgroundEvent(); 334 }); 335 336 it("should return cascaded affected file list", () => { 337 const file1Consumer1Consumer1: File = { 338 path: "/a/b/file1Consumer1Consumer1.ts", 339 content: `import {y} from "./file1Consumer1";` 340 }; 341 const { host, moduleFile1, file1Consumer1, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ 342 getAdditionalFileOrFolder: () => [file1Consumer1Consumer1] 343 }); 344 345 updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T: number;"); 346 verifyNoProjectsUpdatedInBackgroundEvent(); 347 348 // Doesnt change the shape of file1Consumer1 349 host.writeFile(moduleFile1.path, `export var T: number;export function Foo() { };`); 350 verifyProjectsUpdatedInBackgroundEvent(); 351 352 // Change both files before the timeout 353 updateContentOfOpenFile(file1Consumer1, file1Consumer1.content + "export var T2: number;"); 354 host.writeFile(moduleFile1.path, `export var T2: number;export function Foo() { };`); 355 verifyProjectsUpdatedInBackgroundEvent(); 356 }); 357 358 it("should work fine for files with circular references", () => { 359 const file1: File = { 360 path: "/a/b/file1.ts", 361 content: ` 362 /// <reference path="./file2.ts" /> 363 export var t1 = 10;` 364 }; 365 const file2: File = { 366 path: "/a/b/file2.ts", 367 content: ` 368 /// <reference path="./file1.ts" /> 369 export var t2 = 10;` 370 }; 371 const { host, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ 372 getAdditionalFileOrFolder: () => [file1, file2], 373 firstReloadFileList: [file1.path, libFile.path, file2.path, configFilePath] 374 }); 375 376 host.writeFile(file2.path, file2.content + "export var t3 = 10;"); 377 verifyProjectsUpdatedInBackgroundEvent(); 378 }); 379 380 it("should detect removed code file", () => { 381 const referenceFile1: File = { 382 path: "/a/b/referenceFile1.ts", 383 content: ` 384 /// <reference path="./moduleFile1.ts" /> 385 export var x = Foo();` 386 }; 387 const { host, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ 388 getAdditionalFileOrFolder: () => [referenceFile1], 389 firstReloadFileList: [referenceFile1.path, libFile.path, moduleFile1Path, configFilePath] 390 }); 391 392 host.deleteFile(moduleFile1Path); 393 verifyProjectsUpdatedInBackgroundEvent(); 394 }); 395 396 it("should detect non-existing code file", () => { 397 const referenceFile1: File = { 398 path: "/a/b/referenceFile1.ts", 399 content: ` 400 /// <reference path="./moduleFile2.ts" /> 401 export var x = Foo();` 402 }; 403 const { host, moduleFile2, updateContentOfOpenFile, verifyNoProjectsUpdatedInBackgroundEvent, verifyProjectsUpdatedInBackgroundEvent } = getInitialState({ 404 getAdditionalFileOrFolder: () => [referenceFile1], 405 firstReloadFileList: [referenceFile1.path, libFile.path, configFilePath] 406 }); 407 408 updateContentOfOpenFile(referenceFile1, referenceFile1.content + "export var yy = Foo();"); 409 verifyNoProjectsUpdatedInBackgroundEvent(); 410 411 // Create module File2 and see both files are saved 412 host.writeFile(moduleFile2.path, moduleFile2.content); 413 verifyProjectsUpdatedInBackgroundEvent(); 414 }); 415 }); 416 417 describe("resolution when resolution cache size", () => { 418 function verifyWithMaxCacheLimit(subScenario: string, useSlashRootAsSomeNotRootFolderInUserDirectory: boolean) { 419 it(subScenario, () => { 420 const rootFolder = useSlashRootAsSomeNotRootFolderInUserDirectory ? "/user/username/rootfolder/otherfolder/" : "/"; 421 const file1: File = { 422 path: rootFolder + "a/b/project/file1.ts", 423 content: 'import a from "file2"' 424 }; 425 const file2: File = { 426 path: rootFolder + "a/b/node_modules/file2.d.ts", 427 content: "export class a { }" 428 }; 429 const file3: File = { 430 path: rootFolder + "a/b/project/file3.ts", 431 content: "export class c { }" 432 }; 433 const configFile: File = { 434 path: rootFolder + "a/b/project/tsconfig.json", 435 content: JSON.stringify({ compilerOptions: { typeRoots: [] } }) 436 }; 437 438 const openFiles = [file1.path]; 439 const host = createServerHost([file1, file3, libFile, configFile]); 440 const { session, verifyInitialOpen, verifyProjectsUpdatedInBackgroundEventHandler } = createSession(host, createLoggerWithInMemoryLogs(host)); 441 verifyInitialOpen(file1); 442 443 file3.content += "export class d {}"; 444 host.writeFile(file3.path, file3.content); 445 host.checkTimeoutQueueLengthAndRun(2); 446 447 // Since this is first event 448 verifyProjectsUpdatedInBackgroundEventHandler([{ 449 eventName: server.ProjectsUpdatedInBackgroundEvent, 450 data: { 451 openFiles 452 } 453 }]); 454 455 host.writeFile(file2.path, file2.content); 456 host.runQueuedTimeoutCallbacks(); // For invalidation 457 host.runQueuedTimeoutCallbacks(); // For actual update 458 459 verifyProjectsUpdatedInBackgroundEventHandler(useSlashRootAsSomeNotRootFolderInUserDirectory ? [{ 460 eventName: server.ProjectsUpdatedInBackgroundEvent, 461 data: { 462 openFiles 463 } 464 }] : []); 465 baselineTsserverLogs("projectUpdatedInBackground", `${scenario} and ${subScenario}`, session); 466 }); 467 } 468 verifyWithMaxCacheLimit("project is not at root level", /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ true); 469 verifyWithMaxCacheLimit("project is at root level", /*useSlashRootAsSomeNotRootFolderInUserDirectory*/ false); 470 }); 471 } 472 473 describe("when event handler is set in the session", () => { 474 verifyProjectsUpdatedInBackgroundEvent("when event handler is set in the session", createSessionWithProjectChangedEventHandler); 475 476 function createSessionWithProjectChangedEventHandler(host: TestServerHost, logger: Logger | undefined): ProjectsUpdatedInBackgroundEventVerifier { 477 const { session, events: projectChangedEvents } = createSessionWithEventTracking<server.ProjectsUpdatedInBackgroundEvent>( 478 host, 479 server.ProjectsUpdatedInBackgroundEvent, 480 logger && { logger } 481 ); 482 return { 483 session, 484 verifyProjectsUpdatedInBackgroundEventHandler, 485 verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) 486 }; 487 488 function eventToString(event: server.ProjectsUpdatedInBackgroundEvent) { 489 return JSON.stringify(event && { eventName: event.eventName, data: event.data }); 490 } 491 492 function eventsToString(events: readonly server.ProjectsUpdatedInBackgroundEvent[]) { 493 return "[" + map(events, eventToString).join(",") + "]"; 494 } 495 496 function verifyProjectsUpdatedInBackgroundEventHandler(expectedEvents: readonly server.ProjectsUpdatedInBackgroundEvent[]) { 497 assert.equal(projectChangedEvents.length, expectedEvents.length, `Incorrect number of events Actual: ${eventsToString(projectChangedEvents)} Expected: ${eventsToString(expectedEvents)}`); 498 forEach(projectChangedEvents, (actualEvent, i) => { 499 const expectedEvent = expectedEvents[i]; 500 assert.strictEqual(actualEvent.eventName, expectedEvent.eventName); 501 verifyFiles("openFiles", actualEvent.data.openFiles, expectedEvent.data.openFiles); 502 }); 503 504 // Verified the events, reset them 505 projectChangedEvents.length = 0; 506 } 507 } 508 }); 509 510 describe("when event handler is not set but session is created with canUseEvents = true", () => { 511 describe("without noGetErrOnBackgroundUpdate, diagnostics for open files are queued", () => { 512 verifyProjectsUpdatedInBackgroundEvent("without noGetErrOnBackgroundUpdate", createSessionThatUsesEvents); 513 }); 514 515 describe("with noGetErrOnBackgroundUpdate, diagnostics for open file are not queued", () => { 516 verifyProjectsUpdatedInBackgroundEvent("with noGetErrOnBackgroundUpdate", (host, logger) => createSessionThatUsesEvents(host, logger, /*noGetErrOnBackgroundUpdate*/ true)); 517 }); 518 519 520 function createSessionThatUsesEvents(host: TestServerHost, logger: Logger | undefined, noGetErrOnBackgroundUpdate?: boolean): ProjectsUpdatedInBackgroundEventVerifier { 521 const { session, getEvents, clearEvents } = createSessionWithDefaultEventHandler<protocol.ProjectsUpdatedInBackgroundEvent>( 522 host, 523 server.ProjectsUpdatedInBackgroundEvent, 524 { noGetErrOnBackgroundUpdate, logger: logger || createHasErrorMessageLogger() } 525 ); 526 527 return { 528 session, 529 verifyProjectsUpdatedInBackgroundEventHandler, 530 verifyInitialOpen: createVerifyInitialOpen(session, verifyProjectsUpdatedInBackgroundEventHandler) 531 }; 532 533 function verifyProjectsUpdatedInBackgroundEventHandler(expected: readonly server.ProjectsUpdatedInBackgroundEvent[]) { 534 const expectedEvents: protocol.ProjectsUpdatedInBackgroundEventBody[] = map(expected, e => { 535 return { 536 openFiles: e.data.openFiles 537 }; 538 }); 539 const events = getEvents(); 540 assert.equal(events.length, expectedEvents.length, `Incorrect number of events Actual: ${map(events, e => e.body)} Expected: ${expectedEvents}`); 541 forEach(events, (actualEvent, i) => { 542 const expectedEvent = expectedEvents[i]; 543 verifyFiles("openFiles", actualEvent.body.openFiles, expectedEvent.openFiles); 544 }); 545 546 // Verified the events, reset them 547 clearEvents(); 548 549 if (events.length) { 550 host.checkTimeoutQueueLength(noGetErrOnBackgroundUpdate ? 0 : 1); // Error checking queued only if not noGetErrOnBackgroundUpdate 551 } 552 } 553 } 554 }); 555 }); 556} 557