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