1import * as ts from "./_namespaces/ts"; 2import * as documents from "./_namespaces/documents"; 3import * as Harness from "./_namespaces/Harness"; 4import * as fakes from "./_namespaces/fakes"; 5import * as vfs from "./_namespaces/vfs"; 6import * as vpath from "./_namespaces/vpath"; 7import * as Utils from "./_namespaces/Utils"; 8 9// Test case is json of below type in tests/cases/project/ 10interface ProjectRunnerTestCase { 11 scenario: string; 12 projectRoot: string; // project where it lives - this also is the current directory when compiling 13 inputFiles: readonly string[]; // list of input files to be given to program 14 resolveMapRoot?: boolean; // should we resolve this map root and give compiler the absolute disk path as map root? 15 resolveSourceRoot?: boolean; // should we resolve this source root and give compiler the absolute disk path as map root? 16 baselineCheck?: boolean; // Verify the baselines of output files, if this is false, we will write to output to the disk but there is no verification of baselines 17 runTest?: boolean; // Run the resulting test 18 bug?: string; // If there is any bug associated with this test case 19} 20 21interface ProjectRunnerTestCaseResolutionInfo extends ProjectRunnerTestCase { 22 // Apart from actual test case the results of the resolution 23 resolvedInputFiles: readonly string[]; // List of files that were asked to read by compiler 24 emittedFiles: readonly string[]; // List of files that were emitted by the compiler 25} 26 27interface CompileProjectFilesResult { 28 configFileSourceFiles: readonly ts.SourceFile[]; 29 moduleKind: ts.ModuleKind; 30 program?: ts.Program; 31 compilerOptions?: ts.CompilerOptions; 32 errors: readonly ts.Diagnostic[]; 33 sourceMapData?: readonly ts.SourceMapEmitResult[]; 34} 35 36interface BatchCompileProjectTestCaseResult extends CompileProjectFilesResult { 37 outputFiles?: readonly documents.TextDocument[]; 38} 39 40export class ProjectRunner extends Harness.RunnerBase { 41 public enumerateTestFiles() { 42 const all = this.enumerateFiles("tests/cases/project", /\.json$/, { recursive: true }); 43 if (Harness.shards === 1) { 44 return all; 45 } 46 return all.filter((_val, idx) => idx % Harness.shards === (Harness.shardId - 1)); 47 } 48 49 public kind(): Harness.TestRunnerKind { 50 return "project"; 51 } 52 53 public initializeTests() { 54 describe("projects tests", () => { 55 const tests = this.tests.length === 0 ? this.enumerateTestFiles() : this.tests; 56 for (const test of tests) { 57 this.runProjectTestCase(typeof test === "string" ? test : test.file); 58 } 59 }); 60 } 61 62 private runProjectTestCase(testCaseFileName: string) { 63 for (const { name, payload } of ProjectTestCase.getConfigurations(testCaseFileName)) { 64 describe("Compiling project for " + payload.testCase.scenario + ": testcase " + testCaseFileName + (name ? ` (${name})` : ``), () => { 65 let projectTestCase: ProjectTestCase | undefined; 66 before(() => { 67 projectTestCase = new ProjectTestCase(testCaseFileName, payload); 68 }); 69 it(`Correct module resolution tracing for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyResolution()); 70 it(`Correct errors for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyDiagnostics()); 71 it(`Correct JS output for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyJavaScriptOutput()); 72 // NOTE: This check was commented out in previous code. Leaving this here to eventually be restored if needed. 73 // it(`Correct sourcemap content for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifySourceMapRecord()); 74 it(`Correct declarations for ${testCaseFileName}`, () => projectTestCase && projectTestCase.verifyDeclarations()); 75 after(() => { 76 projectTestCase = undefined; 77 }); 78 }); 79 } 80 } 81} 82 83class ProjectCompilerHost extends fakes.CompilerHost { 84 private _testCase: ProjectRunnerTestCase & ts.CompilerOptions; 85 private _projectParseConfigHost: ProjectParseConfigHost | undefined; 86 87 constructor(sys: fakes.System | vfs.FileSystem, compilerOptions: ts.CompilerOptions, _testCaseJustName: string, testCase: ProjectRunnerTestCase & ts.CompilerOptions, _moduleKind: ts.ModuleKind) { 88 super(sys, compilerOptions); 89 this._testCase = testCase; 90 } 91 92 public get parseConfigHost(): fakes.ParseConfigHost { 93 return this._projectParseConfigHost || (this._projectParseConfigHost = new ProjectParseConfigHost(this.sys, this._testCase)); 94 } 95 96 public getDefaultLibFileName(_options: ts.CompilerOptions) { 97 return vpath.resolve(this.getDefaultLibLocation(), "lib.es5.d.ts"); 98 } 99} 100 101class ProjectParseConfigHost extends fakes.ParseConfigHost { 102 private _testCase: ProjectRunnerTestCase & ts.CompilerOptions; 103 104 constructor(sys: fakes.System, testCase: ProjectRunnerTestCase & ts.CompilerOptions) { 105 super(sys); 106 this._testCase = testCase; 107 } 108 109 public readDirectory(path: string, extensions: string[], excludes: string[], includes: string[], depth: number): string[] { 110 const result = super.readDirectory(path, extensions, excludes, includes, depth); 111 const projectRoot = vpath.resolve(vfs.srcFolder, this._testCase.projectRoot); 112 return result.map(item => vpath.relative( 113 projectRoot, 114 vpath.resolve(projectRoot, item), 115 this.vfs.ignoreCase 116 )); 117 } 118} 119 120interface ProjectTestConfiguration { 121 name: string; 122 payload: ProjectTestPayload; 123} 124 125interface ProjectTestPayload { 126 testCase: ProjectRunnerTestCase & ts.CompilerOptions; 127 moduleKind: ts.ModuleKind; 128 vfs: vfs.FileSystem; 129} 130 131class ProjectTestCase { 132 private testCase: ProjectRunnerTestCase & ts.CompilerOptions; 133 private testCaseJustName: string; 134 private sys: fakes.System; 135 private compilerOptions: ts.CompilerOptions; 136 private compilerResult: BatchCompileProjectTestCaseResult; 137 138 constructor(testCaseFileName: string, { testCase, moduleKind, vfs }: ProjectTestPayload) { 139 this.testCase = testCase; 140 this.testCaseJustName = testCaseFileName.replace(/^.*[\\\/]/, "").replace(/\.json/, ""); 141 this.compilerOptions = createCompilerOptions(testCase, moduleKind); 142 this.sys = new fakes.System(vfs); 143 144 let configFileName: string | undefined; 145 let inputFiles = testCase.inputFiles; 146 if (this.compilerOptions.project) { 147 // Parse project 148 configFileName = ts.normalizePath(ts.combinePaths(this.compilerOptions.project, "tsconfig.json")); 149 assert(!inputFiles || inputFiles.length === 0, "cannot specify input files and project option together"); 150 } 151 else if (!inputFiles || inputFiles.length === 0) { 152 configFileName = ts.findConfigFile("", path => this.sys.fileExists(path)); 153 } 154 155 let errors: ts.Diagnostic[] | undefined; 156 const configFileSourceFiles: ts.SourceFile[] = []; 157 if (configFileName) { 158 const result = ts.readJsonConfigFile(configFileName, path => this.sys.readFile(path)); 159 configFileSourceFiles.push(result); 160 const configParseHost = new ProjectParseConfigHost(this.sys, this.testCase); 161 const configParseResult = ts.parseJsonSourceFileConfigFileContent(result, configParseHost, ts.getDirectoryPath(configFileName), this.compilerOptions); 162 inputFiles = configParseResult.fileNames; 163 this.compilerOptions = configParseResult.options; 164 errors = [...result.parseDiagnostics, ...configParseResult.errors]; 165 } 166 167 const compilerHost = new ProjectCompilerHost(this.sys, this.compilerOptions, this.testCaseJustName, this.testCase, moduleKind); 168 const projectCompilerResult = this.compileProjectFiles(moduleKind, configFileSourceFiles, () => inputFiles, compilerHost, this.compilerOptions); 169 170 this.compilerResult = { 171 configFileSourceFiles, 172 moduleKind, 173 program: projectCompilerResult.program, 174 compilerOptions: this.compilerOptions, 175 sourceMapData: projectCompilerResult.sourceMapData, 176 outputFiles: compilerHost.outputs, 177 errors: errors ? ts.concatenate(errors, projectCompilerResult.errors) : projectCompilerResult.errors, 178 }; 179 } 180 181 private get vfs() { 182 return this.sys.vfs; 183 } 184 185 public static getConfigurations(testCaseFileName: string): ProjectTestConfiguration[] { 186 let testCase: ProjectRunnerTestCase & ts.CompilerOptions; 187 188 let testFileText: string | undefined; 189 try { 190 testFileText = Harness.IO.readFile(testCaseFileName); 191 } 192 catch (e) { 193 assert(false, "Unable to open testcase file: " + testCaseFileName + ": " + e.message); 194 } 195 196 try { 197 testCase = JSON.parse(testFileText!) as ProjectRunnerTestCase & ts.CompilerOptions; 198 } 199 catch (e) { 200 throw assert(false, "Testcase: " + testCaseFileName + " does not contain valid json format: " + e.message); 201 } 202 203 const fs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false); 204 fs.mountSync(vpath.resolve(Harness.IO.getWorkspaceRoot(), "tests"), vpath.combine(vfs.srcFolder, "tests"), vfs.createResolver(Harness.IO)); 205 fs.mkdirpSync(vpath.combine(vfs.srcFolder, testCase.projectRoot)); 206 fs.chdir(vpath.combine(vfs.srcFolder, testCase.projectRoot)); 207 fs.makeReadonly(); 208 209 return [ 210 { name: `@module: commonjs`, payload: { testCase, moduleKind: ts.ModuleKind.CommonJS, vfs: fs } }, 211 { name: `@module: amd`, payload: { testCase, moduleKind: ts.ModuleKind.AMD, vfs: fs } } 212 ]; 213 } 214 215 public verifyResolution() { 216 const cwd = this.vfs.cwd(); 217 const ignoreCase = this.vfs.ignoreCase; 218 const resolutionInfo: ProjectRunnerTestCaseResolutionInfo & ts.CompilerOptions = JSON.parse(JSON.stringify(this.testCase)); 219 resolutionInfo.resolvedInputFiles = this.compilerResult.program!.getSourceFiles() 220 .map(({ fileName: input }) => 221 vpath.beneath(vfs.builtFolder, input, this.vfs.ignoreCase) || vpath.beneath(vfs.testLibFolder, input, this.vfs.ignoreCase) ? Utils.removeTestPathPrefixes(input) : 222 vpath.isAbsolute(input) ? vpath.relative(cwd, input, ignoreCase) : 223 input); 224 225 resolutionInfo.emittedFiles = this.compilerResult.outputFiles! 226 .map(output => output.meta.get("fileName") || output.file) 227 .map(output => Utils.removeTestPathPrefixes(vpath.isAbsolute(output) ? vpath.relative(cwd, output, ignoreCase) : output)); 228 229 const content = JSON.stringify(resolutionInfo, undefined, " "); 230 Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".json", content); 231 } 232 233 public verifyDiagnostics() { 234 if (this.compilerResult.errors.length) { 235 Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".errors.txt", getErrorsBaseline(this.compilerResult)); 236 } 237 } 238 239 public verifyJavaScriptOutput() { 240 if (this.testCase.baselineCheck) { 241 const errs: Error[] = []; 242 let nonSubfolderDiskFiles = 0; 243 for (const output of this.compilerResult.outputFiles!) { 244 try { 245 // convert file name to rooted name 246 // if filename is not rooted - concat it with project root and then expand project root relative to current directory 247 const fileName = output.meta.get("fileName") || output.file; 248 const diskFileName = vpath.isAbsolute(fileName) ? fileName : vpath.resolve(this.vfs.cwd(), fileName); 249 250 // compute file name relative to current directory (expanded project root) 251 let diskRelativeName = vpath.relative(this.vfs.cwd(), diskFileName, this.vfs.ignoreCase); 252 if (vpath.isAbsolute(diskRelativeName) || diskRelativeName.startsWith("../")) { 253 // If the generated output file resides in the parent folder or is rooted path, 254 // we need to instead create files that can live in the project reference folder 255 // but make sure extension of these files matches with the fileName the compiler asked to write 256 diskRelativeName = `diskFile${nonSubfolderDiskFiles}${vpath.extname(fileName, [".js.map", ".js", ".d.ts"], this.vfs.ignoreCase)}`; 257 nonSubfolderDiskFiles++; 258 } 259 260 const content = Utils.removeTestPathPrefixes(output.text, /*retainTrailingDirectorySeparator*/ true); 261 Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + diskRelativeName, content as string | null); // TODO: GH#18217 262 } 263 catch (e) { 264 errs.push(e); 265 } 266 } 267 268 if (errs.length) { 269 throw Error(errs.join("\n ")); 270 } 271 } 272 } 273 274 public verifySourceMapRecord() { 275 // NOTE: This check was commented out in previous code. Leaving this here to eventually be restored if needed. 276 // if (compilerResult.sourceMapData) { 277 // Harness.Baseline.runBaseline(getBaselineFolder(compilerResult.moduleKind) + testCaseJustName + ".sourcemap.txt", () => { 278 // return Harness.SourceMapRecorder.getSourceMapRecord(compilerResult.sourceMapData, compilerResult.program, 279 // ts.filter(compilerResult.outputFiles, outputFile => Harness.Compiler.isJS(outputFile.emittedFileName))); 280 // }); 281 // } 282 } 283 284 public verifyDeclarations() { 285 if (!this.compilerResult.errors.length && this.testCase.declaration) { 286 const dTsCompileResult = this.compileDeclarations(this.compilerResult); 287 if (dTsCompileResult && dTsCompileResult.errors.length) { 288 Harness.Baseline.runBaseline(this.getBaselineFolder(this.compilerResult.moduleKind) + this.testCaseJustName + ".dts.errors.txt", getErrorsBaseline(dTsCompileResult)); 289 } 290 } 291 } 292 293 // Project baselines verified go in project/testCaseName/moduleKind/ 294 private getBaselineFolder(moduleKind: ts.ModuleKind) { 295 return "project/" + this.testCaseJustName + "/" + moduleNameToString(moduleKind) + "/"; 296 } 297 298 private cleanProjectUrl(url: string) { 299 let diskProjectPath = ts.normalizeSlashes(Harness.IO.resolvePath(this.testCase.projectRoot)!); 300 let projectRootUrl = "file:///" + diskProjectPath; 301 const normalizedProjectRoot = ts.normalizeSlashes(this.testCase.projectRoot); 302 diskProjectPath = diskProjectPath.substr(0, diskProjectPath.lastIndexOf(normalizedProjectRoot)); 303 projectRootUrl = projectRootUrl.substr(0, projectRootUrl.lastIndexOf(normalizedProjectRoot)); 304 if (url && url.length) { 305 if (url.indexOf(projectRootUrl) === 0) { 306 // replace the disk specific project url path into project root url 307 url = "file:///" + url.substr(projectRootUrl.length); 308 } 309 else if (url.indexOf(diskProjectPath) === 0) { 310 // Replace the disk specific path into the project root path 311 url = url.substr(diskProjectPath.length); 312 if (url.charCodeAt(0) !== ts.CharacterCodes.slash) { 313 url = "/" + url; 314 } 315 } 316 } 317 318 return url; 319 } 320 321 private compileProjectFiles(moduleKind: ts.ModuleKind, configFileSourceFiles: readonly ts.SourceFile[], 322 getInputFiles: () => readonly string[], 323 compilerHost: ts.CompilerHost, 324 compilerOptions: ts.CompilerOptions): CompileProjectFilesResult { 325 326 const program = ts.createProgram(getInputFiles(), compilerOptions, compilerHost); 327 const errors = ts.getPreEmitDiagnostics(program); 328 329 const { sourceMaps: sourceMapData, diagnostics: emitDiagnostics } = program.emit(); 330 331 // Clean up source map data that will be used in baselining 332 if (sourceMapData) { 333 for (const data of sourceMapData) { 334 data.sourceMap = { 335 ...data.sourceMap, 336 sources: data.sourceMap.sources.map(source => this.cleanProjectUrl(source)), 337 sourceRoot: data.sourceMap.sourceRoot && this.cleanProjectUrl(data.sourceMap.sourceRoot) 338 }; 339 } 340 } 341 342 return { 343 configFileSourceFiles, 344 moduleKind, 345 program, 346 errors: ts.concatenate(errors, emitDiagnostics), 347 sourceMapData 348 }; 349 } 350 351 private compileDeclarations(compilerResult: BatchCompileProjectTestCaseResult) { 352 if (!compilerResult.program) { 353 return; 354 } 355 356 const compilerOptions = compilerResult.program.getCompilerOptions(); 357 const allInputFiles: documents.TextDocument[] = []; 358 const rootFiles: string[] = []; 359 ts.forEach(compilerResult.program.getSourceFiles(), sourceFile => { 360 if (sourceFile.isDeclarationFile) { 361 if (!vpath.isDefaultLibrary(sourceFile.fileName)) { 362 allInputFiles.unshift(new documents.TextDocument(sourceFile.fileName, sourceFile.text)); 363 } 364 rootFiles.unshift(sourceFile.fileName); 365 } 366 else if (!(compilerOptions.outFile || compilerOptions.out)) { 367 let emitOutputFilePathWithoutExtension: string | undefined; 368 if (compilerOptions.outDir) { 369 let sourceFilePath = ts.getNormalizedAbsolutePath(sourceFile.fileName, compilerResult.program!.getCurrentDirectory()); 370 sourceFilePath = sourceFilePath.replace(compilerResult.program!.getCommonSourceDirectory(), ""); 371 emitOutputFilePathWithoutExtension = ts.removeFileExtension(ts.combinePaths(compilerOptions.outDir, sourceFilePath)); 372 } 373 else { 374 emitOutputFilePathWithoutExtension = ts.removeFileExtension(sourceFile.fileName); 375 } 376 377 const outputDtsFileName = emitOutputFilePathWithoutExtension + ts.Extension.Dts; 378 const file = findOutputDtsFile(outputDtsFileName); 379 if (file) { 380 allInputFiles.unshift(file); 381 rootFiles.unshift(file.meta.get("fileName") || file.file); 382 } 383 } 384 else { 385 const outputDtsFileName = ts.removeFileExtension(compilerOptions.outFile || compilerOptions.out!) + ts.Extension.Dts; 386 const outputDtsFile = findOutputDtsFile(outputDtsFileName)!; 387 if (!ts.contains(allInputFiles, outputDtsFile)) { 388 allInputFiles.unshift(outputDtsFile); 389 rootFiles.unshift(outputDtsFile.meta.get("fileName") || outputDtsFile.file); 390 } 391 } 392 }); 393 394 const _vfs = vfs.createFromFileSystem(Harness.IO, /*ignoreCase*/ false, { 395 documents: allInputFiles, 396 cwd: vpath.combine(vfs.srcFolder, this.testCase.projectRoot) 397 }); 398 399 // Dont allow config files since we are compiling existing source options 400 const compilerHost = new ProjectCompilerHost(_vfs, compilerResult.compilerOptions!, this.testCaseJustName, this.testCase, compilerResult.moduleKind); 401 return this.compileProjectFiles(compilerResult.moduleKind, compilerResult.configFileSourceFiles, () => rootFiles, compilerHost, compilerResult.compilerOptions!); 402 403 function findOutputDtsFile(fileName: string) { 404 return ts.forEach(compilerResult.outputFiles, outputFile => outputFile.meta.get("fileName") === fileName ? outputFile : undefined); 405 } 406 } 407} 408 409function moduleNameToString(moduleKind: ts.ModuleKind) { 410 return moduleKind === ts.ModuleKind.AMD ? "amd" : 411 moduleKind === ts.ModuleKind.CommonJS ? "node" : "none"; 412} 413 414function getErrorsBaseline(compilerResult: CompileProjectFilesResult) { 415 const inputSourceFiles = compilerResult.configFileSourceFiles.slice(); 416 if (compilerResult.program) { 417 for (const sourceFile of compilerResult.program.getSourceFiles()) { 418 if (!Harness.isDefaultLibraryFile(sourceFile.fileName)) { 419 inputSourceFiles.push(sourceFile); 420 } 421 } 422 } 423 424 const inputFiles = inputSourceFiles.map<Harness.Compiler.TestFile>(sourceFile => ({ 425 unitName: ts.isRootedDiskPath(sourceFile.fileName) ? 426 Harness.RunnerBase.removeFullPaths(sourceFile.fileName) : 427 sourceFile.fileName, 428 content: sourceFile.text 429 })); 430 431 return Harness.Compiler.getErrorBaseline(inputFiles, compilerResult.errors); 432} 433 434function createCompilerOptions(testCase: ProjectRunnerTestCase & ts.CompilerOptions, moduleKind: ts.ModuleKind) { 435 // Set the special options that depend on other testcase options 436 const compilerOptions: ts.CompilerOptions = { 437 noErrorTruncation: false, 438 skipDefaultLibCheck: false, 439 moduleResolution: ts.ModuleResolutionKind.Classic, 440 module: moduleKind, 441 newLine: ts.NewLineKind.CarriageReturnLineFeed, 442 mapRoot: testCase.resolveMapRoot && testCase.mapRoot 443 ? vpath.resolve(vfs.srcFolder, testCase.mapRoot) 444 : testCase.mapRoot, 445 446 sourceRoot: testCase.resolveSourceRoot && testCase.sourceRoot 447 ? vpath.resolve(vfs.srcFolder, testCase.sourceRoot) 448 : testCase.sourceRoot 449 }; 450 451 // Set the values specified using json 452 const optionNameMap = ts.arrayToMap(ts.optionDeclarations, option => option.name); 453 for (const name in testCase) { 454 if (name !== "mapRoot" && name !== "sourceRoot") { 455 const option = optionNameMap.get(name); 456 if (option) { 457 const optType = option.type; 458 let value = testCase[name] as any; 459 if (!ts.isString(optType)) { 460 const key = value.toLowerCase(); 461 const optTypeValue = optType.get(key); 462 if (optTypeValue) { 463 value = optTypeValue; 464 } 465 } 466 compilerOptions[option.name] = value; 467 } 468 } 469 } 470 471 return compilerOptions; 472} 473