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