1namespace ts.tscWatch { 2 export const projects = `/user/username/projects`; 3 export const projectRoot = `${projects}/myproject`; 4 export import WatchedSystem = TestFSWithWatch.TestServerHost; 5 export type File = TestFSWithWatch.File; 6 export type SymLink = TestFSWithWatch.SymLink; 7 export import libFile = TestFSWithWatch.libFile; 8 export import createWatchedSystem = TestFSWithWatch.createWatchedSystem; 9 export import checkArray = TestFSWithWatch.checkArray; 10 export import checkWatchedFiles = TestFSWithWatch.checkWatchedFiles; 11 export import checkWatchedFilesDetailed = TestFSWithWatch.checkWatchedFilesDetailed; 12 export import checkWatchedDirectories = TestFSWithWatch.checkWatchedDirectories; 13 export import checkWatchedDirectoriesDetailed = TestFSWithWatch.checkWatchedDirectoriesDetailed; 14 export import checkOutputContains = TestFSWithWatch.checkOutputContains; 15 export import checkOutputDoesNotContain = TestFSWithWatch.checkOutputDoesNotContain; 16 17 export const commonFile1: File = { 18 path: "/a/b/commonFile1.ts", 19 content: "let x = 1" 20 }; 21 export const commonFile2: File = { 22 path: "/a/b/commonFile2.ts", 23 content: "let y = 1" 24 }; 25 26 export function checkProgramActualFiles(program: Program, expectedFiles: readonly string[]) { 27 checkArray(`Program actual files`, program.getSourceFiles().map(file => file.fileName), expectedFiles); 28 } 29 30 export function checkProgramRootFiles(program: Program, expectedFiles: readonly string[]) { 31 checkArray(`Program rootFileNames`, program.getRootFileNames(), expectedFiles); 32 } 33 34 export type Watch = WatchOfConfigFile<EmitAndSemanticDiagnosticsBuilderProgram> | WatchOfFilesAndCompilerOptions<EmitAndSemanticDiagnosticsBuilderProgram>; 35 36 export function createWatchOfConfigFile(configFileName: string, system: WatchedSystem, optionsToExtend?: CompilerOptions, watchOptionsToExtend?: WatchOptions) { 37 const compilerHost = createWatchCompilerHostOfConfigFile({ configFileName, optionsToExtend, watchOptionsToExtend, system }); 38 return createWatchProgram(compilerHost); 39 } 40 41 export function createWatchOfFilesAndCompilerOptions(rootFiles: string[], system: WatchedSystem, options: CompilerOptions = {}, watchOptions?: WatchOptions) { 42 const compilerHost = createWatchCompilerHostOfFilesAndCompilerOptions({ rootFiles, options, watchOptions, system }); 43 return createWatchProgram(compilerHost); 44 } 45 46 const elapsedRegex = /^Elapsed:: \d+(?:\.\d+)?ms/; 47 const buildVerboseLogRegEx = /^.+ \- /; 48 export enum HostOutputKind { 49 Log, 50 Diagnostic, 51 WatchDiagnostic 52 } 53 54 export interface HostOutputLog { 55 kind: HostOutputKind.Log; 56 expected: string; 57 caption?: string; 58 } 59 60 export interface HostOutputDiagnostic { 61 kind: HostOutputKind.Diagnostic; 62 diagnostic: Diagnostic | string; 63 } 64 65 export interface HostOutputWatchDiagnostic { 66 kind: HostOutputKind.WatchDiagnostic; 67 diagnostic: Diagnostic | string; 68 } 69 70 export type HostOutput = HostOutputLog | HostOutputDiagnostic | HostOutputWatchDiagnostic; 71 72 export function checkOutputErrors( 73 host: WatchedSystem, 74 expected: readonly HostOutput[], 75 disableConsoleClears?: boolean | undefined 76 ) { 77 let screenClears = 0; 78 const outputs = host.getOutput(); 79 assert.equal(outputs.length, expected.length, JSON.stringify(outputs)); 80 let index = 0; 81 forEach(expected, expected => { 82 switch (expected.kind) { 83 case HostOutputKind.Log: 84 return assertLog(expected); 85 case HostOutputKind.Diagnostic: 86 return assertDiagnostic(expected); 87 case HostOutputKind.WatchDiagnostic: 88 return assertWatchDiagnostic(expected); 89 default: 90 return Debug.assertNever(expected); 91 } 92 }); 93 assert.equal(host.screenClears.length, screenClears, "Expected number of screen clears"); 94 host.clearOutput(); 95 96 function isDiagnostic(diagnostic: Diagnostic | string): diagnostic is Diagnostic { 97 return !!(diagnostic as Diagnostic).messageText; 98 } 99 100 function assertDiagnostic({ diagnostic }: HostOutputDiagnostic) { 101 const expected = isDiagnostic(diagnostic) ? formatDiagnostic(diagnostic, host) : diagnostic; 102 assert.equal(outputs[index], expected, getOutputAtFailedMessage("Diagnostic", expected)); 103 index++; 104 } 105 106 function getCleanLogString(log: string) { 107 return log.replace(elapsedRegex, "").replace(buildVerboseLogRegEx, ""); 108 } 109 110 function assertLog({ caption, expected }: HostOutputLog) { 111 const actual = outputs[index]; 112 assert.equal(getCleanLogString(actual), getCleanLogString(expected), getOutputAtFailedMessage(caption || "Log", expected)); 113 index++; 114 } 115 116 function assertWatchDiagnostic({ diagnostic }: HostOutputWatchDiagnostic) { 117 if (isString(diagnostic)) { 118 assert.equal(outputs[index], diagnostic, getOutputAtFailedMessage("Diagnostic", diagnostic)); 119 } 120 else { 121 const expected = getWatchDiagnosticWithoutDate(diagnostic); 122 if (!disableConsoleClears && contains(screenStartingMessageCodes, diagnostic.code)) { 123 assert.equal(host.screenClears[screenClears], index, `Expected screen clear at this diagnostic: ${expected}`); 124 screenClears++; 125 } 126 assert.isTrue(endsWith(outputs[index], expected), getOutputAtFailedMessage("Watch diagnostic", expected)); 127 } 128 index++; 129 } 130 131 function getOutputAtFailedMessage(caption: string, expectedOutput: string) { 132 return `Expected ${caption}: ${JSON.stringify(expectedOutput)} at ${index} in ${JSON.stringify(outputs)}`; 133 } 134 135 function getWatchDiagnosticWithoutDate(diagnostic: Diagnostic) { 136 const newLines = contains(screenStartingMessageCodes, diagnostic.code) 137 ? `${host.newLine}${host.newLine}` 138 : host.newLine; 139 return ` - ${flattenDiagnosticMessageText(diagnostic.messageText, host.newLine)}${newLines}`; 140 } 141 } 142 143 export function hostOutputLog(expected: string, caption?: string): HostOutputLog { 144 return { kind: HostOutputKind.Log, expected, caption }; 145 } 146 export function hostOutputDiagnostic(diagnostic: Diagnostic | string): HostOutputDiagnostic { 147 return { kind: HostOutputKind.Diagnostic, diagnostic }; 148 } 149 export function hostOutputWatchDiagnostic(diagnostic: Diagnostic | string): HostOutputWatchDiagnostic { 150 return { kind: HostOutputKind.WatchDiagnostic, diagnostic }; 151 } 152 153 export function startingCompilationInWatchMode() { 154 return hostOutputWatchDiagnostic(createCompilerDiagnostic(Diagnostics.Starting_compilation_in_watch_mode)); 155 } 156 export function foundErrorsWatching(errors: readonly any[]) { 157 return hostOutputWatchDiagnostic(errors.length === 1 ? 158 createCompilerDiagnostic(Diagnostics.Found_1_error_Watching_for_file_changes) : 159 createCompilerDiagnostic(Diagnostics.Found_0_errors_Watching_for_file_changes, errors.length) 160 ); 161 } 162 export function fileChangeDetected() { 163 return hostOutputWatchDiagnostic(createCompilerDiagnostic(Diagnostics.File_change_detected_Starting_incremental_compilation)); 164 } 165 166 export function checkOutputErrorsInitial(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], disableConsoleClears?: boolean, logsBeforeErrors?: string[]) { 167 checkOutputErrors( 168 host, 169 [ 170 startingCompilationInWatchMode(), 171 ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")), 172 ...map(errors, hostOutputDiagnostic), 173 foundErrorsWatching(errors) 174 ], 175 disableConsoleClears 176 ); 177 } 178 179 export function checkOutputErrorsIncremental(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { 180 checkOutputErrors( 181 host, 182 [ 183 ...map(logsBeforeWatchDiagnostic || emptyArray, expected => hostOutputLog(expected, "logsBeforeWatchDiagnostic")), 184 fileChangeDetected(), 185 ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")), 186 ...map(errors, hostOutputDiagnostic), 187 foundErrorsWatching(errors) 188 ], 189 disableConsoleClears 190 ); 191 } 192 193 export function checkOutputErrorsIncrementalWithExit(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], expectedExitCode: ExitStatus, disableConsoleClears?: boolean, logsBeforeWatchDiagnostic?: string[], logsBeforeErrors?: string[]) { 194 checkOutputErrors( 195 host, 196 [ 197 ...map(logsBeforeWatchDiagnostic || emptyArray, expected => hostOutputLog(expected, "logsBeforeWatchDiagnostic")), 198 fileChangeDetected(), 199 ...map(logsBeforeErrors || emptyArray, expected => hostOutputLog(expected, "logBeforeError")), 200 ...map(errors, hostOutputDiagnostic), 201 ], 202 disableConsoleClears 203 ); 204 assert.equal(host.exitCode, expectedExitCode); 205 } 206 207 export function checkNormalBuildErrors(host: WatchedSystem, errors: readonly Diagnostic[] | readonly string[], reportErrorSummary?: boolean) { 208 checkOutputErrors( 209 host, 210 [ 211 ...map(errors, hostOutputDiagnostic), 212 ...reportErrorSummary ? 213 [hostOutputWatchDiagnostic(getErrorSummaryText(errors.length, host.newLine))] : 214 emptyArray 215 ] 216 ); 217 } 218 219 export function getDiagnosticMessageChain(message: DiagnosticMessage, args?: (string | number)[], next?: DiagnosticMessageChain[]): DiagnosticMessageChain { 220 let text = getLocaleSpecificMessage(message); 221 if (args?.length) { 222 text = formatStringFromArgs(text, args); 223 } 224 return { 225 messageText: text, 226 category: message.category, 227 code: message.code, 228 next 229 }; 230 } 231 232 function isDiagnosticMessageChain(message: DiagnosticMessage | DiagnosticMessageChain): message is DiagnosticMessageChain { 233 return !!(message as DiagnosticMessageChain).messageText; 234 } 235 236 export function getDiagnosticOfFileFrom(file: SourceFile | undefined, start: number | undefined, length: number | undefined, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { 237 return { 238 file, 239 start, 240 length, 241 242 messageText: isDiagnosticMessageChain(message) ? 243 message : 244 getDiagnosticMessageChain(message, args).messageText, 245 category: message.category, 246 code: message.code, 247 }; 248 } 249 250 export function getDiagnosticWithoutFile(message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { 251 return getDiagnosticOfFileFrom(/*file*/ undefined, /*start*/ undefined, /*length*/ undefined, message, ...args); 252 } 253 254 export function getDiagnosticOfFile(file: SourceFile, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { 255 return getDiagnosticOfFileFrom(file, start, length, message, ...args); 256 } 257 258 export function getDiagnosticOfFileFromProgram(program: Program, filePath: string, start: number, length: number, message: DiagnosticMessage | DiagnosticMessageChain, ...args: (string | number)[]): Diagnostic { 259 return getDiagnosticOfFileFrom(program.getSourceFileByPath(toPath(filePath, program.getCurrentDirectory(), s => s.toLowerCase())), 260 start, length, message, ...args); 261 } 262 263 export function getUnknownCompilerOption(program: Program, configFile: File, option: string) { 264 const quotedOption = `"${option}"`; 265 return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0, option); 266 } 267 268 export function getUnknownDidYouMeanCompilerOption(program: Program, configFile: File, option: string, didYouMean: string) { 269 const quotedOption = `"${option}"`; 270 return getDiagnosticOfFile(program.getCompilerOptions().configFile!, configFile.content.indexOf(quotedOption), quotedOption.length, Diagnostics.Unknown_compiler_option_0_Did_you_mean_1, option, didYouMean); 271 } 272 273 export function getDiagnosticModuleNotFoundOfFile(program: Program, file: File, moduleName: string) { 274 const quotedModuleName = `"${moduleName}"`; 275 return getDiagnosticOfFileFromProgram(program, file.path, file.content.indexOf(quotedModuleName), quotedModuleName.length, Diagnostics.Cannot_find_module_0_Did_you_mean_to_set_the_moduleResolution_option_to_node_or_to_add_aliases_to_the_paths_option, moduleName); 276 } 277 278 export function runQueuedTimeoutCallbacks(sys: WatchedSystem) { 279 sys.runQueuedTimeoutCallbacks(); 280 } 281 282 export function checkSingleTimeoutQueueLengthAndRun(sys: WatchedSystem) { 283 sys.checkTimeoutQueueLengthAndRun(1); 284 } 285 286 export function checkSingleTimeoutQueueLengthAndRunAndVerifyNoTimeout(sys: WatchedSystem) { 287 sys.checkTimeoutQueueLengthAndRun(1); 288 sys.checkTimeoutQueueLength(0); 289 } 290 291 export interface TscWatchCompileChange { 292 caption: string; 293 change: (sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles) => void; 294 timeouts: ( 295 sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles, 296 programs: readonly CommandLineProgram[], 297 watchOrSolution: ReturnType<typeof executeCommandLine> 298 ) => void; 299 } 300 export interface TscWatchCheckOptions { 301 baselineSourceMap?: boolean; 302 } 303 export interface TscWatchCompileBase extends TscWatchCheckOptions { 304 scenario: string; 305 subScenario: string; 306 commandLineArgs: readonly string[]; 307 changes: readonly TscWatchCompileChange[]; 308 } 309 export interface TscWatchCompile extends TscWatchCompileBase { 310 sys: () => WatchedSystem; 311 } 312 313 export const noopChange: TscWatchCompileChange = { 314 caption: "No change", 315 change: noop, 316 timeouts: sys => sys.checkTimeoutQueueLength(0), 317 }; 318 319 export type SystemSnap = ReturnType<WatchedSystem["snap"]>; 320 function tscWatchCompile(input: TscWatchCompile) { 321 it("tsc-watch:: Generates files matching the baseline", () => { 322 const { sys, baseline, oldSnap } = createBaseline(input.sys()); 323 const { 324 scenario, subScenario, 325 commandLineArgs, changes, 326 baselineSourceMap 327 } = input; 328 329 if (!isWatch(commandLineArgs)) sys.exit = exitCode => sys.exitCode = exitCode; 330 const { cb, getPrograms } = commandLineCallbacks(sys); 331 const watchOrSolution = executeCommandLine( 332 sys, 333 cb, 334 commandLineArgs, 335 ); 336 runWatchBaseline({ 337 scenario, 338 subScenario, 339 commandLineArgs, 340 sys, 341 baseline, 342 oldSnap, 343 getPrograms, 344 baselineSourceMap, 345 changes, 346 watchOrSolution 347 }); 348 }); 349 } 350 351 export interface Baseline { 352 baseline: string[]; 353 sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles; 354 oldSnap: SystemSnap; 355 } 356 357 export function createBaseline(system: WatchedSystem): Baseline { 358 const sys = TestFSWithWatch.changeToHostTrackingWrittenFiles( 359 fakes.patchHostForBuildInfoReadWrite(system) 360 ); 361 const baseline: string[] = []; 362 baseline.push("Input::"); 363 sys.diff(baseline); 364 return { sys, baseline, oldSnap: sys.snap() }; 365 } 366 367 export function applyChange(sys: Baseline["sys"], baseline: Baseline["baseline"], change: TscWatchCompileChange["change"], caption?: TscWatchCompileChange["caption"]) { 368 const oldSnap = sys.snap(); 369 baseline.push(`Change::${caption ? " " + caption : ""}`, ""); 370 change(sys); 371 baseline.push("Input::"); 372 sys.diff(baseline, oldSnap); 373 return sys.snap(); 374 } 375 376 export interface RunWatchBaseline extends Baseline, TscWatchCompileBase { 377 sys: TestFSWithWatch.TestServerHostTrackingWrittenFiles; 378 getPrograms: () => readonly CommandLineProgram[]; 379 watchOrSolution: ReturnType<typeof executeCommandLine>; 380 } 381 export function runWatchBaseline({ 382 scenario, subScenario, commandLineArgs, 383 getPrograms, sys, baseline, oldSnap, 384 baselineSourceMap, 385 changes, watchOrSolution 386 }: RunWatchBaseline) { 387 baseline.push(`${sys.getExecutingFilePath()} ${commandLineArgs.join(" ")}`); 388 let programs = watchBaseline({ 389 baseline, 390 getPrograms, 391 sys, 392 oldSnap, 393 baselineSourceMap 394 }); 395 396 for (const { caption, change, timeouts } of changes) { 397 oldSnap = applyChange(sys, baseline, change, caption); 398 timeouts(sys, programs, watchOrSolution); 399 programs = watchBaseline({ 400 baseline, 401 getPrograms, 402 sys, 403 oldSnap, 404 baselineSourceMap 405 }); 406 } 407 Harness.Baseline.runBaseline(`${isBuild(commandLineArgs) ? 408 isWatch(commandLineArgs) ? "tsbuild/watchMode" : "tsbuild" : 409 isWatch(commandLineArgs) ? "tscWatch" : "tsc"}/${scenario}/${subScenario.split(" ").join("-")}.js`, baseline.join("\r\n")); 410 } 411 412 function isWatch(commandLineArgs: readonly string[]) { 413 return forEach(commandLineArgs, arg => { 414 if (arg.charCodeAt(0) !== CharacterCodes.minus) return false; 415 const option = arg.slice(arg.charCodeAt(1) === CharacterCodes.minus ? 2 : 1).toLowerCase(); 416 return option === "watch" || option === "w"; 417 }); 418 } 419 420 export interface WatchBaseline extends Baseline, TscWatchCheckOptions { 421 getPrograms: () => readonly CommandLineProgram[]; 422 } 423 export function watchBaseline({ baseline, getPrograms, sys, oldSnap, baselineSourceMap }: WatchBaseline) { 424 if (baselineSourceMap) generateSourceMapBaselineFiles(sys); 425 sys.serializeOutput(baseline); 426 const programs = baselinePrograms(baseline, getPrograms); 427 sys.serializeWatches(baseline); 428 baseline.push(`exitCode:: ExitStatus.${ExitStatus[sys.exitCode as ExitStatus]}`, ""); 429 sys.diff(baseline, oldSnap); 430 sys.writtenFiles.forEach((value, key) => { 431 assert.equal(value, 1, `Expected to write file ${key} only once`); 432 }); 433 sys.writtenFiles.clear(); 434 return programs; 435 } 436 437 export function baselinePrograms(baseline: string[], getPrograms: () => readonly CommandLineProgram[]) { 438 const programs = getPrograms(); 439 for (const program of programs) { 440 baselineProgram(baseline, program); 441 } 442 return programs; 443 } 444 445 function baselineProgram(baseline: string[], [program, builderProgram]: CommandLineProgram) { 446 const options = program.getCompilerOptions(); 447 baseline.push(`Program root files: ${JSON.stringify(program.getRootFileNames())}`); 448 baseline.push(`Program options: ${JSON.stringify(options)}`); 449 baseline.push(`Program structureReused: ${(<any>ts).StructureIsReused[program.structureIsReused]}`); 450 baseline.push("Program files::"); 451 for (const file of program.getSourceFiles()) { 452 baseline.push(file.fileName); 453 } 454 baseline.push(""); 455 if (!builderProgram) return; 456 const state = builderProgram.getState(); 457 if (state.semanticDiagnosticsPerFile?.size) { 458 baseline.push("Semantic diagnostics in builder refreshed for::"); 459 for (const file of program.getSourceFiles()) { 460 if (!state.semanticDiagnosticsFromOldState || !state.semanticDiagnosticsFromOldState.has(file.resolvedPath)) { 461 baseline.push(file.fileName); 462 } 463 } 464 } 465 else { 466 baseline.push("No cached semantic diagnostics in the builder::"); 467 } 468 baseline.push(""); 469 } 470 471 export interface VerifyTscWatch extends TscWatchCompile { 472 baselineIncremental?: boolean; 473 } 474 export function verifyTscWatch(input: VerifyTscWatch) { 475 describe(input.scenario, () => { 476 describe(input.subScenario, () => { 477 tscWatchCompile(input); 478 }); 479 if (input.baselineIncremental) { 480 describe(`${input.subScenario} with incremental`, () => { 481 tscWatchCompile({ 482 ...input, 483 subScenario: `${input.subScenario} with incremental`, 484 commandLineArgs: [...input.commandLineArgs, "--incremental"], 485 }); 486 }); 487 } 488 }); 489 } 490 491 export function replaceFileText(sys: WatchedSystem, file: string, searchValue: string | RegExp, replaceValue: string) { 492 const content = Debug.checkDefined(sys.readFile(file)); 493 sys.writeFile(file, content.replace(searchValue, replaceValue)); 494 } 495} 496