Skip to content

Commit fd4a8d1

Browse files
authored
Let the RWC harness iterate over files instead of building one big file (#18416)
* Let the RWC harness iterate over files instead of building one big file * Handle duplicated-only-in-case outputs better in the type baseliner * Always lowercase output names * Move common code into helper function * Always write .delete for missing files even if there were errors
1 parent d1c4754 commit fd4a8d1

File tree

3 files changed

+176
-54
lines changed

3 files changed

+176
-54
lines changed

Jakefile.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -769,15 +769,16 @@ function exec(cmd, completeHandler, errorHandler) {
769769
ex.run();
770770
}
771771

772+
const del = require("del");
772773
function cleanTestDirs() {
773774
// Clean the local baselines directory
774775
if (fs.existsSync(localBaseline)) {
775-
jake.rmRf(localBaseline);
776+
del.sync(localBaseline);
776777
}
777778

778779
// Clean the local Rwc baselines directory
779780
if (fs.existsSync(localRwcBaseline)) {
780-
jake.rmRf(localRwcBaseline);
781+
del.sync(localRwcBaseline);
781782
}
782783

783784
jake.mkdirP(localRwcBaseline);
@@ -1042,6 +1043,7 @@ function acceptBaseline(sourceFolder, targetFolder) {
10421043
if (fs.existsSync(target)) {
10431044
fs.unlinkSync(target);
10441045
}
1046+
jake.mkdirP(path.dirname(target));
10451047
fs.renameSync(path.join(sourceFolder, filename), target);
10461048
}
10471049
}

src/harness/harness.ts

Lines changed: 158 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,11 +1291,25 @@ namespace Harness {
12911291
}
12921292

12931293
export function getErrorBaseline(inputFiles: ReadonlyArray<TestFile>, diagnostics: ReadonlyArray<ts.Diagnostic>, pretty?: boolean) {
1294+
let outputLines = "";
1295+
const gen = iterateErrorBaseline(inputFiles, diagnostics, pretty);
1296+
for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) {
1297+
const [, content] = value;
1298+
outputLines += content;
1299+
}
1300+
return outputLines;
1301+
}
1302+
1303+
export const diagnosticSummaryMarker = "__diagnosticSummary";
1304+
export const globalErrorsMarker = "__globalErrors";
1305+
export function *iterateErrorBaseline(inputFiles: ReadonlyArray<TestFile>, diagnostics: ReadonlyArray<ts.Diagnostic>, pretty?: boolean): IterableIterator<[string, string, number]> {
12941306
diagnostics = diagnostics.slice().sort(ts.compareDiagnostics);
12951307
let outputLines = "";
12961308
// Count up all errors that were found in files other than lib.d.ts so we don't miss any
12971309
let totalErrorsReportedInNonLibraryFiles = 0;
12981310

1311+
let errorsReported = 0;
1312+
12991313
let firstLine = true;
13001314
function newLine() {
13011315
if (firstLine) {
@@ -1314,6 +1328,7 @@ namespace Harness {
13141328
.filter(s => s.length > 0)
13151329
.map(s => "!!! " + ts.DiagnosticCategory[error.category].toLowerCase() + " TS" + error.code + ": " + s);
13161330
errLines.forEach(e => outputLines += (newLine() + e));
1331+
errorsReported++;
13171332

13181333
// do not count errors from lib.d.ts here, they are computed separately as numLibraryDiagnostics
13191334
// if lib.d.ts is explicitly included in input files and there are some errors in it (i.e. because of duplicate identifiers)
@@ -1325,12 +1340,18 @@ namespace Harness {
13251340
}
13261341
}
13271342

1343+
yield [diagnosticSummaryMarker, minimalDiagnosticsToString(diagnostics, pretty) + Harness.IO.newLine() + Harness.IO.newLine(), diagnostics.length];
1344+
13281345
// Report global errors
13291346
const globalErrors = diagnostics.filter(err => !err.file);
13301347
globalErrors.forEach(outputErrorText);
1348+
yield [globalErrorsMarker, outputLines, errorsReported];
1349+
outputLines = "";
1350+
errorsReported = 0;
13311351

13321352
// 'merge' the lines of each input file with any errors associated with it
1333-
inputFiles.filter(f => f.content !== undefined).forEach(inputFile => {
1353+
const dupeCase = ts.createMap<number>();
1354+
for (const inputFile of inputFiles.filter(f => f.content !== undefined)) {
13341355
// Filter down to the errors in the file
13351356
const fileErrors = diagnostics.filter(e => {
13361357
const errFn = e.file;
@@ -1396,7 +1417,10 @@ namespace Harness {
13961417

13971418
// Verify we didn't miss any errors in this file
13981419
assert.equal(markedErrorCount, fileErrors.length, "count of errors in " + inputFile.unitName);
1399-
});
1420+
yield [checkDuplicatedFileName(inputFile.unitName, dupeCase), outputLines, errorsReported];
1421+
outputLines = "";
1422+
errorsReported = 0;
1423+
}
14001424

14011425
const numLibraryDiagnostics = ts.countWhere(diagnostics, diagnostic => {
14021426
return diagnostic.file && (isDefaultLibraryFile(diagnostic.file.fileName) || isBuiltFile(diagnostic.file.fileName));
@@ -1409,9 +1433,6 @@ namespace Harness {
14091433

14101434
// Verify we didn't miss any errors in total
14111435
assert.equal(totalErrorsReportedInNonLibraryFiles + numLibraryDiagnostics + numTest262HarnessDiagnostics, diagnostics.length, "total number of errors");
1412-
1413-
return minimalDiagnosticsToString(diagnostics, pretty) +
1414-
Harness.IO.newLine() + Harness.IO.newLine() + outputLines;
14151436
}
14161437

14171438
export function doErrorBaseline(baselinePath: string, inputFiles: TestFile[], errors: ts.Diagnostic[], pretty?: boolean) {
@@ -1425,7 +1446,7 @@ namespace Harness {
14251446
});
14261447
}
14271448

1428-
export function doTypeAndSymbolBaseline(baselinePath: string, result: CompilerResult, allFiles: {unitName: string, content: string}[], opts?: Harness.Baseline.BaselineOptions) {
1449+
export function doTypeAndSymbolBaseline(baselinePath: string, result: CompilerResult, allFiles: {unitName: string, content: string}[], opts?: Harness.Baseline.BaselineOptions, multifile?: boolean) {
14291450
if (result.errors.length !== 0) {
14301451
return;
14311452
}
@@ -1486,67 +1507,85 @@ namespace Harness {
14861507
return;
14871508

14881509
function checkBaseLines(isSymbolBaseLine: boolean) {
1489-
const fullBaseLine = generateBaseLine(fullResults, isSymbolBaseLine);
1490-
14911510
const fullExtension = isSymbolBaseLine ? ".symbols" : ".types";
1492-
14931511
// When calling this function from rwc-runner, the baselinePath will have no extension.
14941512
// As rwc test- file is stored in json which ".json" will get stripped off.
14951513
// When calling this function from compiler-runner, the baselinePath will then has either ".ts" or ".tsx" extension
14961514
const outputFileName = ts.endsWith(baselinePath, ts.Extension.Ts) || ts.endsWith(baselinePath, ts.Extension.Tsx) ?
1497-
baselinePath.replace(/\.tsx?/, fullExtension) : baselinePath.concat(fullExtension);
1498-
Harness.Baseline.runBaseline(outputFileName, () => fullBaseLine, opts);
1515+
baselinePath.replace(/\.tsx?/, "") : baselinePath;
1516+
1517+
if (!multifile) {
1518+
const fullBaseLine = generateBaseLine(fullResults, isSymbolBaseLine);
1519+
Harness.Baseline.runBaseline(outputFileName + fullExtension, () => fullBaseLine, opts);
1520+
}
1521+
else {
1522+
Harness.Baseline.runMultifileBaseline(outputFileName, fullExtension, () => {
1523+
return iterateBaseLine(fullResults, isSymbolBaseLine);
1524+
}, opts);
1525+
}
14991526
}
15001527

15011528
function generateBaseLine(typeWriterResults: ts.Map<TypeWriterResult[]>, isSymbolBaseline: boolean): string {
1502-
const typeLines: string[] = [];
1529+
let result = "";
1530+
const gen = iterateBaseLine(typeWriterResults, isSymbolBaseline);
1531+
for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) {
1532+
const [, content] = value;
1533+
result += content;
1534+
}
1535+
return result;
1536+
}
1537+
1538+
function *iterateBaseLine(typeWriterResults: ts.Map<TypeWriterResult[]>, isSymbolBaseline: boolean): IterableIterator<[string, string]> {
1539+
let typeLines = "";
15031540
const typeMap: { [fileName: string]: { [lineNum: number]: string[]; } } = {};
1541+
const dupeCase = ts.createMap<number>();
15041542

1505-
allFiles.forEach(file => {
1543+
for (const file of allFiles) {
15061544
const codeLines = file.content.split("\n");
1545+
const key = file.unitName;
15071546
typeWriterResults.get(file.unitName).forEach(result => {
15081547
if (isSymbolBaseline && !result.symbol) {
15091548
return;
15101549
}
15111550

15121551
const typeOrSymbolString = isSymbolBaseline ? result.symbol : result.type;
15131552
const formattedLine = result.sourceText.replace(/\r?\n/g, "") + " : " + typeOrSymbolString;
1514-
if (!typeMap[file.unitName]) {
1515-
typeMap[file.unitName] = {};
1553+
if (!typeMap[key]) {
1554+
typeMap[key] = {};
15161555
}
15171556

15181557
let typeInfo = [formattedLine];
1519-
const existingTypeInfo = typeMap[file.unitName][result.line];
1558+
const existingTypeInfo = typeMap[key][result.line];
15201559
if (existingTypeInfo) {
15211560
typeInfo = existingTypeInfo.concat(typeInfo);
15221561
}
1523-
typeMap[file.unitName][result.line] = typeInfo;
1562+
typeMap[key][result.line] = typeInfo;
15241563
});
15251564

1526-
typeLines.push("=== " + file.unitName + " ===\r\n");
1565+
typeLines += "=== " + file.unitName + " ===\r\n";
15271566
for (let i = 0; i < codeLines.length; i++) {
15281567
const currentCodeLine = codeLines[i];
1529-
typeLines.push(currentCodeLine + "\r\n");
1530-
if (typeMap[file.unitName]) {
1531-
const typeInfo = typeMap[file.unitName][i];
1568+
typeLines += currentCodeLine + "\r\n";
1569+
if (typeMap[key]) {
1570+
const typeInfo = typeMap[key][i];
15321571
if (typeInfo) {
15331572
typeInfo.forEach(ty => {
1534-
typeLines.push(">" + ty + "\r\n");
1573+
typeLines += ">" + ty + "\r\n";
15351574
});
15361575
if (i + 1 < codeLines.length && (codeLines[i + 1].match(/^\s*[{|}]\s*$/) || codeLines[i + 1].trim() === "")) {
15371576
}
15381577
else {
1539-
typeLines.push("\r\n");
1578+
typeLines += "\r\n";
15401579
}
15411580
}
15421581
}
15431582
else {
1544-
typeLines.push("No type information for this code.");
1583+
typeLines += "No type information for this code.";
15451584
}
15461585
}
1547-
});
1548-
1549-
return typeLines.join("");
1586+
yield [checkDuplicatedFileName(file.unitName, dupeCase), typeLines];
1587+
typeLines = "";
1588+
}
15501589
}
15511590
}
15521591

@@ -1642,31 +1681,54 @@ namespace Harness {
16421681
}
16431682

16441683
export function collateOutputs(outputFiles: Harness.Compiler.GeneratedFile[]): string {
1645-
// Collect, test, and sort the fileNames
1646-
outputFiles.sort((a, b) => ts.compareStrings(cleanName(a.fileName), cleanName(b.fileName)));
1647-
1684+
const gen = iterateOutputs(outputFiles);
16481685
// Emit them
16491686
let result = "";
1650-
for (const outputFile of outputFiles) {
1687+
for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) {
16511688
// Some extra spacing if this isn't the first file
16521689
if (result.length) {
16531690
result += "\r\n\r\n";
16541691
}
1655-
16561692
// FileName header + content
1657-
result += "/*====== " + outputFile.fileName + " ======*/\r\n";
1658-
1659-
result += outputFile.code;
1693+
const [, content] = value;
1694+
result += content;
16601695
}
1661-
16621696
return result;
1697+
}
1698+
1699+
export function *iterateOutputs(outputFiles: Harness.Compiler.GeneratedFile[]): IterableIterator<[string, string]> {
1700+
// Collect, test, and sort the fileNames
1701+
outputFiles.sort((a, b) => ts.compareStrings(cleanName(a.fileName), cleanName(b.fileName)));
1702+
const dupeCase = ts.createMap<number>();
1703+
// Yield them
1704+
for (const outputFile of outputFiles) {
1705+
yield [checkDuplicatedFileName(outputFile.fileName, dupeCase), "/*====== " + outputFile.fileName + " ======*/\r\n" + outputFile.code];
1706+
}
16631707

16641708
function cleanName(fn: string) {
16651709
const lastSlash = ts.normalizeSlashes(fn).lastIndexOf("/");
16661710
return fn.substr(lastSlash + 1).toLowerCase();
16671711
}
16681712
}
16691713

1714+
function checkDuplicatedFileName(resultName: string, dupeCase: ts.Map<number>): string {
1715+
resultName = sanitizeTestFilePath(resultName);
1716+
if (dupeCase.has(resultName)) {
1717+
// A different baseline filename should be manufactured if the names differ only in case, for windows compat
1718+
const count = 1 + dupeCase.get(resultName);
1719+
dupeCase.set(resultName, count);
1720+
resultName = `${resultName}.dupe${count}`;
1721+
}
1722+
else {
1723+
dupeCase.set(resultName, 0);
1724+
}
1725+
return resultName;
1726+
}
1727+
1728+
function sanitizeTestFilePath(name: string) {
1729+
return ts.normalizeSlashes(name.replace(/[\^<>:"|?*%]/g, "_")).replace(/\.\.\//g, "__dotdot/").toLowerCase();
1730+
}
1731+
16701732
// This does not need to exist strictly speaking, but many tests will need to be updated if it's removed
16711733
export function compileString(_code: string, _unitName: string, _callback: (result: CompilerResult) => void) {
16721734
// NEWTODO: Re-implement 'compileString'
@@ -2004,6 +2066,66 @@ namespace Harness {
20042066
const comparison = compareToBaseline(actual, relativeFileName, opts);
20052067
writeComparison(comparison.expected, comparison.actual, relativeFileName, actualFileName);
20062068
}
2069+
2070+
export function runMultifileBaseline(relativeFileBase: string, extension: string, generateContent: () => IterableIterator<[string, string, number]> | IterableIterator<[string, string]>, opts?: BaselineOptions, referencedExtensions?: string[]): void {
2071+
const gen = generateContent();
2072+
const writtenFiles = ts.createMap<true>();
2073+
const canonicalize = ts.createGetCanonicalFileName(/*caseSensitive*/ false); // This is done so tests work on windows _and_ linux
2074+
/* tslint:disable-next-line:no-null-keyword */
2075+
const errors: Error[] = [];
2076+
if (gen !== null) {
2077+
for (let {done, value} = gen.next(); !done; { done, value } = gen.next()) {
2078+
const [name, content, count] = value as [string, string, number | undefined];
2079+
if (count === 0) continue; // Allow error reporter to skip writing files without errors
2080+
const relativeFileName = ts.combinePaths(relativeFileBase, name) + extension;
2081+
const actualFileName = localPath(relativeFileName, opts && opts.Baselinefolder, opts && opts.Subfolder);
2082+
const actual = content;
2083+
const comparison = compareToBaseline(actual, relativeFileName, opts);
2084+
try {
2085+
writeComparison(comparison.expected, comparison.actual, relativeFileName, actualFileName);
2086+
}
2087+
catch (e) {
2088+
errors.push(e);
2089+
}
2090+
const path = ts.toPath(relativeFileName, "", canonicalize);
2091+
writtenFiles.set(path, true);
2092+
}
2093+
}
2094+
2095+
const referenceDir = referencePath(relativeFileBase, opts && opts.Baselinefolder, opts && opts.Subfolder);
2096+
let existing = Harness.IO.readDirectory(referenceDir, referencedExtensions || [extension]);
2097+
if (extension === ".ts" || referencedExtensions && referencedExtensions.indexOf(".ts") > -1 && referencedExtensions.indexOf(".d.ts") === -1) {
2098+
// special-case and filter .d.ts out of .ts results
2099+
existing = existing.filter(f => !ts.endsWith(f, ".d.ts"));
2100+
}
2101+
const missing: string[] = [];
2102+
for (const name of existing) {
2103+
const localCopy = name.substring(referenceDir.length - relativeFileBase.length);
2104+
const path = ts.toPath(localCopy, "", canonicalize);
2105+
if (!writtenFiles.has(path)) {
2106+
missing.push(localCopy);
2107+
}
2108+
}
2109+
if (missing.length) {
2110+
for (const file of missing) {
2111+
IO.writeFile(localPath(file + ".delete", opts && opts.Baselinefolder, opts && opts.Subfolder), "");
2112+
}
2113+
}
2114+
2115+
if (errors.length || missing.length) {
2116+
let errorMsg = "";
2117+
if (errors.length) {
2118+
errorMsg += `The baseline for ${relativeFileBase} has changed:${"\n " + errors.map(e => e.message).join("\n ")}`;
2119+
}
2120+
if (errors.length && missing.length) {
2121+
errorMsg += "\n";
2122+
}
2123+
if (missing.length) {
2124+
errorMsg += `Baseline missing files:${"\n " + missing.join("\n ") + "\n"}Written:${"\n " + ts.arrayFrom(writtenFiles.keys()).join("\n ")}`;
2125+
}
2126+
throw new Error(errorMsg);
2127+
}
2128+
}
20072129
}
20082130

20092131
export function isDefaultLibraryFile(filePath: string): boolean {

0 commit comments

Comments
 (0)