• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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