1import * as vpath from "./_namespaces/vpath"; 2import * as ts from "./_namespaces/ts"; 3import * as compiler from "./_namespaces/compiler"; 4import * as Utils from "./_namespaces/Utils"; 5import { 6 Baseline, Compiler, FileBasedTest, FileBasedTestConfiguration, getFileBasedTestConfigurationDescription, 7 getFileBasedTestConfigurations, IO, RunnerBase, TestCaseParser, TestRunnerKind, 8} from "./_namespaces/Harness"; 9 10export const enum CompilerTestType { 11 Conformance, 12 Regressions, 13 Test262, 14 OH 15} 16 17interface CompilerFileBasedTest extends FileBasedTest { 18 readonly content?: string; 19} 20 21export class CompilerBaselineRunner extends RunnerBase { 22 private basePath = "tests/cases"; 23 private testSuiteName: TestRunnerKind; 24 private emit: boolean; 25 26 public options: string | undefined; 27 28 constructor(public testType: CompilerTestType) { 29 super(); 30 this.emit = true; 31 if (testType === CompilerTestType.Conformance) { 32 this.testSuiteName = "conformance"; 33 } 34 else if (testType === CompilerTestType.Regressions) { 35 this.testSuiteName = "compiler"; 36 } 37 else if (testType === CompilerTestType.Test262) { 38 this.testSuiteName = "test262"; 39 } 40 else if (testType === CompilerTestType.OH) { 41 this.testSuiteName = "compiler-oh"; 42 } 43 else { 44 this.testSuiteName = "compiler"; // default to this for historical reasons 45 } 46 this.basePath += "/" + this.testSuiteName; 47 } 48 49 public kind() { 50 return this.testSuiteName; 51 } 52 53 public enumerateTestFiles() { 54 // see also: `enumerateTestFiles` in tests/webTestServer.ts 55 return this.enumerateFiles(this.basePath, /\.(ets|tsx?)$/, { recursive: true }).map(CompilerTest.getConfigurations); 56 } 57 58 public initializeTests() { 59 describe(this.testSuiteName + " tests", () => { 60 describe("Setup compiler for compiler baselines", () => { 61 this.parseOptions(); 62 }); 63 64 // this will set up a series of describe/it blocks to run between the setup and cleanup phases 65 const files = this.tests.length > 0 ? this.tests : IO.enumerateTestFiles(this); 66 files.forEach(test => { 67 const file = typeof test === "string" ? test : test.file; 68 this.checkTestCodeOutput(vpath.normalizeSeparators(file), typeof test === "string" ? CompilerTest.getConfigurations(test) : test); 69 }); 70 }); 71 } 72 73 public checkTestCodeOutput(fileName: string, test?: CompilerFileBasedTest) { 74 if (test && ts.some(test.configurations)) { 75 test.configurations.forEach(configuration => { 76 describe(`${this.testSuiteName} tests for ${fileName}${configuration ? ` (${getFileBasedTestConfigurationDescription(configuration)})` : ``}`, () => { 77 this.runSuite(fileName, test, configuration); 78 }); 79 }); 80 } 81 else { 82 describe(`${this.testSuiteName} tests for ${fileName}`, () => { 83 this.runSuite(fileName, test); 84 }); 85 } 86 } 87 88 private runSuite(fileName: string, test?: CompilerFileBasedTest, configuration?: FileBasedTestConfiguration) { 89 // Mocha holds onto the closure environment of the describe callback even after the test is done. 90 // Everything declared here should be cleared out in the "after" callback. 91 let compilerTest!: CompilerTest; 92 before(() => { 93 let payload; 94 if (test && test.content) { 95 const rootDir = test.file.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(test.file) + "/"; 96 payload = TestCaseParser.makeUnitsFromTest(test.content, test.file, rootDir); 97 } 98 compilerTest = new CompilerTest(fileName, payload, configuration); 99 }); 100 it(`Correct errors for ${fileName}`, () => compilerTest.verifyDiagnostics()); 101 it(`Correct module resolution tracing for ${fileName}`, () => compilerTest.verifyModuleResolution()); 102 it(`Correct sourcemap content for ${fileName}`, () => compilerTest.verifySourceMapRecord()); 103 it(`Correct JS output for ${fileName}`, () => (this.emit && compilerTest.verifyJavaScriptOutput())); 104 it(`Correct Sourcemap output for ${fileName}`, () => compilerTest.verifySourceMapOutput()); 105 it(`Correct type/symbol baselines for ${fileName}`, () => compilerTest.verifyTypesAndSymbols()); 106 after(() => { 107 compilerTest = undefined!; 108 }); 109 } 110 111 private parseOptions() { 112 if (this.options && this.options.length > 0) { 113 this.emit = false; 114 115 const opts = this.options.split(","); 116 for (const opt of opts) { 117 switch (opt) { 118 case "emit": 119 this.emit = true; 120 break; 121 default: 122 throw new Error("unsupported flag"); 123 } 124 } 125 } 126 } 127} 128 129class CompilerTest { 130 private static varyBy: readonly string[] = [ 131 "module", 132 "moduleResolution", 133 "moduleDetection", 134 "target", 135 "jsx", 136 "removeComments", 137 "importHelpers", 138 "importHelpers", 139 "downlevelIteration", 140 "isolatedModules", 141 "strict", 142 "noImplicitAny", 143 "strictNullChecks", 144 "strictFunctionTypes", 145 "strictBindCallApply", 146 "strictPropertyInitialization", 147 "noImplicitThis", 148 "alwaysStrict", 149 "allowSyntheticDefaultImports", 150 "esModuleInterop", 151 "emitDecoratorMetadata", 152 "skipDefaultLibCheck", 153 "preserveConstEnums", 154 "skipLibCheck", 155 "exactOptionalPropertyTypes", 156 "useDefineForClassFields", 157 "useUnknownInCatchVariables", 158 "noUncheckedIndexedAccess", 159 "noPropertyAccessFromIndexSignature", 160 ]; 161 private fileName: string; 162 private justName: string; 163 private configuredName: string; 164 private lastUnit: TestCaseParser.TestUnitData; 165 private harnessSettings: TestCaseParser.CompilerSettings; 166 private hasNonDtsFiles: boolean; 167 private result: compiler.CompilationResult; 168 private options: ts.CompilerOptions; 169 private tsConfigFiles: Compiler.TestFile[]; 170 // equivalent to the files that will be passed on the command line 171 private toBeCompiled: Compiler.TestFile[]; 172 // equivalent to other files on the file system not directly passed to the compiler (ie things that are referenced by other files) 173 private otherFiles: Compiler.TestFile[]; 174 175 constructor(fileName: string, testCaseContent?: TestCaseParser.TestCaseContent, configurationOverrides?: TestCaseParser.CompilerSettings) { 176 this.fileName = fileName; 177 this.justName = vpath.basename(fileName); 178 this.configuredName = this.justName; 179 if (configurationOverrides) { 180 let configuredName = ""; 181 const keys = Object 182 .keys(configurationOverrides) 183 .sort(); 184 for (const key of keys) { 185 if (configuredName) { 186 configuredName += ","; 187 } 188 configuredName += `${key.toLowerCase()}=${configurationOverrides[key].toLowerCase()}`; 189 } 190 if (configuredName) { 191 const extname = vpath.extname(this.justName); 192 const basename = vpath.basename(this.justName, extname, /*ignoreCase*/ true); 193 this.configuredName = `${basename}(${configuredName})${extname}`; 194 } 195 } 196 197 const rootDir = fileName.indexOf("conformance") === -1 ? "tests/cases/compiler/" : ts.getDirectoryPath(fileName) + "/"; 198 199 if (testCaseContent === undefined) { 200 testCaseContent = TestCaseParser.makeUnitsFromTest(IO.readFile(fileName)!, fileName, rootDir); 201 } 202 203 if (configurationOverrides) { 204 testCaseContent = { ...testCaseContent, settings: { ...testCaseContent.settings, ...configurationOverrides } }; 205 } 206 207 const units = testCaseContent.testUnitData; 208 this.harnessSettings = testCaseContent.settings; 209 let tsConfigOptions: ts.CompilerOptions | undefined; 210 this.tsConfigFiles = []; 211 if (testCaseContent.tsConfig) { 212 assert.equal(testCaseContent.tsConfig.fileNames.length, 0, `list of files in tsconfig is not currently supported`); 213 assert.equal(testCaseContent.tsConfig.raw.exclude, undefined, `exclude in tsconfig is not currently supported`); 214 215 tsConfigOptions = ts.cloneCompilerOptions(testCaseContent.tsConfig.options); 216 this.tsConfigFiles.push(this.createHarnessTestFile(testCaseContent.tsConfigFileUnitData!, rootDir, ts.combinePaths(rootDir, tsConfigOptions.configFilePath))); 217 } 218 else { 219 const baseUrl = this.harnessSettings.baseUrl; 220 if (baseUrl !== undefined && !ts.isRootedDiskPath(baseUrl)) { 221 this.harnessSettings.baseUrl = ts.getNormalizedAbsolutePath(baseUrl, rootDir); 222 } 223 } 224 225 this.lastUnit = units[units.length - 1]; 226 this.hasNonDtsFiles = units.some(unit => !ts.isDeclarationFileName(unit.name)); 227 // 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) 228 // 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, 229 // otherwise, assume all files are just meant to be in the same compilation session without explicit references to one another. 230 this.toBeCompiled = []; 231 this.otherFiles = []; 232 233 if (testCaseContent.settings.noImplicitReferences || /require\(/.test(this.lastUnit.content) || /reference\spath/.test(this.lastUnit.content)) { 234 this.toBeCompiled.push(this.createHarnessTestFile(this.lastUnit, rootDir)); 235 units.forEach(unit => { 236 if (unit.name !== this.lastUnit.name) { 237 this.otherFiles.push(this.createHarnessTestFile(unit, rootDir)); 238 } 239 }); 240 } 241 else { 242 this.toBeCompiled = units.map(unit => { 243 return this.createHarnessTestFile(unit, rootDir); 244 }); 245 } 246 247 if (tsConfigOptions && tsConfigOptions.configFilePath !== undefined) { 248 tsConfigOptions.configFilePath = ts.combinePaths(rootDir, tsConfigOptions.configFilePath); 249 tsConfigOptions.configFile!.fileName = tsConfigOptions.configFilePath; 250 } 251 252 this.result = Compiler.compileFiles( 253 this.toBeCompiled, 254 this.otherFiles, 255 this.harnessSettings, 256 /*options*/ tsConfigOptions, 257 /*currentDirectory*/ this.harnessSettings.currentDirectory, 258 testCaseContent.symlinks 259 ); 260 261 this.options = this.result.options; 262 } 263 264 public static getConfigurations(file: string): CompilerFileBasedTest { 265 // also see `parseCompilerTestConfigurations` in tests/webTestServer.ts 266 const content = IO.readFile(file)!; 267 const settings = TestCaseParser.extractCompilerSettings(content); 268 const configurations = getFileBasedTestConfigurations(settings, CompilerTest.varyBy); 269 return { file, configurations, content }; 270 } 271 272 public verifyDiagnostics() { 273 // check errors 274 Compiler.doErrorBaseline( 275 this.configuredName, 276 this.tsConfigFiles.concat(this.toBeCompiled, this.otherFiles), 277 this.result.diagnostics, 278 !!this.options.pretty); 279 } 280 281 public verifyModuleResolution() { 282 if (this.options.traceResolution) { 283 Baseline.runBaseline(this.configuredName.replace(/\.(ets|tsx?)$/, ".trace.json"), 284 JSON.stringify(this.result.traces.map(Utils.sanitizeTraceResolutionLogEntry), undefined, 4)); 285 } 286 } 287 288 public verifySourceMapRecord() { 289 if (this.options.sourceMap || this.options.inlineSourceMap || this.options.declarationMap) { 290 const record = Utils.removeTestPathPrefixes(this.result.getSourceMapRecord()!); 291 const baseline = (this.options.noEmitOnError && this.result.diagnostics.length !== 0) || record === undefined 292 // Because of the noEmitOnError option no files are created. We need to return null because baselining isn't required. 293 ? null // eslint-disable-line no-null/no-null 294 : record; 295 Baseline.runBaseline(this.configuredName.replace(/\.(ets|tsx?)$/, ".sourcemap.txt"), baseline); 296 } 297 } 298 299 public verifyJavaScriptOutput() { 300 if (this.hasNonDtsFiles) { 301 Compiler.doJsEmitBaseline( 302 this.configuredName, 303 this.fileName, 304 this.options, 305 this.result, 306 this.tsConfigFiles, 307 this.toBeCompiled, 308 this.otherFiles, 309 this.harnessSettings); 310 } 311 } 312 313 public verifySourceMapOutput() { 314 Compiler.doSourcemapBaseline( 315 this.configuredName, 316 this.options, 317 this.result, 318 this.harnessSettings); 319 } 320 321 public verifyTypesAndSymbols() { 322 if (this.fileName.indexOf("APISample") >= 0) { 323 return; 324 } 325 326 const noTypesAndSymbols = 327 this.harnessSettings.noTypesAndSymbols && 328 this.harnessSettings.noTypesAndSymbols.toLowerCase() === "true"; 329 if (noTypesAndSymbols) { 330 return; 331 } 332 333 Compiler.doTypeAndSymbolBaseline( 334 this.configuredName, 335 this.result.program!, 336 this.toBeCompiled.concat(this.otherFiles).filter(file => !!this.result.program!.getSourceFile(file.unitName)), 337 /*opts*/ undefined, 338 /*multifile*/ undefined, 339 /*skipTypeBaselines*/ undefined, 340 /*skipSymbolBaselines*/ undefined, 341 !!ts.length(this.result.diagnostics) 342 ); 343 } 344 345 private makeUnitName(name: string, root: string) { 346 const path = ts.toPath(name, root, ts.identity); 347 const pathStart = ts.toPath(IO.getCurrentDirectory(), "", ts.identity); 348 return pathStart ? path.replace(pathStart, "/") : path; 349 } 350 351 private createHarnessTestFile(lastUnit: TestCaseParser.TestUnitData, rootDir: string, unitName?: string): Compiler.TestFile { 352 return { unitName: unitName || this.makeUnitName(lastUnit.name, rootDir), content: lastUnit.content, fileOptions: lastUnit.fileOptions }; 353 } 354} 355