import { Baseline, Compiler, FileBasedTest, FileBasedTestConfiguration, getFileBasedTestConfigurationDescription, getFileBasedTestConfigurations, IO, RunnerBase, TestCaseParser, TestRunnerKind, } from "./_namespaces/Harness.js"; import * as ts from "./_namespaces/ts.js"; import * as Utils from "./_namespaces/Utils.js"; import * as vfs from "./_namespaces/vfs.js"; import * as vpath from "./_namespaces/vpath.js"; export const enum CompilerTestType { Conformance, Regressions, } interface CompilerFileBasedTest extends FileBasedTest { readonly content?: string; } export class CompilerBaselineRunner extends RunnerBase { private basePath = "tests/cases"; private testSuiteName: TestRunnerKind; private emit: boolean; public options: string | undefined; constructor(public testType: CompilerTestType) { super(); this.emit = true; if (testType === CompilerTestType.Conformance) { this.testSuiteName = "conformance"; } else if (testType === CompilerTestType.Regressions) { this.testSuiteName = "compiler"; } else { this.testSuiteName = "compiler"; // default to this for historical reasons } this.basePath += "/" + this.testSuiteName; } public kind(): TestRunnerKind { return this.testSuiteName; } private testFiles: string[] | undefined; public enumerateTestFiles(): string[] { // see also: `enumerateTestFiles` in tests/webTestServer.ts return this.testFiles ??= this.enumerateFiles(this.basePath, /\.tsx?$/, { recursive: true }); } public initializeTests(): void { describe(this.testSuiteName + " tests", () => { describe("Setup compiler for compiler baselines", () => { this.parseOptions(); }); // this will set up a series of describe/it blocks to run between the setup and cleanup phases const files = this.tests.length > 0 ? this.tests : IO.enumerateTestFiles(this); files.forEach(file => { this.checkTestCodeOutput(vpath.normalizeSeparators(file), CompilerTest.getConfigurations(file)); }); }); } public checkTestCodeOutput(fileName: string, test?: CompilerFileBasedTest): void { if (test && ts.some(test.configurations)) { test.configurations.forEach(configuration => { describe(`${this.testSuiteName} tests for ${fileName}${configuration ? ` (${getFileBasedTestConfigurationDescription(configuration)})` : ``}`, () => { this.runSuite(fileName, test, configuration); }); }); } else { describe(`${this.testSuiteName} tests for ${fileName}`, () => { this.runSuite(fileName, test); }); } } private runSuite(fileName: string, test?: CompilerFileBasedTest, configuration?: FileBasedTestConfiguration) { // Mocha holds onto the closure environment of the describe callback even after the test is done. // Everything declared here should be cleared out in the "after" callback. let compilerTest!: CompilerTest; before(() => { let payload; if (test && test.content) { payload = TestCaseParser.makeUnitsFromTest(test.content, test.file); } compilerTest = new CompilerTest(fileName, payload, configuration); }); it(`Correct errors for ${fileName}`, () => compilerTest.verifyDiagnostics()); it(`Correct module resolution tracing for ${fileName}`, () => compilerTest.verifyModuleResolution()); it(`Correct sourcemap content for ${fileName}`, () => compilerTest.verifySourceMapRecord()); it(`Correct JS output for ${fileName}`, () => (this.emit && compilerTest.verifyJavaScriptOutput())); it(`Correct Sourcemap output for ${fileName}`, () => compilerTest.verifySourceMapOutput()); it(`Correct type/symbol baselines for ${fileName}`, () => compilerTest.verifyTypesAndSymbols()); after(() => { compilerTest = undefined!; }); } private parseOptions() { if (this.options && this.options.length > 0) { this.emit = false; const opts = this.options.split(","); for (const opt of opts) { switch (opt) { case "emit": this.emit = true; break; default: throw new Error("unsupported flag"); } } } } } class CompilerTest { private static varyBy: readonly string[] = [ // implicit variations from defined options ...ts.optionDeclarations .filter(option => !option.isCommandLineOnly && ( option.type === "boolean" || typeof option.type === "object" ) && ( option.affectsProgramStructure || option.affectsEmit || option.affectsModuleResolution || option.affectsBindDiagnostics || option.affectsSemanticDiagnostics || option.affectsSourceFile || option.affectsDeclarationPath || option.affectsBuildInfo ) ) .map(option => option.name), // explicit variations that do not match above conditions "noEmit", "isolatedModules", ]; private fileName: string; private justName: string; private configuredName: string; private harnessSettings: TestCaseParser.CompilerSettings; private hasNonDtsFiles: boolean; private result: Compiler.CompileFilesResult; private options: ts.CompilerOptions; private tsConfigFiles: Compiler.TestFile[]; // equivalent to the files that will be passed on the command line private toBeCompiled: Compiler.TestFile[]; // equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files) private otherFiles: Compiler.TestFile[]; constructor(fileName: string, testCaseContent?: TestCaseParser.TestCaseContent, configurationOverrides?: TestCaseParser.CompilerSettings) { const absoluteRootDir = vfs.srcFolder; this.fileName = fileName; this.justName = vpath.basename(fileName); this.configuredName = this.justName; if (configurationOverrides) { let configuredName = ""; const keys = Object .keys(configurationOverrides) .sort(); for (const key of keys) { if (configuredName) { configuredName += ","; } configuredName += `${key.toLowerCase()}=${configurationOverrides[key].toLowerCase()}`; } if (configuredName) { const extname = vpath.extname(this.justName); const basename = vpath.basename(this.justName, extname, /*ignoreCase*/ true); this.configuredName = `${basename}(${configuredName})${extname}`; } } if (testCaseContent === undefined) { testCaseContent = TestCaseParser.makeUnitsFromTest(IO.readFile(fileName)!, fileName); } if (configurationOverrides) { testCaseContent = { ...testCaseContent, settings: { ...testCaseContent.settings, ...configurationOverrides } }; } const units = testCaseContent.testUnitData; this.toBeCompiled = []; this.otherFiles = []; this.hasNonDtsFiles = units.some(unit => !ts.fileExtensionIs(unit.name, ts.Extension.Dts)); this.harnessSettings = testCaseContent.settings; let tsConfigOptions: ts.CompilerOptions | undefined; this.tsConfigFiles = []; if (testCaseContent.tsConfig) { tsConfigOptions = ts.cloneCompilerOptions(testCaseContent.tsConfig.options); this.tsConfigFiles.push(this.createHarnessTestFile(testCaseContent.tsConfigFileUnitData!)); for (const unit of units) { if (testCaseContent.tsConfig.fileNames.includes(ts.getNormalizedAbsolutePath(unit.name, absoluteRootDir))) { this.toBeCompiled.push(this.createHarnessTestFile(unit)); } else { this.otherFiles.push(this.createHarnessTestFile(unit)); } } } else { const baseUrl = this.harnessSettings.baseUrl; if (baseUrl !== undefined && !ts.isRootedDiskPath(baseUrl)) { this.harnessSettings.baseUrl = ts.getNormalizedAbsolutePath(baseUrl, absoluteRootDir); } const lastUnit = units[units.length - 1]; // We need to assemble the list of input files for the compiler and other related files on the 'filesystem' (ie in a multi-file test) // If the last file in a test uses require or a triple slash reference we'll assume all other files will be brought in via references, // otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another. if (testCaseContent.settings.noImplicitReferences || /require\(/.test(lastUnit.content) || /reference\spath/.test(lastUnit.content)) { this.toBeCompiled.push(this.createHarnessTestFile(lastUnit)); units.forEach(unit => { if (unit.name !== lastUnit.name) { this.otherFiles.push(this.createHarnessTestFile(unit)); } }); } else { this.toBeCompiled = units.map(unit => { return this.createHarnessTestFile(unit); }); } } if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) { tsConfigOptions.configFile!.fileName = tsConfigOptions.configFilePath; } this.result = Compiler.compileFiles( this.toBeCompiled, this.otherFiles, this.harnessSettings, /*options*/ tsConfigOptions, /*currentDirectory*/ this.harnessSettings.currentDirectory, testCaseContent.symlinks, ); this.options = this.result.options; } public static getConfigurations(file: string): CompilerFileBasedTest { // also see `parseCompilerTestConfigurations` in tests/webTestServer.ts const content = IO.readFile(file)!; const settings = TestCaseParser.extractCompilerSettings(content); const configurations = getFileBasedTestConfigurations(settings, CompilerTest.varyBy); return { file, configurations, content }; } public verifyDiagnostics() { // check errors Compiler.doErrorBaseline( this.configuredName, this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles), this.result.diagnostics, !!this.options.pretty, ); } public verifyModuleResolution() { if (this.options.traceResolution) { Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".trace.json"), JSON.stringify(this.result.traces.map(Utils.sanitizeTraceResolutionLogEntry), undefined, 4)); } } public verifySourceMapRecord() { if (this.options.sourceMap || this.options.inlineSourceMap || this.options.declarationMap) { const record = Utils.removeTestPathPrefixes(this.result.getSourceMapRecord()!); const baseline = (this.options.noEmitOnError && this.result.diagnostics.length !== 0) || record === undefined // Because of the noEmitOnError option no files are created. We need to return null because baselining isn't required. ? null // eslint-disable-line no-restricted-syntax : record; Baseline.runBaseline(this.configuredName.replace(/\.tsx?$/, ".sourcemap.txt"), baseline); } } public verifyJavaScriptOutput() { if (this.hasNonDtsFiles) { Compiler.doJsEmitBaseline( this.configuredName, this.fileName, this.options, this.result, this.tsConfigFiles, this.toBeCompiled, this.otherFiles, this.harnessSettings, ); } } public verifySourceMapOutput() { Compiler.doSourcemapBaseline( this.configuredName, this.options, this.result, this.harnessSettings, ); } public verifyTypesAndSymbols() { const noTypesAndSymbols = this.harnessSettings.noTypesAndSymbols && this.harnessSettings.noTypesAndSymbols.toLowerCase() === "true"; if (noTypesAndSymbols) { return; } Compiler.doTypeAndSymbolBaseline( this.configuredName, this.fileName, this.result.program!, this.toBeCompiled.concat(this.otherFiles).filter(file => !!this.result.program!.getSourceFile(file.unitName)), /*opts*/ undefined, /*multifile*/ undefined, /*skipTypeBaselines*/ undefined, /*skipSymbolBaselines*/ undefined, !!ts.length(this.result.diagnostics), ); } private createHarnessTestFile(unit: TestCaseParser.TestUnitData): Compiler.TestFile { return { unitName: unit.name, content: unit.content, fileOptions: unit.fileOptions, }; } }