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