• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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