1namespace ts { 2 export function errorDiagnostic(message: fakes.ExpectedDiagnosticMessage): fakes.ExpectedErrorDiagnostic { 3 return { message }; 4 } 5 6 export function getExpectedDiagnosticForProjectsInBuild(...projects: string[]): fakes.ExpectedDiagnostic { 7 return [Diagnostics.Projects_in_this_build_Colon_0, projects.map(p => "\r\n * " + p).join("")]; 8 } 9 10 export function changeCompilerVersion(host: fakes.SolutionBuilderHost) { 11 const originalReadFile = host.readFile; 12 host.readFile = path => { 13 const value = originalReadFile.call(host, path); 14 if (!value || !isBuildInfoFile(path)) return value; 15 const buildInfo = getBuildInfo(path, value); 16 if (!buildInfo) return value; 17 buildInfo.version = fakes.version; 18 return getBuildInfoText(buildInfo); 19 }; 20 } 21 22 export function replaceText(fs: vfs.FileSystem, path: string, oldText: string, newText: string) { 23 if (!fs.statSync(path).isFile()) { 24 throw new Error(`File ${path} does not exist`); 25 } 26 const old = fs.readFileSync(path, "utf-8"); 27 if (old.indexOf(oldText) < 0) { 28 throw new Error(`Text "${oldText}" does not exist in file ${path}`); 29 } 30 const newContent = old.replace(oldText, newText); 31 fs.writeFileSync(path, newContent, "utf-8"); 32 } 33 34 export function prependText(fs: vfs.FileSystem, path: string, additionalContent: string) { 35 if (!fs.statSync(path).isFile()) { 36 throw new Error(`File ${path} does not exist`); 37 } 38 const old = fs.readFileSync(path, "utf-8"); 39 fs.writeFileSync(path, `${additionalContent}${old}`, "utf-8"); 40 } 41 42 export function appendText(fs: vfs.FileSystem, path: string, additionalContent: string) { 43 if (!fs.statSync(path).isFile()) { 44 throw new Error(`File ${path} does not exist`); 45 } 46 const old = fs.readFileSync(path, "utf-8"); 47 fs.writeFileSync(path, `${old}${additionalContent}`); 48 } 49 50 export function indexOf(fs: vfs.FileSystem, path: string, searchStr: string) { 51 if (!fs.statSync(path).isFile()) { 52 throw new Error(`File ${path} does not exist`); 53 } 54 const content = fs.readFileSync(path, "utf-8"); 55 return content.indexOf(searchStr); 56 } 57 58 export function lastIndexOf(fs: vfs.FileSystem, path: string, searchStr: string) { 59 if (!fs.statSync(path).isFile()) { 60 throw new Error(`File ${path} does not exist`); 61 } 62 const content = fs.readFileSync(path, "utf-8"); 63 return content.lastIndexOf(searchStr); 64 } 65 66 export function expectedLocationIndexOf(fs: vfs.FileSystem, file: string, searchStr: string): fakes.ExpectedDiagnosticLocation { 67 return { 68 file, 69 start: indexOf(fs, file, searchStr), 70 length: searchStr.length 71 }; 72 } 73 74 export function expectedLocationLastIndexOf(fs: vfs.FileSystem, file: string, searchStr: string): fakes.ExpectedDiagnosticLocation { 75 return { 76 file, 77 start: lastIndexOf(fs, file, searchStr), 78 length: searchStr.length 79 }; 80 } 81 82 export const libContent = `${TestFSWithWatch.libFile.content} 83interface ReadonlyArray<T> {} 84declare const console: { log(msg: any): void; };`; 85 86 export const symbolLibContent = ` 87interface SymbolConstructor { 88 readonly species: symbol; 89 readonly toStringTag: symbol; 90} 91declare var Symbol: SymbolConstructor; 92interface Symbol { 93 readonly [Symbol.toStringTag]: string; 94} 95`; 96 97 /** 98 * Load project from disk into /src folder 99 */ 100 export function loadProjectFromDisk( 101 root: string, 102 libContentToAppend?: string 103 ): vfs.FileSystem { 104 const resolver = vfs.createResolver(Harness.IO); 105 const fs = new vfs.FileSystem(/*ignoreCase*/ true, { 106 files: { 107 ["/src"]: new vfs.Mount(vpath.resolve(Harness.IO.getWorkspaceRoot(), root), resolver) 108 }, 109 cwd: "/", 110 meta: { defaultLibLocation: "/lib" }, 111 }); 112 addLibAndMakeReadonly(fs, libContentToAppend); 113 return fs; 114 } 115 116 /** 117 * All the files must be in /src 118 */ 119 export function loadProjectFromFiles( 120 files: vfs.FileSet, 121 libContentToAppend?: string 122 ): vfs.FileSystem { 123 const fs = new vfs.FileSystem(/*ignoreCase*/ true, { 124 files, 125 cwd: "/", 126 meta: { defaultLibLocation: "/lib" }, 127 }); 128 addLibAndMakeReadonly(fs, libContentToAppend); 129 return fs; 130 } 131 132 function addLibAndMakeReadonly(fs: vfs.FileSystem, libContentToAppend?: string) { 133 fs.mkdirSync("/lib"); 134 fs.writeFileSync("/lib/lib.d.ts", libContentToAppend ? `${libContent}${libContentToAppend}` : libContent); 135 fs.makeReadonly(); 136 } 137 138 export function verifyOutputsPresent(fs: vfs.FileSystem, outputs: readonly string[]) { 139 for (const output of outputs) { 140 assert(fs.existsSync(output), `Expect file ${output} to exist`); 141 } 142 } 143 144 export function verifyOutputsAbsent(fs: vfs.FileSystem, outputs: readonly string[]) { 145 for (const output of outputs) { 146 assert.isFalse(fs.existsSync(output), `Expect file ${output} to not exist`); 147 } 148 } 149 150 export function generateSourceMapBaselineFiles(sys: System & { writtenFiles: ReadonlyCollection<Path>; }) { 151 const mapFileNames = mapDefinedIterator(sys.writtenFiles.keys(), f => f.endsWith(".map") ? f : undefined); 152 while (true) { 153 const result = mapFileNames.next(); 154 if (result.done) break; 155 const mapFile = result.value; 156 const text = Harness.SourceMapRecorder.getSourceMapRecordWithSystem(sys, mapFile); 157 sys.writeFile(`${mapFile}.baseline.txt`, text); 158 } 159 } 160 161 function generateBundleFileSectionInfo(sys: System, originalReadCall: System["readFile"], baselineRecorder: Harness.Compiler.WriterAggregator, bundleFileInfo: BundleFileInfo | undefined, outFile: string | undefined) { 162 if (!length(bundleFileInfo && bundleFileInfo.sections) && !outFile) return; // Nothing to baseline 163 164 const content = outFile && sys.fileExists(outFile) ? originalReadCall.call(sys, outFile, "utf8")! : ""; 165 baselineRecorder.WriteLine("======================================================================"); 166 baselineRecorder.WriteLine(`File:: ${outFile}`); 167 for (const section of bundleFileInfo ? bundleFileInfo.sections : emptyArray) { 168 baselineRecorder.WriteLine("----------------------------------------------------------------------"); 169 writeSectionHeader(section); 170 if (section.kind !== BundleFileSectionKind.Prepend) { 171 writeTextOfSection(section.pos, section.end); 172 } 173 else if (section.texts.length > 0) { 174 Debug.assert(section.pos === first(section.texts).pos); 175 Debug.assert(section.end === last(section.texts).end); 176 for (const text of section.texts) { 177 baselineRecorder.WriteLine(">>--------------------------------------------------------------------"); 178 writeSectionHeader(text); 179 writeTextOfSection(text.pos, text.end); 180 } 181 } 182 else { 183 Debug.assert(section.pos === section.end); 184 } 185 } 186 baselineRecorder.WriteLine("======================================================================"); 187 188 function writeTextOfSection(pos: number, end: number) { 189 const textLines = content.substring(pos, end).split(/\r?\n/); 190 for (const line of textLines) { 191 baselineRecorder.WriteLine(line); 192 } 193 } 194 195 function writeSectionHeader(section: BundleFileSection) { 196 baselineRecorder.WriteLine(`${section.kind}: (${section.pos}-${section.end})${section.data ? ":: " + section.data : ""}${section.kind === BundleFileSectionKind.Prepend ? " texts:: " + section.texts.length : ""}`); 197 } 198 } 199 200 type ReadableProgramBuildInfoDiagnostic = string | [string, readonly ReusableDiagnostic[]]; 201 type ReadableProgramBuilderInfoFilePendingEmit = [string, "DtsOnly" | "Full"]; 202 type ReadableProgramBuildInfoEmitSignature = string | [string, string]; 203 type ReadableProgramBuildInfoFileInfo = Omit<BuilderState.FileInfo, "impliedFormat"> & { impliedFormat: string | undefined; }; 204 type ReadableProgramMultiFileEmitBuildInfo = Omit<ProgramMultiFileEmitBuildInfo, 205 "fileIdsList" | "fileInfos" | 206 "referencedMap" | "exportedModulesMap" | "semanticDiagnosticsPerFile" | 207 "affectedFilesPendingEmit" | "changeFileSet" | "emitSignatures" 208 > & { 209 fileNamesList: readonly (readonly string[])[] | undefined; 210 fileInfos: MapLike<ReadableProgramBuildInfoFileInfo>; 211 referencedMap?: MapLike<string[]>; 212 exportedModulesMap?: MapLike<string[]>; 213 semanticDiagnosticsPerFile?: readonly ReadableProgramBuildInfoDiagnostic[]; 214 affectedFilesPendingEmit?: readonly ReadableProgramBuilderInfoFilePendingEmit[]; 215 changeFileSet?: readonly string[]; 216 emitSignatures?: readonly ReadableProgramBuildInfoEmitSignature[]; 217 }; 218 type ReadableProgramBundleEmitBuildInfo = Omit<ProgramBundleEmitBuildInfo, "fileInfos"> & { 219 fileInfos: MapLike<string>; 220 }; 221 222 type ReadableProgramBuildInfo = ReadableProgramMultiFileEmitBuildInfo | ReadableProgramBundleEmitBuildInfo; 223 224 function isReadableProgramBundleEmitBuildInfo(info: ReadableProgramBuildInfo | undefined): info is ReadableProgramBundleEmitBuildInfo { 225 return !!info && !!outFile(info.options || {}); 226 } 227 type ReadableBuildInfo = Omit<BuildInfo, "program"> & { program: ReadableProgramBuildInfo | undefined; size: number; }; 228 function generateBuildInfoProgramBaseline(sys: System, buildInfoPath: string, buildInfo: BuildInfo) { 229 let program: ReadableProgramBuildInfo | undefined; 230 let fileNamesList: string[][] | undefined; 231 if (buildInfo.program && isProgramBundleEmitBuildInfo(buildInfo.program)) { 232 const fileInfos: ReadableProgramBundleEmitBuildInfo["fileInfos"] = {}; 233 buildInfo.program?.fileInfos?.forEach((fileInfo, index) => fileInfos[toFileName(index + 1 as ProgramBuildInfoFileId)] = fileInfo); 234 program = { 235 ...buildInfo.program, 236 fileInfos 237 }; 238 } 239 else if (buildInfo.program) { 240 const fileInfos: ReadableProgramMultiFileEmitBuildInfo["fileInfos"] = {}; 241 buildInfo.program?.fileInfos?.forEach((fileInfo, index) => fileInfos[toFileName(index + 1 as ProgramBuildInfoFileId)] = toReadableFileInfo(fileInfo)); 242 fileNamesList = buildInfo.program.fileIdsList?.map(fileIdsListId => fileIdsListId.map(toFileName)); 243 program = buildInfo.program && { 244 fileNames: buildInfo.program.fileNames, 245 fileNamesList, 246 fileInfos: buildInfo.program.fileInfos ? fileInfos : undefined!, 247 options: buildInfo.program.options, 248 referencedMap: toMapOfReferencedSet(buildInfo.program.referencedMap), 249 exportedModulesMap: toMapOfReferencedSet(buildInfo.program.exportedModulesMap), 250 semanticDiagnosticsPerFile: buildInfo.program.semanticDiagnosticsPerFile?.map(d => 251 isNumber(d) ? 252 toFileName(d) : 253 [toFileName(d[0]), d[1]] 254 ), 255 affectedFilesPendingEmit: buildInfo.program.affectedFilesPendingEmit?.map(([fileId, emitKind]) => [ 256 toFileName(fileId), 257 emitKind === BuilderFileEmit.DtsOnly ? "DtsOnly" : 258 emitKind === BuilderFileEmit.Full ? "Full" : 259 Debug.assertNever(emitKind) 260 ]), 261 changeFileSet: buildInfo.program.changeFileSet?.map(toFileName), 262 emitSignatures: buildInfo.program.emitSignatures?.map(s => 263 isNumber(s) ? 264 toFileName(s) : 265 [toFileName(s[0]), s[1]] 266 ), 267 latestChangedDtsFile: buildInfo.program.latestChangedDtsFile, 268 }; 269 } 270 const version = buildInfo.version === ts.version ? fakes.version : buildInfo.version; 271 const result: ReadableBuildInfo = { 272 // Baseline fixed order for bundle 273 bundle: buildInfo.bundle && { 274 ...buildInfo.bundle, 275 js: buildInfo.bundle.js && { 276 sections: buildInfo.bundle.js.sections, 277 hash: buildInfo.bundle.js.hash, 278 mapHash: buildInfo.bundle.js.mapHash, 279 sources: buildInfo.bundle.js.sources, 280 }, 281 dts: buildInfo.bundle.dts && { 282 sections: buildInfo.bundle.dts.sections, 283 hash: buildInfo.bundle.dts.hash, 284 mapHash: buildInfo.bundle.dts.mapHash, 285 sources: buildInfo.bundle.dts.sources, 286 }, 287 }, 288 program, 289 version, 290 size: getBuildInfoText({ ...buildInfo, version }).length, 291 }; 292 // For now its just JSON.stringify 293 sys.writeFile(`${buildInfoPath}.readable.baseline.txt`, JSON.stringify(result, /*replacer*/ undefined, 2)); 294 295 function toFileName(fileId: ProgramBuildInfoFileId) { 296 return buildInfo.program!.fileNames[fileId - 1]; 297 } 298 299 function toFileNames(fileIdsListId: ProgramBuildInfoFileIdListId) { 300 return fileNamesList![fileIdsListId - 1]; 301 } 302 303 function toReadableFileInfo(fileInfo: ProgramBuildInfoFileInfo): ReadableProgramBuildInfoFileInfo { 304 const info = toBuilderStateFileInfo(fileInfo); 305 return { 306 ...info, 307 impliedFormat: info.impliedFormat && getNameOfCompilerOptionValue(info.impliedFormat, moduleOptionDeclaration.type), 308 }; 309 } 310 311 function toMapOfReferencedSet(referenceMap: ProgramBuildInfoReferencedMap | undefined): MapLike<string[]> | undefined { 312 if (!referenceMap) return undefined; 313 const result: MapLike<string[]> = {}; 314 for (const [fileNamesKey, fileNamesListKey] of referenceMap) { 315 result[toFileName(fileNamesKey)] = toFileNames(fileNamesListKey); 316 } 317 return result; 318 } 319 } 320 321 export function toPathWithSystem(sys: System, fileName: string): Path { 322 return toPath(fileName, sys.getCurrentDirectory(), createGetCanonicalFileName(sys.useCaseSensitiveFileNames)); 323 } 324 325 export function baselineBuildInfo( 326 options: CompilerOptions, 327 sys: TscCompileSystem | tscWatch.WatchedSystem, 328 originalReadCall?: System["readFile"], 329 ) { 330 const buildInfoPath = getTsBuildInfoEmitOutputFilePath(options); 331 if (!buildInfoPath || !sys.writtenFiles!.has(toPathWithSystem(sys, buildInfoPath))) return; 332 if (!sys.fileExists(buildInfoPath)) return; 333 334 const buildInfo = getBuildInfo(buildInfoPath, (originalReadCall || sys.readFile).call(sys, buildInfoPath, "utf8")!); 335 if (!buildInfo) return sys.writeFile(`${buildInfoPath}.baseline.txt`, "Error reading valid buildinfo file"); 336 generateBuildInfoProgramBaseline(sys, buildInfoPath, buildInfo); 337 338 if (!outFile(options)) return; 339 const { jsFilePath, declarationFilePath } = getOutputPathsForBundle(options, /*forceDts*/ false); 340 const bundle = buildInfo.bundle; 341 if (!bundle || (!length(bundle.js && bundle.js.sections) && !length(bundle.dts && bundle.dts.sections))) return; 342 343 // Write the baselines: 344 const baselineRecorder = new Harness.Compiler.WriterAggregator(); 345 generateBundleFileSectionInfo(sys, originalReadCall || sys.readFile, baselineRecorder, bundle.js, jsFilePath); 346 generateBundleFileSectionInfo(sys, originalReadCall || sys.readFile, baselineRecorder, bundle.dts, declarationFilePath); 347 baselineRecorder.Close(); 348 const text = baselineRecorder.lines.join("\r\n"); 349 sys.writeFile(`${buildInfoPath}.baseline.txt`, text); 350 } 351 interface VerifyTscEditDiscrepanciesInput { 352 index: number; 353 scenario: TestTscCompile["scenario"]; 354 subScenario: TestTscCompile["subScenario"]; 355 baselines: string[] | undefined; 356 commandLineArgs: TestTscCompile["commandLineArgs"]; 357 modifyFs: TestTscCompile["modifyFs"]; 358 editFs: TestTscEdit["modifyFs"]; 359 baseFs: vfs.FileSystem; 360 newSys: TscCompileSystem; 361 discrepancyExplanation: TestTscEdit["discrepancyExplanation"]; 362 } 363 function verifyTscEditDiscrepancies({ 364 index, scenario, subScenario, commandLineArgs, 365 discrepancyExplanation, baselines, 366 modifyFs, editFs, baseFs, newSys 367 }: VerifyTscEditDiscrepanciesInput): string[] | undefined { 368 const sys = testTscCompile({ 369 scenario, 370 subScenario, 371 fs: () => baseFs.makeReadonly(), 372 commandLineArgs, 373 modifyFs: fs => { 374 if (modifyFs) modifyFs(fs); 375 editFs(fs); 376 }, 377 disableUseFileVersionAsSignature: true, 378 }); 379 let headerAdded = false; 380 for (const outputFile of arrayFrom(sys.writtenFiles.keys())) { 381 const cleanBuildText = sys.readFile(outputFile); 382 const incrementalBuildText = newSys.readFile(outputFile); 383 if (isBuildInfoFile(outputFile)) { 384 // Check only presence and absence and not text as we will do that for readable baseline 385 if (!sys.fileExists(`${outputFile}.readable.baseline.txt`)) addBaseline(`Readable baseline not present in clean build:: File:: ${outputFile}`); 386 if (!newSys.fileExists(`${outputFile}.readable.baseline.txt`)) addBaseline(`Readable baseline not present in incremental build:: File:: ${outputFile}`); 387 verifyPresenceAbsence(incrementalBuildText, cleanBuildText, `Incremental and clean tsbuildinfo file presence differs:: File:: ${outputFile}`); 388 } 389 else if (!fileExtensionIs(outputFile, ".tsbuildinfo.readable.baseline.txt")) { 390 verifyTextEqual(incrementalBuildText, cleanBuildText, `File: ${outputFile}`); 391 } 392 else if (incrementalBuildText !== cleanBuildText) { 393 // Verify build info without affectedFilesPendingEmit 394 const { buildInfo: incrementalBuildInfo, readableBuildInfo: incrementalReadableBuildInfo } = getBuildInfoForIncrementalCorrectnessCheck(incrementalBuildText); 395 const { buildInfo: cleanBuildInfo, readableBuildInfo: cleanReadableBuildInfo } = getBuildInfoForIncrementalCorrectnessCheck(cleanBuildText); 396 verifyTextEqual(incrementalBuildInfo, cleanBuildInfo, `TsBuild info text without affectedFilesPendingEmit:: ${outputFile}::`); 397 // Verify file info sigantures 398 verifyMapLike( 399 incrementalReadableBuildInfo?.program?.fileInfos as ReadableProgramMultiFileEmitBuildInfo["fileInfos"], 400 cleanReadableBuildInfo?.program?.fileInfos as ReadableProgramMultiFileEmitBuildInfo["fileInfos"], 401 (key, incrementalFileInfo, cleanFileInfo) => { 402 if (incrementalFileInfo.signature !== cleanFileInfo.signature && incrementalFileInfo.signature !== incrementalFileInfo.version) { 403 return [ 404 `Incremental signature is neither dts signature nor file version for File:: ${key}`, 405 `Incremental:: ${JSON.stringify(incrementalFileInfo, /*replacer*/ undefined, 2)}`, 406 `Clean:: ${JSON.stringify(cleanFileInfo, /*replacer*/ undefined, 2)}` 407 ]; 408 } 409 }, 410 `FileInfos:: File:: ${outputFile}` 411 ); 412 if (!isReadableProgramBundleEmitBuildInfo(incrementalReadableBuildInfo?.program)) { 413 Debug.assert(!isReadableProgramBundleEmitBuildInfo(cleanReadableBuildInfo?.program)); 414 // Verify exportedModulesMap 415 verifyMapLike( 416 incrementalReadableBuildInfo?.program?.exportedModulesMap, 417 cleanReadableBuildInfo?.program?.exportedModulesMap, 418 (key, incrementalReferenceSet, cleanReferenceSet) => { 419 if (!arrayIsEqualTo(incrementalReferenceSet, cleanReferenceSet) && !arrayIsEqualTo(incrementalReferenceSet, (incrementalReadableBuildInfo!.program! as ReadableProgramMultiFileEmitBuildInfo).referencedMap![key])) { 420 return [ 421 `Incremental Reference set is neither from dts nor files reference map for File:: ${key}::`, 422 `Incremental:: ${JSON.stringify(incrementalReferenceSet, /*replacer*/ undefined, 2)}`, 423 `Clean:: ${JSON.stringify(cleanReferenceSet, /*replacer*/ undefined, 2)}`, 424 `IncrementalReferenceMap:: ${JSON.stringify((incrementalReadableBuildInfo!.program! as ReadableProgramMultiFileEmitBuildInfo).referencedMap![key], /*replacer*/ undefined, 2)}`, 425 `CleanReferenceMap:: ${JSON.stringify((cleanReadableBuildInfo!.program! as ReadableProgramMultiFileEmitBuildInfo).referencedMap![key], /*replacer*/ undefined, 2)}`, 426 ]; 427 } 428 }, 429 `exportedModulesMap:: File:: ${outputFile}` 430 ); 431 // Verify that incrementally pending affected file emit are in clean build since clean build can contain more files compared to incremental depending of noEmitOnError option 432 if (incrementalReadableBuildInfo?.program?.affectedFilesPendingEmit) { 433 if (cleanReadableBuildInfo?.program?.affectedFilesPendingEmit === undefined) { 434 addBaseline( 435 `Incremental build contains affectedFilesPendingEmit, clean build does not have it: ${outputFile}::`, 436 `Incremental buildInfoText:: ${incrementalBuildText}`, 437 `Clean buildInfoText:: ${cleanBuildText}` 438 ); 439 } 440 let expectedIndex = 0; 441 incrementalReadableBuildInfo.program.affectedFilesPendingEmit.forEach(([actualFile]) => { 442 expectedIndex = findIndex((cleanReadableBuildInfo!.program! as ReadableProgramMultiFileEmitBuildInfo).affectedFilesPendingEmit, ([expectedFile]) => actualFile === expectedFile, expectedIndex); 443 if (expectedIndex === -1) { 444 addBaseline( 445 `Incremental build contains ${actualFile} file as pending emit, clean build does not have it: ${outputFile}::`, 446 `Incremental buildInfoText:: ${incrementalBuildText}`, 447 `Clean buildInfoText:: ${cleanBuildText}` 448 ); 449 } 450 expectedIndex++; 451 }); 452 } 453 } 454 } 455 } 456 if (!headerAdded && discrepancyExplanation) addBaseline("*** Supplied discrepancy explanation but didnt file any difference"); 457 return baselines; 458 459 function verifyTextEqual(incrementalText: string | undefined, cleanText: string | undefined, message: string) { 460 if (incrementalText !== cleanText) writeNotEqual(incrementalText, cleanText, message); 461 } 462 463 function verifyMapLike<T>(incremental: MapLike<T> | undefined, clean: MapLike<T> | undefined, verifyValue: (key: string, incrementalValue: T, cleanValue: T) => string[] | undefined, message: string) { 464 verifyPresenceAbsence(incremental, clean, `Incremental and clean do not match:: ${message}`); 465 if (!incremental || !clean) return; 466 const incrementalMap = new Map(getEntries(incremental)); 467 const cleanMap = new Map(getEntries(clean)); 468 if (incrementalMap.size !== cleanMap.size) { 469 addBaseline( 470 `Incremental and clean size of maps do not match:: ${message}`, 471 `Incremental: ${JSON.stringify(incremental, /*replacer*/ undefined, 2)}`, 472 `Clean: ${JSON.stringify(clean, /*replacer*/ undefined, 2)}`, 473 ); 474 return; 475 } 476 cleanMap.forEach((cleanValue, key) => { 477 const incrementalValue = incrementalMap.get(key); 478 if (!incrementalValue) { 479 addBaseline( 480 `Incremental does not contain ${key} which is present in clean:: ${message}`, 481 `Incremental: ${JSON.stringify(incremental, /*replacer*/ undefined, 2)}`, 482 `Clean: ${JSON.stringify(clean, /*replacer*/ undefined, 2)}`, 483 ); 484 } 485 else { 486 const result = verifyValue(key, incrementalMap.get(key)!, cleanValue); 487 if (result) addBaseline(...result); 488 } 489 }); 490 } 491 492 function verifyPresenceAbsence<T>(actual: T | undefined, expected: T | undefined, message: string) { 493 if (expected === undefined) { 494 if (actual === undefined) return; 495 } 496 else { 497 if (actual !== undefined) return; 498 } 499 writeNotEqual(actual, expected, message); 500 } 501 502 function writeNotEqual<T>(actual: T | undefined, expected: T | undefined, message: string) { 503 addBaseline( 504 message, 505 "CleanBuild:", 506 isString(expected) ? expected : JSON.stringify(expected), 507 "IncrementalBuild:", 508 isString(actual) ? actual : JSON.stringify(actual), 509 ); 510 } 511 512 function addBaseline(...text: string[]) { 513 if (!baselines || !headerAdded) { 514 (baselines ||= []).push(`${index}:: ${subScenario}`, ...(discrepancyExplanation?.()|| ["*** Needs explanation"])); 515 headerAdded = true; 516 } 517 baselines.push(...text); 518 } 519 } 520 521 function getBuildInfoForIncrementalCorrectnessCheck(text: string | undefined): { 522 buildInfo: string | undefined; 523 readableBuildInfo?: ReadableBuildInfo; 524 } { 525 if (!text) return { buildInfo: text }; 526 const readableBuildInfo = JSON.parse(text) as ReadableBuildInfo; 527 let sanitizedFileInfos: MapLike<ReadableProgramBuildInfoFileInfo | string> | undefined; 528 if (readableBuildInfo.program?.fileInfos) { 529 sanitizedFileInfos = {}; 530 for (const id in readableBuildInfo.program.fileInfos) { 531 if (hasProperty(readableBuildInfo.program.fileInfos, id)) { 532 const info = readableBuildInfo.program.fileInfos[id]; 533 sanitizedFileInfos[id] = isString(info) ? info : { ...info, signature: undefined }; 534 } 535 } 536 } 537 return { 538 buildInfo: JSON.stringify({ 539 ...readableBuildInfo, 540 program: readableBuildInfo.program && { 541 ...readableBuildInfo.program, 542 fileNames: undefined, 543 fileNamesList: undefined, 544 fileInfos: sanitizedFileInfos, 545 // Ignore noEmit since that shouldnt be reason to emit the tsbuild info and presence of it in the buildinfo file does not matter 546 options: { ...readableBuildInfo.program.options, noEmit: undefined }, 547 exportedModulesMap: undefined, 548 affectedFilesPendingEmit: undefined, 549 latestChangedDtsFile: readableBuildInfo.program.latestChangedDtsFile ? "FakeFileName" : undefined, 550 }, 551 size: undefined, // Size doesnt need to be equal 552 }, /*replacer*/ undefined, 2), 553 readableBuildInfo, 554 }; 555 } 556 557 export enum CleanBuildDescrepancy { 558 CleanFileTextDifferent, 559 CleanFilePresent, 560 } 561 562 export interface TestTscEdit { 563 modifyFs: (fs: vfs.FileSystem) => void; 564 subScenario: string; 565 commandLineArgs?: readonly string[]; 566 /** An array of lines to be printed in order when a discrepancy is detected */ 567 discrepancyExplanation?: () => readonly string[]; 568 } 569 570 export interface VerifyTscWithEditsInput extends TestTscCompile { 571 edits: TestTscEdit[]; 572 } 573 574 /** 575 * Verify non watch tsc invokcation after each edit 576 */ 577 export function verifyTscWithEdits({ 578 subScenario, fs, scenario, commandLineArgs, 579 baselineSourceMap, modifyFs, baselineReadFileCalls, baselinePrograms, 580 edits 581 }: VerifyTscWithEditsInput) { 582 describe(`tsc ${commandLineArgs.join(" ")} ${scenario}:: ${subScenario} serializedEdits`, () => { 583 let sys: TscCompileSystem; 584 let baseFs: vfs.FileSystem; 585 let editsSys: TscCompileSystem[]; 586 before(() => { 587 Debug.assert(!!edits.length, `${scenario}/${subScenario}:: No incremental scenarios, you probably want to use verifyTsc instead.`); 588 baseFs = fs().makeReadonly(); 589 sys = testTscCompile({ 590 scenario, 591 subScenario, 592 fs: () => baseFs, 593 commandLineArgs, 594 modifyFs, 595 baselineSourceMap, 596 baselineReadFileCalls, 597 baselinePrograms 598 }); 599 edits.forEach(( 600 { modifyFs, subScenario: editScenario, commandLineArgs: editCommandLineArgs }, 601 index 602 ) => { 603 (editsSys || (editsSys = [])).push(testTscCompile({ 604 scenario, 605 subScenario: editScenario || subScenario, 606 diffWithInitial: true, 607 fs: () => index === 0 ? sys.vfs : editsSys[index - 1].vfs, 608 commandLineArgs: editCommandLineArgs || commandLineArgs, 609 modifyFs, 610 baselineSourceMap, 611 baselineReadFileCalls, 612 baselinePrograms 613 })); 614 }); 615 }); 616 after(() => { 617 baseFs = undefined!; 618 sys = undefined!; 619 editsSys = undefined!; 620 }); 621 verifyTscBaseline(() => ({ 622 baseLine: () => { 623 const { file, text } = sys.baseLine(); 624 const texts: string[] = [text]; 625 editsSys.forEach((sys, index) => { 626 const incrementalScenario = edits[index]; 627 texts.push(""); 628 texts.push(`Change:: ${incrementalScenario.subScenario}`); 629 texts.push(sys.baseLine().text); 630 }); 631 return { file, text: texts.join("\r\n") }; 632 } 633 })); 634 it("tsc invocation after edit and clean build correctness", () => { 635 let baselines: string[] | undefined; 636 for (let index = 0; index < edits.length; index++) { 637 baselines = verifyTscEditDiscrepancies({ 638 index, 639 scenario, 640 subScenario: edits[index].subScenario, 641 baselines, 642 baseFs, 643 newSys: editsSys[index], 644 commandLineArgs: edits[index].commandLineArgs || commandLineArgs, 645 discrepancyExplanation: edits[index].discrepancyExplanation, 646 editFs: fs => { 647 for (let i = 0; i <= index; i++) { 648 edits[i].modifyFs(fs); 649 } 650 }, 651 modifyFs 652 }); 653 } 654 Harness.Baseline.runBaseline( 655 `${isBuild(commandLineArgs) ? "tsbuild" : "tsc"}/${scenario}/${subScenario.split(" ").join("-")}-discrepancies.js`, 656 baselines ? baselines.join("\r\n") : null // eslint-disable-line no-null/no-null 657 ); 658 }); 659 }); 660 } 661 662 export function enableStrict(fs: vfs.FileSystem, path: string) { 663 replaceText(fs, path, `"strict": false`, `"strict": true`); 664 } 665 666 export function addTestPrologue(fs: vfs.FileSystem, path: string, prologue: string) { 667 prependText(fs, path, `${prologue} 668`); 669 } 670 671 export function addShebang(fs: vfs.FileSystem, project: string, file: string) { 672 prependText(fs, `src/${project}/${file}.ts`, `#!someshebang ${project} ${file} 673`); 674 } 675 676 export function restContent(project: string, file: string) { 677 return `function for${project}${file}Rest() { 678const { b, ...rest } = { a: 10, b: 30, yy: 30 }; 679}`; 680 } 681 682 function nonrestContent(project: string, file: string) { 683 return `function for${project}${file}Rest() { }`; 684 } 685 686 export function addRest(fs: vfs.FileSystem, project: string, file: string) { 687 appendText(fs, `src/${project}/${file}.ts`, restContent(project, file)); 688 } 689 690 export function removeRest(fs: vfs.FileSystem, project: string, file: string) { 691 replaceText(fs, `src/${project}/${file}.ts`, restContent(project, file), nonrestContent(project, file)); 692 } 693 694 export function addStubFoo(fs: vfs.FileSystem, project: string, file: string) { 695 appendText(fs, `src/${project}/${file}.ts`, nonrestContent(project, file)); 696 } 697 698 export function changeStubToRest(fs: vfs.FileSystem, project: string, file: string) { 699 replaceText(fs, `src/${project}/${file}.ts`, nonrestContent(project, file), restContent(project, file)); 700 } 701 702 export function addSpread(fs: vfs.FileSystem, project: string, file: string) { 703 const path = `src/${project}/${file}.ts`; 704 const content = fs.readFileSync(path, "utf8"); 705 fs.writeFileSync(path, `${content} 706function ${project}${file}Spread(...b: number[]) { } 707const ${project}${file}_ar = [20, 30]; 708${project}${file}Spread(10, ...${project}${file}_ar);`); 709 710 replaceText(fs, `src/${project}/tsconfig.json`, `"strict": false,`, `"strict": false, 711 "downlevelIteration": true,`); 712 } 713 714 export function getTripleSlashRef(project: string) { 715 return `/src/${project}/tripleRef.d.ts`; 716 } 717 718 export function addTripleSlashRef(fs: vfs.FileSystem, project: string, file: string) { 719 fs.writeFileSync(getTripleSlashRef(project), `declare class ${project}${file} { }`); 720 prependText(fs, `src/${project}/${file}.ts`, `///<reference path="./tripleRef.d.ts"/> 721const ${file}Const = new ${project}${file}(); 722`); 723 } 724} 725