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