Handle output file names descripency between tsc --b and actual program emit file path calculation (#41811)

* Baseline showing #41801 and other issues with output path calculation

* Add a way to note descripencies between clean and incremental build

* Add descripency when no rootDir is specified but project is composite

* if rootDir is specified, irrespective of whether all files belong to rootDir, the paths should be calculated from rootDir

* Fix the output file names api to use the correct common source directory

* Tests for #41780

* Spelling
This commit is contained in:
Sheetal Nandi
2020-12-07 11:53:22 -08:00
committed by GitHub
parent 37e898cfd7
commit bfb259128b
23 changed files with 745 additions and 87 deletions

View File

@@ -125,6 +125,7 @@
"unittests/tsbuild/moduleSpecifiers.ts",
"unittests/tsbuild/noEmitOnError.ts",
"unittests/tsbuild/outFile.ts",
"unittests/tsbuild/outputPaths.ts",
"unittests/tsbuild/referencesWithRootDirInParent.ts",
"unittests/tsbuild/resolveJsonModule.ts",
"unittests/tsbuild/sample.ts",

View File

@@ -270,11 +270,12 @@ interface Symbol {
tick: () => void;
baseFs: vfs.FileSystem;
newSys: TscCompileSystem;
cleanBuildDiscrepancies: TscIncremental["cleanBuildDiscrepancies"];
}
function verifyIncrementalCorrectness(input: () => VerifyIncrementalCorrectness, index: number) {
it(`Verify emit output file text is same when built clean for incremental scenario at:: ${index}`, () => {
const {
scenario, subScenario, commandLineArgs,
scenario, subScenario, commandLineArgs, cleanBuildDiscrepancies,
modifyFs, incrementalModifyFs,
tick, baseFs, newSys
} = input();
@@ -289,54 +290,82 @@ interface Symbol {
incrementalModifyFs(fs);
},
});
const discrepancies = cleanBuildDiscrepancies?.();
for (const outputFile of arrayFrom(sys.writtenFiles.keys())) {
const expectedText = sys.readFile(outputFile);
const actualText = newSys.readFile(outputFile);
const cleanBuildText = sys.readFile(outputFile);
const incrementalBuildText = newSys.readFile(outputFile);
const descrepancyInClean = discrepancies?.get(outputFile);
if (!isBuildInfoFile(outputFile)) {
assert.equal(actualText, expectedText, `File: ${outputFile}`);
verifyTextEqual(incrementalBuildText, cleanBuildText, descrepancyInClean, `File: ${outputFile}`);
}
else if (actualText !== expectedText) {
else if (incrementalBuildText !== cleanBuildText) {
// Verify build info without affectedFilesPendingEmit
const { buildInfo: actualBuildInfo, affectedFilesPendingEmit: actualAffectedFilesPendingEmit } = getBuildInfoForIncrementalCorrectnessCheck(actualText);
const { buildInfo: expectedBuildInfo, affectedFilesPendingEmit: expectedAffectedFilesPendingEmit } = getBuildInfoForIncrementalCorrectnessCheck(expectedText);
assert.deepEqual(actualBuildInfo, expectedBuildInfo, `TsBuild info text without affectedFilesPendingEmit: ${outputFile}::\nIncremental buildInfoText:: ${actualText}\nClean buildInfoText:: ${expectedText}`);
const { buildInfo: incrementalBuildInfo, affectedFilesPendingEmit: incrementalBuildAffectedFilesPendingEmit } = getBuildInfoForIncrementalCorrectnessCheck(incrementalBuildText);
const { buildInfo: cleanBuildInfo, affectedFilesPendingEmit: incrementalAffectedFilesPendingEmit } = getBuildInfoForIncrementalCorrectnessCheck(cleanBuildText);
verifyTextEqual(incrementalBuildInfo, cleanBuildInfo, descrepancyInClean, `TsBuild info text without affectedFilesPendingEmit ${subScenario}:: ${outputFile}::\nIncremental buildInfoText:: ${incrementalBuildText}\nClean buildInfoText:: ${cleanBuildText}`);
// 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
if (actualAffectedFilesPendingEmit) {
assert.isDefined(expectedAffectedFilesPendingEmit, `Incremental build contains affectedFilesPendingEmit, clean build should also have it: ${outputFile}::\nIncremental buildInfoText:: ${actualText}\nClean buildInfoText:: ${expectedText}`);
if (incrementalBuildAffectedFilesPendingEmit && descrepancyInClean === undefined) {
assert.isDefined(incrementalAffectedFilesPendingEmit, `Incremental build contains affectedFilesPendingEmit, clean build should also have it: ${outputFile}::\nIncremental buildInfoText:: ${incrementalBuildText}\nClean buildInfoText:: ${cleanBuildText}`);
let expectedIndex = 0;
actualAffectedFilesPendingEmit.forEach(([actualFile]) => {
expectedIndex = findIndex(expectedAffectedFilesPendingEmit!, ([expectedFile]) => actualFile === expectedFile, expectedIndex);
assert.notEqual(expectedIndex, -1, `Incremental build contains ${actualFile} file as pending emit, clean build should also have it: ${outputFile}::\nIncremental buildInfoText:: ${actualText}\nClean buildInfoText:: ${expectedText}`);
incrementalBuildAffectedFilesPendingEmit.forEach(([actualFile]) => {
expectedIndex = findIndex(incrementalAffectedFilesPendingEmit!, ([expectedFile]) => actualFile === expectedFile, expectedIndex);
assert.notEqual(expectedIndex, -1, `Incremental build contains ${actualFile} file as pending emit, clean build should also have it: ${outputFile}::\nIncremental buildInfoText:: ${incrementalBuildText}\nClean buildInfoText:: ${cleanBuildText}`);
expectedIndex++;
});
}
}
}
function verifyTextEqual(incrementalText: string | undefined, cleanText: string | undefined, descrepancyInClean: CleanBuildDescrepancy | undefined, message: string) {
if (descrepancyInClean === undefined) {
assert.equal(incrementalText, cleanText, message);
return;
}
switch (descrepancyInClean) {
case CleanBuildDescrepancy.CleanFileTextDifferent:
assert.isDefined(incrementalText, `Incremental file should be present:: ${message}`);
assert.isDefined(cleanText, `Clean file should be present present:: ${message}`);
assert.notEqual(incrementalText, cleanText, message);
return;
case CleanBuildDescrepancy.CleanFilePresent:
assert.isUndefined(incrementalText, `Incremental file should be absent:: ${message}`);
assert.isDefined(cleanText, `Clean file should be present:: ${message}`);
return;
default:
Debug.assertNever(descrepancyInClean);
}
}
});
}
function getBuildInfoForIncrementalCorrectnessCheck(text: string | undefined): { buildInfo: BuildInfo | undefined; affectedFilesPendingEmit?: ProgramBuildInfo["affectedFilesPendingEmit"]; } {
function getBuildInfoForIncrementalCorrectnessCheck(text: string | undefined): { buildInfo: string | undefined; affectedFilesPendingEmit?: ProgramBuildInfo["affectedFilesPendingEmit"]; } {
const buildInfo = text ? getBuildInfo(text) : undefined;
if (!buildInfo?.program) return { buildInfo };
if (!buildInfo?.program) return { buildInfo: text };
// Ignore noEmit since that shouldnt be reason to emit the tsbuild info and presence of it in the buildinfo file does not matter
const { program: { affectedFilesPendingEmit, options: { noEmit, ...optionsRest}, ...programRest }, ...rest } = buildInfo;
return {
buildInfo: {
buildInfo: getBuildInfoText({
...rest,
program: {
options: optionsRest,
...programRest
}
},
}),
affectedFilesPendingEmit
};
}
export enum CleanBuildDescrepancy {
CleanFileTextDifferent,
CleanFilePresent,
}
export interface TscIncremental {
buildKind: BuildKind;
modifyFs: (fs: vfs.FileSystem) => void;
subScenario?: string;
commandLineArgs?: readonly string[];
cleanBuildDiscrepancies?: () => ESMap<string, CleanBuildDescrepancy>;
}
export interface VerifyTsBuildInput extends VerifyTsBuildInputWorker {
@@ -396,7 +425,8 @@ interface Symbol {
buildKind,
modifyFs: incrementalModifyFs,
subScenario: incrementalSubScenario,
commandLineArgs: incrementalCommandLineArgs
commandLineArgs: incrementalCommandLineArgs,
cleanBuildDiscrepancies,
}, index) => {
describe(incrementalSubScenario || buildKind, () => {
let newSys: TscCompileSystem;
@@ -425,10 +455,11 @@ interface Symbol {
verifyTscBaseline(() => newSys);
verifyIncrementalCorrectness(() => ({
scenario,
subScenario,
subScenario: incrementalSubScenario || subScenario,
baseFs,
newSys,
commandLineArgs: incrementalCommandLineArgs || commandLineArgs,
cleanBuildDiscrepancies,
incrementalModifyFs,
modifyFs,
tick
@@ -520,12 +551,13 @@ interface Symbol {
}));
});
describe("incremental correctness", () => {
incrementalScenarios.forEach(({ commandLineArgs: incrementalCommandLineArgs }, index) => verifyIncrementalCorrectness(() => ({
incrementalScenarios.forEach(({ commandLineArgs: incrementalCommandLineArgs, subScenario, buildKind, cleanBuildDiscrepancies }, index) => verifyIncrementalCorrectness(() => ({
scenario,
subScenario,
subScenario: subScenario || buildKind,
baseFs,
newSys: incrementalSys[index],
commandLineArgs: incrementalCommandLineArgs || commandLineArgs,
cleanBuildDiscrepancies,
incrementalModifyFs: fs => {
for (let i = 0; i <= index; i++) {
incrementalScenarios[i].modifyFs(fs);

View File

@@ -0,0 +1,117 @@
namespace ts {
describe("unittests:: tsbuild - output file paths", () => {
const noChangeProject: TscIncremental = {
buildKind: BuildKind.NoChangeRun,
modifyFs: noop,
subScenario: "Normal build without change, that does not block emit on error to show files that get emitted",
commandLineArgs: ["-p", "/src/tsconfig.json"],
};
const incrementalScenarios: TscIncremental[] = [
noChangeRun,
noChangeProject,
];
function verify(input: Pick<VerifyTsBuildInput, "subScenario" | "fs" | "incrementalScenarios">, expectedOuptutNames: readonly string[]) {
verifyTscSerializedIncrementalEdits({
scenario: "outputPaths",
commandLineArgs: ["--b", "/src/tsconfig.json", "-v"],
...input
});
it("verify getOutputFileNames", () => {
const sys = new fakes.System(input.fs().makeReadonly(), { executingFilePath: "/lib/tsc" }) as TscCompileSystem;
;
assert.deepEqual(
getOutputFileNames(
parseConfigFileWithSystem("/src/tsconfig.json", {}, {}, sys, noop)!,
"/src/src/index.ts",
/*ignoreCase*/ false
),
expectedOuptutNames
);
});
}
verify({
subScenario: "when rootDir is not specified",
fs: () => loadProjectFromFiles({
"/src/src/index.ts": "export const x = 10;",
"/src/tsconfig.json": JSON.stringify({
compilerOptions: {
outDir: "dist"
}
})
}),
incrementalScenarios,
}, ["/src/dist/index.js"]);
verify({
subScenario: "when rootDir is not specified and is composite",
fs: () => loadProjectFromFiles({
"/src/src/index.ts": "export const x = 10;",
"/src/tsconfig.json": JSON.stringify({
compilerOptions: {
outDir: "dist",
composite: true
}
})
}),
incrementalScenarios: [
noChangeRun,
{
...noChangeProject,
cleanBuildDiscrepancies: () => {
const map = new Map<string, CleanBuildDescrepancy>();
map.set("/src/dist/tsconfig.tsbuildinfo", CleanBuildDescrepancy.CleanFileTextDifferent); // tsbuildinfo will have -p setting when built using -p vs no build happens incrementally because of no change.
return map;
}
}
],
}, ["/src/dist/src/index.js", "/src/dist/src/index.d.ts"]);
verify({
subScenario: "when rootDir is specified",
fs: () => loadProjectFromFiles({
"/src/src/index.ts": "export const x = 10;",
"/src/tsconfig.json": JSON.stringify({
compilerOptions: {
outDir: "dist",
rootDir: "src"
}
})
}),
incrementalScenarios,
}, ["/src/dist/index.js"]);
verify({
subScenario: "when rootDir is specified but not all files belong to rootDir",
fs: () => loadProjectFromFiles({
"/src/src/index.ts": "export const x = 10;",
"/src/types/type.ts": "export type t = string;",
"/src/tsconfig.json": JSON.stringify({
compilerOptions: {
outDir: "dist",
rootDir: "src"
}
})
}),
incrementalScenarios,
}, ["/src/dist/index.js"]);
verify({
subScenario: "when rootDir is specified but not all files belong to rootDir and is composite",
fs: () => loadProjectFromFiles({
"/src/src/index.ts": "export const x = 10;",
"/src/types/type.ts": "export type t = string;",
"/src/tsconfig.json": JSON.stringify({
compilerOptions: {
outDir: "dist",
rootDir: "src",
composite: true
}
})
}),
incrementalScenarios,
}, ["/src/dist/index.js", "/src/dist/index.d.ts"]);
});
}